index.vue 16 KB

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