index.vue 17 KB

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