Playlists.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. <template>
  2. <div class="station-playlists">
  3. <div class="tabs-container">
  4. <div class="tab-selection">
  5. <button
  6. class="button is-default"
  7. :class="{ selected: tab === 'current' }"
  8. @click="showTab('current')"
  9. >
  10. Current
  11. </button>
  12. <button
  13. class="button is-default"
  14. :class="{ selected: tab === 'search' }"
  15. @click="showTab('search')"
  16. >
  17. Search
  18. </button>
  19. <button
  20. v-if="station.type === 'community'"
  21. class="button is-default"
  22. :class="{ selected: tab === 'my-playlists' }"
  23. @click="showTab('my-playlists')"
  24. >
  25. My Playlists
  26. </button>
  27. </div>
  28. <div class="tab" v-show="tab === 'current'">
  29. <div v-if="currentPlaylists.length > 0">
  30. <playlist-item
  31. v-for="(playlist, index) in currentPlaylists"
  32. :key="'key-' + index"
  33. :playlist="playlist"
  34. :show-owner="true"
  35. >
  36. <div class="icons-group" slot="actions">
  37. <confirm
  38. v-if="isOwnerOrAdmin()"
  39. @confirm="deselectPlaylist(playlist._id)"
  40. >
  41. <i
  42. class="material-icons stop-icon"
  43. content="Stop playing songs from this playlist"
  44. v-tippy
  45. >
  46. stop
  47. </i>
  48. </confirm>
  49. <confirm
  50. v-if="isOwnerOrAdmin()"
  51. @confirm="blacklistPlaylist(playlist._id)"
  52. >
  53. <i
  54. class="material-icons stop-icon"
  55. content="Blacklist Playlist"
  56. v-tippy
  57. >block</i
  58. >
  59. </confirm>
  60. <i
  61. v-if="playlist.createdBy === myUserId"
  62. @click="showPlaylist(playlist._id)"
  63. class="material-icons edit-icon"
  64. content="Edit Playlist"
  65. v-tippy
  66. >edit</i
  67. >
  68. <i
  69. v-if="
  70. playlist.createdBy !== myUserId &&
  71. (playlist.privacy === 'public' ||
  72. isAdmin())
  73. "
  74. @click="showPlaylist(playlist._id)"
  75. class="material-icons edit-icon"
  76. content="View Playlist"
  77. v-tippy
  78. >visibility</i
  79. >
  80. </div>
  81. </playlist-item>
  82. </div>
  83. <p v-else class="nothing-here-text scrollable-list">
  84. No playlists currently selected.
  85. </p>
  86. </div>
  87. <div class="tab" v-show="tab === 'search'">
  88. <label class="label"> Search for a public playlist </label>
  89. <div class="control is-grouped input-with-button">
  90. <p class="control is-expanded">
  91. <input
  92. class="input"
  93. type="text"
  94. placeholder="Enter your playlist query here..."
  95. v-model="search.query"
  96. @keyup.enter="searchForPlaylists(1)"
  97. />
  98. </p>
  99. <p class="control">
  100. <a class="button is-info" @click="searchForPlaylists(1)"
  101. ><i class="material-icons icon-with-button"
  102. >search</i
  103. >Search</a
  104. >
  105. </p>
  106. </div>
  107. <div v-if="search.results.length > 0">
  108. <playlist-item
  109. v-for="(playlist, index) in search.results"
  110. :key="'searchKey-' + index"
  111. :playlist="playlist"
  112. :show-owner="true"
  113. >
  114. <div class="icons-group" slot="actions">
  115. <confirm
  116. v-if="
  117. (isOwnerOrAdmin() ||
  118. (station.type === 'community' &&
  119. station.partyMode)) &&
  120. isSelected(playlist._id)
  121. "
  122. @confirm="deselectPlaylist(playlist._id)"
  123. >
  124. <i
  125. class="material-icons stop-icon"
  126. content="Stop playing songs from this playlist"
  127. v-tippy
  128. >
  129. stop
  130. </i>
  131. </confirm>
  132. <i
  133. v-if="
  134. (isOwnerOrAdmin() ||
  135. (station.type === 'community' &&
  136. station.partyMode)) &&
  137. !isSelected(playlist._id)
  138. "
  139. @click="selectPlaylist(playlist)"
  140. class="material-icons play-icon"
  141. :content="
  142. station.partyMode
  143. ? 'Request songs from this playlist'
  144. : 'Play songs from this playlist'
  145. "
  146. v-tippy
  147. >play_arrow</i
  148. >
  149. <confirm
  150. v-if="isOwnerOrAdmin()"
  151. @confirm="blacklistPlaylist(playlist._id)"
  152. >
  153. <i
  154. class="material-icons stop-icon"
  155. content="Blacklist Playlist"
  156. v-tippy
  157. >block</i
  158. >
  159. </confirm>
  160. <i
  161. v-if="playlist.createdBy === myUserId"
  162. @click="showPlaylist(playlist._id)"
  163. class="material-icons edit-icon"
  164. content="Edit Playlist"
  165. v-tippy
  166. >edit</i
  167. >
  168. <i
  169. v-if="
  170. playlist.createdBy !== myUserId &&
  171. (playlist.privacy === 'public' ||
  172. isAdmin())
  173. "
  174. @click="showPlaylist(playlist._id)"
  175. class="material-icons edit-icon"
  176. content="View Playlist"
  177. v-tippy
  178. >visibility</i
  179. >
  180. </div>
  181. </playlist-item>
  182. <button
  183. v-if="resultsLeftCount > 0"
  184. class="button is-primary"
  185. @click="searchForPlaylists(search.page + 1)"
  186. >
  187. Load {{ nextPageResultsCount }} more results
  188. </button>
  189. </div>
  190. </div>
  191. <div
  192. v-if="station.type === 'community'"
  193. class="tab"
  194. v-show="tab === 'my-playlists'"
  195. >
  196. <button
  197. class="button is-primary"
  198. id="create-new-playlist-button"
  199. @click="openModal('createPlaylist')"
  200. >
  201. Create new playlist
  202. </button>
  203. <draggable
  204. class="menu-list scrollable-list"
  205. v-if="playlists.length > 0"
  206. v-model="playlists"
  207. v-bind="dragOptions"
  208. @start="drag = true"
  209. @end="drag = false"
  210. @change="savePlaylistOrder"
  211. >
  212. <transition-group
  213. type="transition"
  214. :name="!drag ? 'draggable-list-transition' : null"
  215. >
  216. <playlist-item
  217. class="item-draggable"
  218. v-for="playlist in playlists"
  219. :key="playlist._id"
  220. :playlist="playlist"
  221. >
  222. <div slot="actions">
  223. <i
  224. v-if="
  225. station.type === 'community' &&
  226. (isOwnerOrAdmin() ||
  227. station.partyMode) &&
  228. !isSelected(playlist._id)
  229. "
  230. @click="selectPlaylist(playlist)"
  231. class="material-icons play-icon"
  232. :content="
  233. station.partyMode
  234. ? 'Request songs from this playlist'
  235. : 'Play songs from this playlist'
  236. "
  237. v-tippy
  238. >play_arrow</i
  239. >
  240. <confirm
  241. v-if="
  242. station.type === 'community' &&
  243. (isOwnerOrAdmin() ||
  244. station.partyMode) &&
  245. isSelected(playlist._id)
  246. "
  247. @confirm="deselectPlaylist(playlist._id)"
  248. >
  249. <i
  250. class="material-icons stop-icon"
  251. :content="
  252. station.partyMode
  253. ? 'Stop requesting songs from this playlist'
  254. : 'Stop playing songs from this playlist'
  255. "
  256. v-tippy
  257. >stop</i
  258. >
  259. </confirm>
  260. <confirm
  261. v-if="isOwnerOrAdmin()"
  262. @confirm="blacklistPlaylist(playlist._id)"
  263. >
  264. <i
  265. class="material-icons stop-icon"
  266. content="Blacklist Playlist"
  267. v-tippy
  268. >block</i
  269. >
  270. </confirm>
  271. <i
  272. @click="showPlaylist(playlist._id)"
  273. class="material-icons edit-icon"
  274. content="Edit Playlist"
  275. v-tippy
  276. >edit</i
  277. >
  278. </div>
  279. </playlist-item>
  280. </transition-group>
  281. </draggable>
  282. <p v-else class="nothing-here-text scrollable-list">
  283. You don't have any playlists!
  284. </p>
  285. </div>
  286. </div>
  287. </div>
  288. </template>
  289. <script>
  290. import { mapActions, mapState, mapGetters } from "vuex";
  291. import Toast from "toasters";
  292. import draggable from "vuedraggable";
  293. import PlaylistItem from "@/components/PlaylistItem.vue";
  294. import Confirm from "@/components/Confirm.vue";
  295. import SortablePlaylists from "@/mixins/SortablePlaylists.vue";
  296. export default {
  297. components: {
  298. draggable,
  299. PlaylistItem,
  300. Confirm
  301. // CreatePlaylist: () => import("@/components/modals/CreatePlaylist.vue")
  302. },
  303. mixins: [SortablePlaylists],
  304. data() {
  305. return {
  306. tab: "current",
  307. search: {
  308. query: "",
  309. searchedQuery: "",
  310. page: 0,
  311. count: 0,
  312. resultsLeft: 0,
  313. results: []
  314. }
  315. };
  316. },
  317. computed: {
  318. playlists: {
  319. get() {
  320. return this.$store.state.user.playlists.playlists;
  321. },
  322. set(playlists) {
  323. this.$store.commit("user/playlists/setPlaylists", playlists);
  324. }
  325. },
  326. currentPlaylists() {
  327. if (this.station.type === "community" && this.station.partyMode) {
  328. return this.partyPlaylists;
  329. }
  330. return this.includedPlaylists;
  331. },
  332. resultsLeftCount() {
  333. return this.search.count - this.search.results.length;
  334. },
  335. nextPageResultsCount() {
  336. return Math.min(this.search.pageSize, this.resultsLeftCount);
  337. },
  338. ...mapState({
  339. loggedIn: state => state.user.auth.loggedIn,
  340. role: state => state.user.auth.role,
  341. myUserId: state => state.user.auth.userId,
  342. userId: state => state.user.auth.userId,
  343. partyPlaylists: state => state.station.partyPlaylists
  344. }),
  345. ...mapState("modals/manageStation", {
  346. station: state => state.station,
  347. originalStation: state => state.originalStation,
  348. includedPlaylists: state => state.includedPlaylists,
  349. excludedPlaylists: state => state.excludedPlaylists,
  350. songsList: state => state.songsList
  351. }),
  352. ...mapGetters({
  353. socket: "websockets/getSocket"
  354. })
  355. },
  356. mounted() {
  357. this.socket.dispatch("playlists.indexMyPlaylists", true, res => {
  358. if (res.status === "success") this.playlists = res.data.playlists;
  359. this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
  360. });
  361. this.socket.on("event:playlist.create", res => {
  362. this.playlists.push(res.data.playlist);
  363. });
  364. this.socket.on("event:playlist.delete", res => {
  365. this.playlists.forEach((playlist, index) => {
  366. if (playlist._id === res.data.playlistId) {
  367. this.playlists.splice(index, 1);
  368. }
  369. });
  370. });
  371. this.socket.on("event:playlist.addSong", res => {
  372. this.playlists.forEach((playlist, index) => {
  373. if (playlist._id === res.data.playlistId) {
  374. this.playlists[index].songs.push(res.data.song);
  375. }
  376. });
  377. });
  378. this.socket.on("event:playlist.removeSong", res => {
  379. this.playlists.forEach((playlist, index) => {
  380. if (playlist._id === res.data.playlistId) {
  381. this.playlists[index].songs.forEach((song, index2) => {
  382. if (song.youtubeId === res.data.youtubeId) {
  383. this.playlists[index].songs.splice(index2, 1);
  384. }
  385. });
  386. }
  387. });
  388. });
  389. this.socket.on("event:playlist.updateDisplayName", res => {
  390. this.playlists.forEach((playlist, index) => {
  391. if (playlist._id === res.data.playlistId) {
  392. this.playlists[index].displayName = res.data.displayName;
  393. }
  394. });
  395. });
  396. this.socket.on("event:playlist.updatePrivacy", res => {
  397. this.playlists.forEach((playlist, index) => {
  398. if (playlist._id === res.data.playlist._id) {
  399. this.playlists[index].privacy = res.data.playlist.privacy;
  400. }
  401. });
  402. });
  403. this.socket.on(
  404. "event:user.orderOfPlaylists.changed",
  405. orderOfPlaylists => {
  406. const sortedPlaylists = [];
  407. this.playlists.forEach(playlist => {
  408. sortedPlaylists[
  409. orderOfPlaylists.indexOf(playlist._id)
  410. ] = playlist;
  411. });
  412. this.playlists = sortedPlaylists;
  413. this.orderOfPlaylists = this.calculatePlaylistOrder();
  414. }
  415. );
  416. this.socket.dispatch(
  417. `stations.getStationIncludedPlaylistsById`,
  418. this.station._id,
  419. res => {
  420. if (res.status === "success") {
  421. this.station.includedPlaylists = res.data.playlists;
  422. this.originalStation.includedPlaylists = res.data.playlists;
  423. }
  424. }
  425. );
  426. this.socket.dispatch(
  427. `stations.getStationExcludedPlaylistsById`,
  428. this.station._id,
  429. res => {
  430. if (res.status === "success") {
  431. this.station.excludedPlaylists = res.data.playlists;
  432. this.originalStation.excludedPlaylists = res.data.playlists;
  433. }
  434. }
  435. );
  436. },
  437. methods: {
  438. showTab(tab) {
  439. this.tab = tab;
  440. },
  441. isOwner() {
  442. return this.loggedIn && this.userId === this.station.owner;
  443. },
  444. isAdmin() {
  445. return this.loggedIn && this.role === "admin";
  446. },
  447. isOwnerOrAdmin() {
  448. return this.isOwner() || this.isAdmin();
  449. },
  450. showPlaylist(playlistId) {
  451. this.editPlaylist(playlistId);
  452. this.openModal("editPlaylist");
  453. },
  454. selectPlaylist(playlist) {
  455. if (this.station.type === "community" && this.station.partyMode) {
  456. if (!this.isSelected(playlist.id)) {
  457. this.partyPlaylists.push(playlist);
  458. this.addPartyPlaylistSongToQueue();
  459. new Toast(
  460. "Successfully selected playlist to auto request songs."
  461. );
  462. } else {
  463. new Toast("Error: Playlist already selected.");
  464. }
  465. } else {
  466. this.socket.dispatch(
  467. "stations.includePlaylist",
  468. this.station._id,
  469. playlist._id,
  470. res => {
  471. new Toast(res.message);
  472. }
  473. );
  474. }
  475. },
  476. deselectPlaylist(id) {
  477. if (this.station.type === "community" && this.station.partyMode) {
  478. let selected = false;
  479. this.currentPlaylists.forEach((playlist, index) => {
  480. if (playlist._id === id) {
  481. selected = true;
  482. this.partyPlaylists.splice(index, 1);
  483. }
  484. });
  485. if (selected) {
  486. new Toast("Successfully deselected playlist.");
  487. } else {
  488. new Toast("Playlist not selected.");
  489. }
  490. } else {
  491. this.socket.dispatch(
  492. "stations.removeIncludedPlaylist",
  493. this.station._id,
  494. id,
  495. res => {
  496. new Toast(res.message);
  497. }
  498. );
  499. }
  500. },
  501. isSelected(id) {
  502. // TODO Also change this once it changes for a station
  503. let selected = false;
  504. this.currentPlaylists.forEach(playlist => {
  505. if (playlist._id === id) selected = true;
  506. });
  507. return selected;
  508. },
  509. searchForPlaylists(page) {
  510. if (
  511. this.search.page >= page ||
  512. this.search.searchedQuery !== this.search.query
  513. ) {
  514. this.search.results = [];
  515. this.search.page = 0;
  516. this.search.count = 0;
  517. this.search.resultsLeft = 0;
  518. this.search.pageSize = 0;
  519. }
  520. const { query } = this.search;
  521. const action =
  522. this.station.type === "official"
  523. ? "playlists.searchOfficial"
  524. : "playlists.searchCommunity";
  525. this.search.searchedQuery = this.search.query;
  526. this.socket.dispatch(action, query, page, res => {
  527. const { data } = res;
  528. const { count, pageSize, playlists } = data;
  529. if (res.status === "success") {
  530. this.search.results = [
  531. ...this.search.results,
  532. ...playlists
  533. ];
  534. this.search.page = page;
  535. this.search.count = count;
  536. this.search.resultsLeft =
  537. count - this.search.results.length;
  538. this.search.pageSize = pageSize;
  539. } else if (res.status === "error") {
  540. this.search.results = [];
  541. this.search.page = 0;
  542. this.search.count = 0;
  543. this.search.resultsLeft = 0;
  544. this.search.pageSize = 0;
  545. new Toast(res.message);
  546. }
  547. });
  548. },
  549. blacklistPlaylist(id) {
  550. if (this.isSelected(id)) {
  551. this.deselectPlaylist(id);
  552. }
  553. this.socket.dispatch(
  554. "stations.excludePlaylist",
  555. this.station._id,
  556. id,
  557. res => {
  558. new Toast(res.message);
  559. }
  560. );
  561. },
  562. addPartyPlaylistSongToQueue() {
  563. let isInQueue = false;
  564. if (
  565. this.station.type === "community" &&
  566. this.station.partyMode === true
  567. ) {
  568. this.songsList.forEach(queueSong => {
  569. if (queueSong.requestedBy === this.userId) isInQueue = true;
  570. });
  571. if (!isInQueue && this.partyPlaylists) {
  572. const selectedPlaylist = this.partyPlaylists[
  573. Math.floor(Math.random() * this.partyPlaylists.length)
  574. ];
  575. if (
  576. selectedPlaylist._id &&
  577. selectedPlaylist.songs.length > 0
  578. ) {
  579. const selectedSong =
  580. selectedPlaylist.songs[
  581. Math.floor(
  582. Math.random() *
  583. selectedPlaylist.songs.length
  584. )
  585. ];
  586. if (selectedSong.youtubeId) {
  587. this.socket.dispatch(
  588. "stations.addToQueue",
  589. this.station._id,
  590. selectedSong.youtubeId,
  591. data => {
  592. if (data.status !== "success")
  593. new Toast("Error auto queueing song");
  594. }
  595. );
  596. }
  597. }
  598. }
  599. }
  600. },
  601. ...mapActions("station", ["updatePartyPlaylists"]),
  602. ...mapActions("modalVisibility", ["openModal"]),
  603. ...mapActions("user/playlists", ["editPlaylist", "setPlaylists"])
  604. }
  605. };
  606. </script>
  607. <style lang="scss" scoped>
  608. .station-playlists {
  609. .tabs-container {
  610. .tab-selection {
  611. display: flex;
  612. .button {
  613. border-radius: 0;
  614. border: 0;
  615. text-transform: uppercase;
  616. font-size: 14px;
  617. color: var(--dark-grey-3);
  618. background-color: var(--light-grey-2);
  619. flex-grow: 1;
  620. height: 32px;
  621. &:not(:first-of-type) {
  622. margin-left: 5px;
  623. }
  624. }
  625. .selected {
  626. background-color: var(--dark-grey-3) !important;
  627. color: var(--white) !important;
  628. }
  629. }
  630. .tab {
  631. padding: 15px 0;
  632. border-radius: 0;
  633. .playlist-item:not(:last-of-type),
  634. .item.item-draggable:not(:last-of-type) {
  635. margin-bottom: 10px;
  636. }
  637. }
  638. }
  639. }
  640. .draggable-list-transition-move {
  641. transition: transform 0.5s;
  642. }
  643. .draggable-list-ghost {
  644. opacity: 0.5;
  645. filter: brightness(95%);
  646. }
  647. </style>