index.vue 15 KB

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