index.vue 18 KB

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