Playlists.vue 15 KB

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