index.vue 19 KB

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