index.vue 19 KB

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