index.vue 14 KB

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