Playlists.vue 19 KB

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