index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. <template>
  2. <modal
  3. v-if="station"
  4. :title="
  5. sector === 'home' && !isOwnerOrAdmin()
  6. ? 'View Queue'
  7. : !isOwnerOrAdmin() && station.partyMode
  8. ? 'Add Song to Queue'
  9. : 'Manage Station'
  10. "
  11. :style="`--primary-color: var(--${station.theme})`"
  12. class="manage-station-modal"
  13. :size="isOwnerOrAdmin() || sector !== 'home' ? 'wide' : null"
  14. :split="isOwnerOrAdmin() || sector !== 'home'"
  15. >
  16. <template #body v-if="station && station._id">
  17. <div class="left-section">
  18. <div class="section">
  19. <div id="about-station-container">
  20. <div id="station-info">
  21. <div id="station-name">
  22. <h1>{{ station.displayName }}</h1>
  23. <i
  24. v-if="station.type === 'official'"
  25. class="material-icons verified-station"
  26. content="Verified Station"
  27. v-tippy
  28. >
  29. check_circle
  30. </i>
  31. <i
  32. class="material-icons stationMode"
  33. :content="
  34. station.partyMode
  35. ? 'Station in Party mode'
  36. : 'Station in Playlist mode'
  37. "
  38. v-tippy
  39. >{{
  40. station.partyMode
  41. ? "emoji_people"
  42. : "playlist_play"
  43. }}</i
  44. >
  45. </div>
  46. <p>{{ station.description }}</p>
  47. </div>
  48. <div id="admin-buttons">
  49. <!-- (Admin) Pause/Resume Button -->
  50. <button
  51. v-if="isOwnerOrAdmin() && stationPaused"
  52. class="button is-danger"
  53. @click="resumeStation()"
  54. >
  55. <i class="material-icons icon-with-button"
  56. >play_arrow</i
  57. >
  58. <span> Resume Station </span>
  59. </button>
  60. <button
  61. v-if="isOwnerOrAdmin() && !stationPaused"
  62. class="button is-danger"
  63. @click="pauseStation()"
  64. >
  65. <i class="material-icons icon-with-button"
  66. >pause</i
  67. >
  68. <span> Pause Station </span>
  69. </button>
  70. <!-- (Admin) Skip Button -->
  71. <button
  72. v-if="isOwnerOrAdmin()"
  73. class="button is-danger"
  74. @click="skipStation()"
  75. >
  76. <i class="material-icons icon-with-button"
  77. >skip_next</i
  78. >
  79. <span> Force Skip </span>
  80. </button>
  81. <router-link
  82. v-if="sector !== 'station' && station.name"
  83. :to="{
  84. name: 'station',
  85. params: { id: station.name }
  86. }"
  87. class="button is-primary"
  88. >
  89. Go To Station
  90. </router-link>
  91. </div>
  92. </div>
  93. <div v-if="isOwnerOrAdmin() || sector !== 'home'">
  94. <div class="tab-selection">
  95. <button
  96. v-if="isOwnerOrAdmin()"
  97. class="button is-default"
  98. :class="{ selected: tab === 'settings' }"
  99. ref="settings-tab"
  100. @click="showTab('settings')"
  101. >
  102. Settings
  103. </button>
  104. <button
  105. v-if="isAllowedToParty() || isOwnerOrAdmin()"
  106. class="button is-default"
  107. :class="{ selected: tab === 'playlists' }"
  108. ref="playlists-tab"
  109. @click="showTab('playlists')"
  110. >
  111. Playlists
  112. </button>
  113. <button
  114. v-if="isAllowedToParty() || isOwnerOrAdmin()"
  115. class="button is-default"
  116. :class="{ selected: tab === 'songs' }"
  117. ref="songs-tab"
  118. @click="showTab('songs')"
  119. >
  120. Songs
  121. </button>
  122. </div>
  123. <settings
  124. v-if="isOwnerOrAdmin()"
  125. class="tab"
  126. v-show="tab === 'settings'"
  127. />
  128. <playlists
  129. v-if="isAllowedToParty() || isOwnerOrAdmin()"
  130. class="tab"
  131. v-show="tab === 'playlists'"
  132. />
  133. <songs
  134. v-if="isAllowedToParty() || isOwnerOrAdmin()"
  135. class="tab"
  136. v-show="tab === 'songs'"
  137. />
  138. </div>
  139. </div>
  140. </div>
  141. <div class="right-section">
  142. <div class="section">
  143. <div class="queue-title">
  144. <h4 class="section-title">Queue</h4>
  145. </div>
  146. <hr class="section-horizontal-rule" />
  147. <song-item
  148. v-if="currentSong._id"
  149. :song="currentSong"
  150. :requested-by="
  151. station.type === 'community' &&
  152. station.partyMode === true
  153. "
  154. header="Currently Playing.."
  155. class="currently-playing"
  156. />
  157. <queue sector="manageStation" />
  158. </div>
  159. </div>
  160. </template>
  161. <template #footer>
  162. <div v-if="isOwnerOrAdmin()" class="right">
  163. <quick-confirm @confirm="clearAndRefillStationQueue()">
  164. <a class="button is-danger">
  165. Clear and refill station queue
  166. </a>
  167. </quick-confirm>
  168. <quick-confirm @confirm="removeStation()">
  169. <button class="button is-danger">Delete station</button>
  170. </quick-confirm>
  171. </div>
  172. </template>
  173. </modal>
  174. </template>
  175. <script>
  176. import { mapState, mapGetters, mapActions } from "vuex";
  177. import Toast from "toasters";
  178. import QuickConfirm from "@/components/QuickConfirm.vue";
  179. import Queue from "@/components/Queue.vue";
  180. import SongItem from "@/components/SongItem.vue";
  181. import Modal from "../../Modal.vue";
  182. import Settings from "./Tabs/Settings.vue";
  183. import Playlists from "./Tabs/Playlists.vue";
  184. import Songs from "./Tabs/Songs.vue";
  185. export default {
  186. components: {
  187. Modal,
  188. QuickConfirm,
  189. Queue,
  190. SongItem,
  191. Settings,
  192. Playlists,
  193. Songs
  194. },
  195. props: {
  196. stationId: { type: String, default: "" },
  197. sector: { type: String, default: "admin" }
  198. },
  199. computed: {
  200. ...mapState({
  201. loggedIn: state => state.user.auth.loggedIn,
  202. userId: state => state.user.auth.userId,
  203. role: state => state.user.auth.role
  204. }),
  205. ...mapState("modals/manageStation", {
  206. tab: state => state.tab,
  207. station: state => state.station,
  208. originalStation: state => state.originalStation,
  209. songsList: state => state.songsList,
  210. stationPlaylist: state => state.stationPlaylist,
  211. includedPlaylists: state => state.includedPlaylists,
  212. excludedPlaylists: state => state.excludedPlaylists,
  213. stationPaused: state => state.stationPaused,
  214. currentSong: state => state.currentSong
  215. }),
  216. ...mapGetters({
  217. socket: "websockets/getSocket"
  218. })
  219. },
  220. mounted() {
  221. this.socket.dispatch(`stations.getStationById`, this.stationId, res => {
  222. if (res.status === "success") {
  223. const { station } = res.data;
  224. this.editStation(station);
  225. if (!this.isOwnerOrAdmin() && this.station.partyMode)
  226. this.showTab("songs");
  227. const currentSong = res.data.station.currentSong
  228. ? res.data.station.currentSong
  229. : {};
  230. this.updateCurrentSong(currentSong);
  231. this.updateStationPaused(res.data.station.paused);
  232. this.socket.dispatch(
  233. "stations.getStationIncludedPlaylistsById",
  234. this.stationId,
  235. res => {
  236. if (res.status === "success")
  237. this.setIncludedPlaylists(res.data.playlists);
  238. }
  239. );
  240. this.socket.dispatch(
  241. "stations.getStationExcludedPlaylistsById",
  242. this.stationId,
  243. res => {
  244. if (res.status === "success")
  245. this.setExcludedPlaylists(res.data.playlists);
  246. }
  247. );
  248. if (this.isOwnerOrAdmin()) {
  249. this.socket.dispatch(
  250. "playlists.getPlaylistForStation",
  251. this.station._id,
  252. true,
  253. res => {
  254. if (res.status === "success") {
  255. this.updateStationPlaylist(res.data.playlist);
  256. }
  257. }
  258. );
  259. }
  260. this.socket.dispatch(
  261. "stations.getQueue",
  262. this.stationId,
  263. res => {
  264. if (res.status === "success")
  265. this.updateSongsList(res.data.queue);
  266. }
  267. );
  268. this.socket.dispatch(
  269. "apis.joinRoom",
  270. `manage-station.${this.stationId}`
  271. );
  272. this.socket.on(
  273. "event:station.name.updated",
  274. res => {
  275. this.station.name = res.data.name;
  276. },
  277. { modal: "manageStation" }
  278. );
  279. this.socket.on(
  280. "event:station.displayName.updated",
  281. res => {
  282. this.station.displayName = res.data.displayName;
  283. },
  284. { modal: "manageStation" }
  285. );
  286. this.socket.on(
  287. "event:station.description.updated",
  288. res => {
  289. this.station.description = res.data.description;
  290. },
  291. { modal: "manageStation" }
  292. );
  293. this.socket.on(
  294. "event:station.partyMode.updated",
  295. res => {
  296. if (this.station.type === "community")
  297. this.station.partyMode = res.data.partyMode;
  298. },
  299. { modal: "manageStation" }
  300. );
  301. this.socket.on(
  302. "event:station.playMode.updated",
  303. res => {
  304. this.station.playMode = res.data.playMode;
  305. },
  306. { modal: "manageStation" }
  307. );
  308. this.socket.on(
  309. "event:station.theme.updated",
  310. res => {
  311. const { theme } = res.data;
  312. this.station.theme = theme;
  313. },
  314. { modal: "manageStation" }
  315. );
  316. this.socket.on(
  317. "event:station.privacy.updated",
  318. res => {
  319. this.station.privacy = res.data.privacy;
  320. },
  321. { modal: "manageStation" }
  322. );
  323. this.socket.on(
  324. "event:station.queue.lock.toggled",
  325. res => {
  326. this.station.locked = res.data.locked;
  327. },
  328. { modal: "manageStation" }
  329. );
  330. this.socket.on(
  331. "event:station.includedPlaylist",
  332. res => {
  333. const { playlist } = res.data;
  334. const playlistIndex = this.includedPlaylists
  335. .map(includedPlaylist => includedPlaylist._id)
  336. .indexOf(playlist._id);
  337. if (playlistIndex === -1)
  338. this.includedPlaylists.push(playlist);
  339. },
  340. { modal: "manageStation" }
  341. );
  342. this.socket.on(
  343. "event:station.excludedPlaylist",
  344. res => {
  345. const { playlist } = res.data;
  346. const playlistIndex = this.excludedPlaylists
  347. .map(excludedPlaylist => excludedPlaylist._id)
  348. .indexOf(playlist._id);
  349. if (playlistIndex === -1)
  350. this.excludedPlaylists.push(playlist);
  351. },
  352. { modal: "manageStation" }
  353. );
  354. this.socket.on(
  355. "event:station.removedIncludedPlaylist",
  356. res => {
  357. const { playlistId } = res.data;
  358. const playlistIndex = this.includedPlaylists
  359. .map(playlist => playlist._id)
  360. .indexOf(playlistId);
  361. if (playlistIndex >= 0)
  362. this.includedPlaylists.splice(playlistIndex, 1);
  363. },
  364. { modal: "manageStation" }
  365. );
  366. this.socket.on(
  367. "event:station.removedExcludedPlaylist",
  368. res => {
  369. const { playlistId } = res.data;
  370. const playlistIndex = this.excludedPlaylists
  371. .map(playlist => playlist._id)
  372. .indexOf(playlistId);
  373. if (playlistIndex >= 0)
  374. this.excludedPlaylists.splice(playlistIndex, 1);
  375. },
  376. { modal: "manageStation" }
  377. );
  378. this.socket.on(
  379. "event:station.deleted",
  380. () => {
  381. new Toast(`The station you were editing was deleted.`);
  382. this.closeModal("manageStation");
  383. },
  384. { modal: "manageStation" }
  385. );
  386. } else {
  387. new Toast(`Station with that ID not found`);
  388. this.closeModal("manageStation");
  389. }
  390. });
  391. this.socket.on(
  392. "event:manageStation.queue.updated",
  393. res => {
  394. if (res.data.stationId === this.station._id)
  395. this.updateSongsList(res.data.queue);
  396. },
  397. { modal: "manageStation" }
  398. );
  399. this.socket.on(
  400. "event:manageStation.queue.song.repositioned",
  401. res => {
  402. if (res.data.stationId === this.station._id)
  403. this.repositionSongInList(res.data.song);
  404. },
  405. { modal: "manageStation" }
  406. );
  407. this.socket.on(
  408. "event:station.pause",
  409. res => {
  410. if (res.data.stationId === this.station._id)
  411. this.updateStationPaused(true);
  412. },
  413. { modal: "manageStation" }
  414. );
  415. this.socket.on(
  416. "event:station.resume",
  417. res => {
  418. if (res.data.stationId === this.station._id)
  419. this.updateStationPaused(false);
  420. },
  421. { modal: "manageStation" }
  422. );
  423. this.socket.on(
  424. "event:station.nextSong",
  425. res => {
  426. if (res.data.stationId === this.station._id)
  427. this.updateCurrentSong(res.data.currentSong || {});
  428. },
  429. { modal: "manageStation" }
  430. );
  431. if (this.isOwnerOrAdmin()) {
  432. this.socket.on(
  433. "event:playlist.song.added",
  434. res => {
  435. if (this.stationPlaylist._id === res.data.playlistId)
  436. this.stationPlaylist.songs.push(res.data.song);
  437. },
  438. {
  439. modal: "manageStation"
  440. }
  441. );
  442. this.socket.on(
  443. "event:playlist.song.removed",
  444. res => {
  445. if (this.stationPlaylist._id === res.data.playlistId) {
  446. // remove song from array of playlists
  447. this.stationPlaylist.songs.forEach((song, index) => {
  448. if (song.youtubeId === res.data.youtubeId)
  449. this.stationPlaylist.songs.splice(index, 1);
  450. });
  451. }
  452. },
  453. {
  454. modal: "manageStation"
  455. }
  456. );
  457. this.socket.on(
  458. "event:playlist.songs.repositioned",
  459. res => {
  460. if (this.stationPlaylist._id === res.data.playlistId) {
  461. // for each song that has a new position
  462. res.data.songsBeingChanged.forEach(changedSong => {
  463. this.stationPlaylist.songs.forEach(
  464. (song, index) => {
  465. // find song locally
  466. if (
  467. song.youtubeId === changedSong.youtubeId
  468. ) {
  469. // change song position attribute
  470. this.stationPlaylist.songs[
  471. index
  472. ].position = changedSong.position;
  473. // reposition in array if needed
  474. if (index !== changedSong.position - 1)
  475. this.stationPlaylist.songs.splice(
  476. changedSong.position - 1,
  477. 0,
  478. this.stationPlaylist.songs.splice(
  479. index,
  480. 1
  481. )[0]
  482. );
  483. }
  484. }
  485. );
  486. });
  487. }
  488. },
  489. {
  490. modal: "manageStation"
  491. }
  492. );
  493. }
  494. },
  495. beforeUnmount() {
  496. this.socket.dispatch(
  497. "apis.leaveRoom",
  498. `manage-station.${this.stationId}`,
  499. () => {}
  500. );
  501. if (this.isOwnerOrAdmin()) this.showTab("settings");
  502. this.clearStation();
  503. },
  504. methods: {
  505. isOwner() {
  506. return (
  507. this.loggedIn &&
  508. this.station &&
  509. this.userId === this.station.owner
  510. );
  511. },
  512. isAdmin() {
  513. return this.loggedIn && this.role === "admin";
  514. },
  515. isOwnerOrAdmin() {
  516. return this.isOwner() || this.isAdmin();
  517. },
  518. isPartyMode() {
  519. return (
  520. this.station &&
  521. this.station.type === "community" &&
  522. this.station.partyMode
  523. );
  524. },
  525. isAllowedToParty() {
  526. return (
  527. this.station &&
  528. this.isPartyMode() &&
  529. (!this.station.locked || this.isOwnerOrAdmin()) &&
  530. this.loggedIn
  531. );
  532. },
  533. isPlaylistMode() {
  534. return this.station && !this.isPartyMode();
  535. },
  536. removeStation() {
  537. this.socket.dispatch("stations.remove", this.station._id, res => {
  538. new Toast(res.message);
  539. });
  540. },
  541. resumeStation() {
  542. this.socket.dispatch("stations.resume", this.station._id, res => {
  543. if (res.status !== "success")
  544. new Toast(`Error: ${res.message}`);
  545. else new Toast("Successfully resumed the station.");
  546. });
  547. },
  548. pauseStation() {
  549. this.socket.dispatch("stations.pause", this.station._id, res => {
  550. if (res.status !== "success")
  551. new Toast(`Error: ${res.message}`);
  552. else new Toast("Successfully paused the station.");
  553. });
  554. },
  555. skipStation() {
  556. this.socket.dispatch(
  557. "stations.forceSkip",
  558. this.station._id,
  559. res => {
  560. if (res.status !== "success")
  561. new Toast(`Error: ${res.message}`);
  562. else
  563. new Toast(
  564. "Successfully skipped the station's current song."
  565. );
  566. }
  567. );
  568. },
  569. clearAndRefillStationQueue() {
  570. this.socket.dispatch(
  571. "stations.clearAndRefillStationQueue",
  572. this.station._id,
  573. res => {
  574. if (res.status !== "success")
  575. new Toast({
  576. content: `Error: ${res.message}`,
  577. timeout: 8000
  578. });
  579. else new Toast({ content: res.message, timeout: 4000 });
  580. }
  581. );
  582. },
  583. ...mapActions("modals/manageStation", [
  584. "editStation",
  585. "setIncludedPlaylists",
  586. "setExcludedPlaylists",
  587. "clearStation",
  588. "updateSongsList",
  589. "updateStationPlaylist",
  590. "repositionSongInList",
  591. "updateStationPaused",
  592. "updateCurrentSong"
  593. ]),
  594. ...mapActions({
  595. showTab(dispatch, payload) {
  596. if (this.$refs[`${payload}-tab`])
  597. this.$refs[`${payload}-tab`].scrollIntoView({
  598. block: "nearest"
  599. }); // Only works if the ref exists, which it doesn't always
  600. return dispatch("modals/manageStation/showTab", payload);
  601. }
  602. }),
  603. ...mapActions("modalVisibility", ["openModal", "closeModal"]),
  604. ...mapActions("user/playlists", ["editPlaylist"])
  605. }
  606. };
  607. </script>
  608. <style lang="less">
  609. .manage-station-modal.modal .modal-card {
  610. .tab > button {
  611. width: 100%;
  612. margin-bottom: 10px;
  613. }
  614. .currently-playing.song-item {
  615. .thumbnail {
  616. min-width: 130px;
  617. width: 130px;
  618. height: 130px;
  619. }
  620. }
  621. }
  622. </style>
  623. <style lang="less" scoped>
  624. .night-mode {
  625. .manage-station-modal.modal .modal-card-body {
  626. .left-section {
  627. #about-station-container {
  628. background-color: var(--dark-grey-3) !important;
  629. border: 0;
  630. }
  631. .section {
  632. background-color: transparent !important;
  633. }
  634. .tab-selection .button {
  635. background: var(--dark-grey);
  636. color: var(--white);
  637. }
  638. .tab {
  639. background-color: var(--dark-grey-3);
  640. border: 0;
  641. }
  642. }
  643. .right-section .section,
  644. #queue {
  645. border-radius: @border-radius;
  646. background-color: transparent !important;
  647. }
  648. }
  649. }
  650. .manage-station-modal.modal .modal-card-body {
  651. display: flex;
  652. flex-wrap: wrap;
  653. height: 100%;
  654. .left-section {
  655. #about-station-container {
  656. padding: 20px;
  657. display: flex;
  658. flex-direction: column;
  659. flex-grow: unset;
  660. border-radius: @border-radius;
  661. margin: 0 0 20px 0;
  662. background-color: var(--white);
  663. border: 1px solid var(--light-grey-3);
  664. #station-info {
  665. #station-name {
  666. flex-direction: row !important;
  667. display: flex;
  668. flex-direction: row;
  669. max-width: 100%;
  670. h1 {
  671. margin: 0;
  672. font-size: 36px;
  673. line-height: 0.8;
  674. text-overflow: ellipsis;
  675. overflow: hidden;
  676. }
  677. i {
  678. margin-left: 10px;
  679. font-size: 30px;
  680. color: var(--yellow);
  681. &.stationMode {
  682. padding-left: 10px;
  683. margin-left: auto;
  684. color: var(--primary-color);
  685. }
  686. }
  687. .verified-station {
  688. color: var(--primary-color);
  689. }
  690. }
  691. p {
  692. display: -webkit-box;
  693. max-width: 700px;
  694. margin-bottom: 10px;
  695. overflow: hidden;
  696. text-overflow: ellipsis;
  697. -webkit-box-orient: vertical;
  698. -webkit-line-clamp: 3;
  699. }
  700. }
  701. #admin-buttons {
  702. display: flex;
  703. .button {
  704. margin: 3px;
  705. }
  706. }
  707. }
  708. .tab-selection {
  709. display: flex;
  710. overflow-x: auto;
  711. .button {
  712. border-radius: @border-radius @border-radius 0 0;
  713. border: 0;
  714. text-transform: uppercase;
  715. font-size: 14px;
  716. color: var(--dark-grey-3);
  717. background-color: var(--light-grey-2);
  718. flex-grow: 1;
  719. height: 32px;
  720. &:not(:first-of-type) {
  721. margin-left: 5px;
  722. }
  723. }
  724. .selected {
  725. background-color: var(--primary-color) !important;
  726. color: var(--white) !important;
  727. font-weight: 600;
  728. }
  729. }
  730. .tab {
  731. border: 1px solid var(--light-grey-3);
  732. padding: 15px;
  733. border-radius: 0 0 @border-radius @border-radius;
  734. }
  735. }
  736. .right-section {
  737. .section {
  738. .queue-title {
  739. display: flex;
  740. line-height: 30px;
  741. .material-icons {
  742. margin-left: 5px;
  743. margin-bottom: 5px;
  744. font-size: 28px;
  745. cursor: pointer;
  746. &:first-of-type {
  747. margin-left: auto;
  748. }
  749. &.skip-station {
  750. color: var(--dark-red);
  751. }
  752. &.resume-station,
  753. &.pause-station {
  754. color: var(--primary-color);
  755. }
  756. }
  757. }
  758. .currently-playing {
  759. margin-bottom: 10px;
  760. }
  761. }
  762. }
  763. &.modal-wide .left-section .section:first-child {
  764. padding: 0 15px 15px !important;
  765. }
  766. }
  767. </style>