index.vue 16 KB

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