Request.vue 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  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="songs-tab"
  8. :class="{ selected: tab === 'songs' }"
  9. @click="showTab('songs')"
  10. >
  11. Songs
  12. </button>
  13. <button
  14. v-if="sector === 'station'"
  15. class="button is-default"
  16. ref="autorequest-tab"
  17. :class="{ selected: tab === 'autorequest' }"
  18. @click="showTab('autorequest')"
  19. >
  20. Autorequest
  21. </button>
  22. <button
  23. v-else
  24. class="button is-default disabled"
  25. content="Only available on station pages"
  26. v-tippy
  27. >
  28. Autorequest
  29. </button>
  30. </div>
  31. <div class="tab" v-show="tab === 'songs'">
  32. <div class="musare-songs">
  33. <label class="label"> Search for a song on Musare </label>
  34. <div class="control is-grouped input-with-button">
  35. <p class="control is-expanded">
  36. <input
  37. class="input"
  38. type="text"
  39. placeholder="Enter your song query here..."
  40. v-model="musareSearch.query"
  41. @keyup.enter="searchForMusareSongs(1)"
  42. />
  43. </p>
  44. <p class="control">
  45. <a
  46. class="button is-info"
  47. @click="searchForMusareSongs(1)"
  48. ><i class="material-icons icon-with-button"
  49. >search</i
  50. >Search</a
  51. >
  52. </p>
  53. </div>
  54. <div v-if="musareSearch.results.length > 0">
  55. <song-item
  56. v-for="song in musareSearch.results"
  57. :key="song._id"
  58. :song="song"
  59. >
  60. <template #actions>
  61. <transition
  62. name="musare-search-query-actions"
  63. mode="out-in"
  64. >
  65. <i
  66. v-if="
  67. songsInQueue.indexOf(
  68. song.youtubeId
  69. ) !== -1
  70. "
  71. class="material-icons added-to-playlist-icon"
  72. content="Song is already in queue"
  73. v-tippy
  74. >done</i
  75. >
  76. <i
  77. v-else
  78. class="material-icons add-to-queue-icon"
  79. @click="addSongToQueue(song.youtubeId)"
  80. content="Add Song to Queue"
  81. v-tippy
  82. >queue</i
  83. >
  84. </transition>
  85. </template>
  86. </song-item>
  87. <button
  88. v-if="playlistResultsLeftCount > 0"
  89. class="button is-primary load-more-button"
  90. @click="searchForMusareSongs(musareSearch.page + 1)"
  91. >
  92. Load {{ nextPagePlaylistsResultsCount }} more
  93. results
  94. </button>
  95. </div>
  96. </div>
  97. <div class="youtube-search">
  98. <label class="label"> Search for a song on YouTube </label>
  99. <div class="control is-grouped input-with-button">
  100. <p class="control is-expanded">
  101. <input
  102. class="input"
  103. type="text"
  104. placeholder="Enter your YouTube query here..."
  105. v-model="youtubeSearch.songs.query"
  106. autofocus
  107. @keyup.enter="searchForSongs()"
  108. />
  109. </p>
  110. <p class="control">
  111. <a
  112. class="button is-info"
  113. @click.prevent="searchForSongs()"
  114. ><i class="material-icons icon-with-button"
  115. >search</i
  116. >Search</a
  117. >
  118. </p>
  119. </div>
  120. <div
  121. v-if="youtubeSearch.songs.results.length > 0"
  122. id="song-query-results"
  123. >
  124. <search-query-item
  125. v-for="(result, index) in youtubeSearch.songs
  126. .results"
  127. :key="result.id"
  128. :result="result"
  129. >
  130. <template #actions>
  131. <transition
  132. name="youtube-search-query-actions"
  133. mode="out-in"
  134. >
  135. <i
  136. v-if="
  137. songsInQueue.indexOf(result.id) !==
  138. -1
  139. "
  140. class="material-icons added-to-playlist-icon"
  141. content="Song is already in queue"
  142. v-tippy
  143. >done</i
  144. >
  145. <i
  146. v-else
  147. class="material-icons add-to-queue-icon"
  148. @click="
  149. addSongToQueue(result.id, index)
  150. "
  151. content="Add Song to Queue"
  152. v-tippy
  153. >queue</i
  154. >
  155. </transition>
  156. </template>
  157. </search-query-item>
  158. <a
  159. class="button is-primary load-more-button"
  160. @click.prevent="loadMoreSongs()"
  161. >
  162. Load more...
  163. </a>
  164. </div>
  165. </div>
  166. </div>
  167. <div v-if="sector === 'station'" v-show="tab === 'autorequest'">
  168. <div class="tab-selection">
  169. <button
  170. class="button is-default"
  171. ref="search-child-tab"
  172. :class="{ selected: childTab === 'search' }"
  173. @click="showChildTab('search')"
  174. >
  175. Search
  176. </button>
  177. <button
  178. class="button is-default"
  179. ref="current-child-tab"
  180. :class="{ selected: childTab === 'current' }"
  181. @click="showChildTab('current')"
  182. >
  183. Current
  184. </button>
  185. <button
  186. class="button is-default"
  187. ref="my-playlists-child-tab"
  188. :class="{ selected: childTab === 'my-playlists' }"
  189. @click="showChildTab('my-playlists')"
  190. >
  191. My Playlists
  192. </button>
  193. </div>
  194. <div class="tab" v-show="childTab === 'search'">
  195. <div v-if="featuredPlaylists.length > 0">
  196. <label class="label"> Featured playlists </label>
  197. <playlist-item
  198. v-for="featuredPlaylist in featuredPlaylists"
  199. :key="`featuredKey-${featuredPlaylist._id}`"
  200. :playlist="featuredPlaylist"
  201. :show-owner="true"
  202. >
  203. <template #item-icon>
  204. <i
  205. class="material-icons"
  206. v-if="isSelected(featuredPlaylist._id)"
  207. content="This playlist is currently selected"
  208. v-tippy
  209. >
  210. radio
  211. </i>
  212. <i
  213. class="material-icons blacklisted-icon"
  214. v-else-if="
  215. isBlacklisted(featuredPlaylist._id)
  216. "
  217. content="This playlist is currently blacklisted"
  218. v-tippy
  219. >
  220. block
  221. </i>
  222. <i
  223. class="material-icons"
  224. v-else
  225. content="This playlist is currently not selected or blacklisted"
  226. v-tippy
  227. >
  228. play_disabled
  229. </i>
  230. </template>
  231. <template #actions>
  232. <i
  233. v-if="isBlacklisted(featuredPlaylist._id)"
  234. class="material-icons stop-icon"
  235. content="This playlist is blacklisted in this station"
  236. v-tippy="{ theme: 'info' }"
  237. >play_disabled</i
  238. >
  239. <quick-confirm
  240. v-if="isSelected(featuredPlaylist._id)"
  241. @confirm="deselect(featuredPlaylist._id)"
  242. >
  243. <i
  244. class="material-icons stop-icon"
  245. content="Stop playing songs from this playlist"
  246. v-tippy
  247. >
  248. stop
  249. </i>
  250. </quick-confirm>
  251. <i
  252. v-if="
  253. !isSelected(featuredPlaylist._id) &&
  254. !isBlacklisted(featuredPlaylist._id)
  255. "
  256. @click="select(featuredPlaylist)"
  257. class="material-icons play-icon"
  258. content="Request songs from this playlist"
  259. v-tippy
  260. >play_arrow</i
  261. >
  262. <quick-confirm
  263. v-if="
  264. isOwnerOrAdmin() &&
  265. !isBlacklisted(featuredPlaylist._id)
  266. "
  267. @confirm="
  268. blacklistPlaylist(featuredPlaylist._id)
  269. "
  270. >
  271. <i
  272. class="material-icons stop-icon"
  273. content="Blacklist Playlist"
  274. v-tippy
  275. >block</i
  276. >
  277. </quick-confirm>
  278. <quick-confirm
  279. v-if="
  280. isOwnerOrAdmin() &&
  281. isBlacklisted(featuredPlaylist._id)
  282. "
  283. @confirm="
  284. removeBlacklistedPlaylist(
  285. featuredPlaylist._id
  286. )
  287. "
  288. >
  289. <i
  290. class="material-icons stop-icon"
  291. content="Stop blacklisting songs from this playlist"
  292. v-tippy
  293. >
  294. stop
  295. </i>
  296. </quick-confirm>
  297. <i
  298. v-if="
  299. featuredPlaylist.createdBy === myUserId
  300. "
  301. @click="showPlaylist(featuredPlaylist._id)"
  302. class="material-icons edit-icon"
  303. content="Edit Playlist"
  304. v-tippy
  305. >edit</i
  306. >
  307. <i
  308. v-if="
  309. featuredPlaylist.createdBy !==
  310. myUserId &&
  311. (featuredPlaylist.privacy ===
  312. 'public' ||
  313. isAdmin())
  314. "
  315. @click="showPlaylist(featuredPlaylist._id)"
  316. class="material-icons edit-icon"
  317. content="View Playlist"
  318. v-tippy
  319. >visibility</i
  320. >
  321. </template>
  322. </playlist-item>
  323. <br />
  324. </div>
  325. <label class="label"> Search for a public playlist </label>
  326. <div class="control is-grouped input-with-button">
  327. <p class="control is-expanded">
  328. <input
  329. class="input"
  330. type="text"
  331. placeholder="Enter your playlist query here..."
  332. v-model="playlistSearch.query"
  333. @keyup.enter="searchForPlaylists(1)"
  334. />
  335. </p>
  336. <p class="control">
  337. <a
  338. class="button is-info"
  339. @click="searchForPlaylists(1)"
  340. ><i class="material-icons icon-with-button"
  341. >search</i
  342. >Search</a
  343. >
  344. </p>
  345. </div>
  346. <div v-if="playlistSearch.results.length > 0">
  347. <playlist-item
  348. v-for="playlist in playlistSearch.results"
  349. :key="`searchKey-${playlist._id}`"
  350. :playlist="playlist"
  351. :show-owner="true"
  352. >
  353. <template #item-icon>
  354. <i
  355. class="material-icons"
  356. v-if="isSelected(playlist._id)"
  357. content="This playlist is currently selected"
  358. v-tippy
  359. >
  360. radio
  361. </i>
  362. <i
  363. class="material-icons blacklisted-icon"
  364. v-else-if="isBlacklisted(playlist._id)"
  365. content="This playlist is currently blacklisted"
  366. v-tippy
  367. >
  368. block
  369. </i>
  370. <i
  371. class="material-icons"
  372. v-else
  373. content="This playlist is currently not selected or blacklisted"
  374. v-tippy
  375. >
  376. play_disabled
  377. </i>
  378. </template>
  379. <template #actions>
  380. <i
  381. v-if="isBlacklisted(playlist._id)"
  382. class="material-icons stop-icon"
  383. content="This playlist is blacklisted in this station"
  384. v-tippy="{ theme: 'info' }"
  385. >play_disabled</i
  386. >
  387. <quick-confirm
  388. v-if="isSelected(playlist._id)"
  389. @confirm="deselect(playlist._id)"
  390. >
  391. <i
  392. class="material-icons stop-icon"
  393. content="Stop playing songs from this playlist"
  394. v-tippy
  395. >
  396. stop
  397. </i>
  398. </quick-confirm>
  399. <i
  400. v-if="
  401. !isSelected(playlist._id) &&
  402. !isBlacklisted(playlist._id)
  403. "
  404. @click="select(playlist)"
  405. class="material-icons play-icon"
  406. content="Request songs from this playlist"
  407. v-tippy
  408. >play_arrow</i
  409. >
  410. <quick-confirm
  411. v-if="
  412. isOwnerOrAdmin() &&
  413. !isBlacklisted(playlist._id)
  414. "
  415. @confirm="blacklistPlaylist(playlist._id)"
  416. >
  417. <i
  418. class="material-icons stop-icon"
  419. content="Blacklist Playlist"
  420. v-tippy
  421. >block</i
  422. >
  423. </quick-confirm>
  424. <quick-confirm
  425. v-if="
  426. isOwnerOrAdmin() &&
  427. isBlacklisted(playlist._id)
  428. "
  429. @confirm="
  430. removeBlacklistedPlaylist(playlist._id)
  431. "
  432. >
  433. <i
  434. class="material-icons stop-icon"
  435. content="Stop blacklisting songs from this playlist"
  436. v-tippy
  437. >
  438. stop
  439. </i>
  440. </quick-confirm>
  441. <i
  442. v-if="playlist.createdBy === myUserId"
  443. @click="showPlaylist(playlist._id)"
  444. class="material-icons edit-icon"
  445. content="Edit Playlist"
  446. v-tippy
  447. >edit</i
  448. >
  449. <i
  450. v-if="
  451. playlist.createdBy !== myUserId &&
  452. (playlist.privacy === 'public' ||
  453. isAdmin())
  454. "
  455. @click="showPlaylist(playlist._id)"
  456. class="material-icons edit-icon"
  457. content="View Playlist"
  458. v-tippy
  459. >visibility</i
  460. >
  461. </template>
  462. </playlist-item>
  463. <button
  464. v-if="playlistResultsLeftCount > 0"
  465. class="button is-primary load-more-button"
  466. @click="searchForPlaylists(playlistSearch.page + 1)"
  467. >
  468. Load {{ nextPagePlaylistsResultsCount }} more
  469. results
  470. </button>
  471. </div>
  472. </div>
  473. <div class="tab" v-show="childTab === 'my-playlists'">
  474. <button
  475. class="button is-primary"
  476. id="create-new-playlist-button"
  477. @click="openModal('createPlaylist')"
  478. >
  479. Create new playlist
  480. </button>
  481. <div
  482. class="menu-list scrollable-list"
  483. v-if="playlists.length > 0"
  484. >
  485. <draggable
  486. tag="transition-group"
  487. :component-data="{
  488. name: !drag ? 'draggable-list-transition' : null
  489. }"
  490. item-key="_id"
  491. v-model="playlists"
  492. v-bind="dragOptions"
  493. @start="drag = true"
  494. @end="drag = false"
  495. @change="savePlaylistOrder"
  496. >
  497. <template #item="{ element }">
  498. <playlist-item
  499. class="item-draggable"
  500. :playlist="element"
  501. >
  502. <template #item-icon>
  503. <i
  504. class="material-icons"
  505. v-if="isSelected(element._id)"
  506. content="This playlist is currently selected"
  507. v-tippy
  508. >
  509. radio
  510. </i>
  511. <i
  512. class="material-icons blacklisted-icon"
  513. v-else-if="
  514. isBlacklisted(element._id)
  515. "
  516. content="This playlist is currently blacklisted"
  517. v-tippy
  518. >
  519. block
  520. </i>
  521. <i
  522. class="material-icons"
  523. v-else
  524. content="This playlist is currently not selected or blacklisted"
  525. v-tippy
  526. >
  527. play_disabled
  528. </i>
  529. </template>
  530. <template #actions>
  531. <i
  532. v-if="!isSelected(element._id)"
  533. @click="select(element)"
  534. class="material-icons play-icon"
  535. content="Request songs from this playlist"
  536. v-tippy
  537. >play_arrow</i
  538. >
  539. <quick-confirm
  540. v-if="isSelected(element._id)"
  541. @confirm="deselect(element._id)"
  542. >
  543. <i
  544. class="material-icons stop-icon"
  545. content="Stop requesting songs from this playlist"
  546. v-tippy
  547. >stop</i
  548. >
  549. </quick-confirm>
  550. <quick-confirm
  551. v-if="
  552. isOwnerOrAdmin() &&
  553. !isBlacklisted(element._id)
  554. "
  555. @confirm="
  556. blacklistPlaylist(element._id)
  557. "
  558. >
  559. <i
  560. class="material-icons stop-icon"
  561. content="Blacklist Playlist"
  562. v-tippy
  563. >block</i
  564. >
  565. </quick-confirm>
  566. <quick-confirm
  567. v-if="
  568. isOwnerOrAdmin() &&
  569. isBlacklisted(element._id)
  570. "
  571. @confirm="
  572. removeBlacklistedPlaylist(
  573. element._id
  574. )
  575. "
  576. >
  577. <i
  578. class="material-icons stop-icon"
  579. content="Stop blacklisting songs from this playlist"
  580. v-tippy
  581. >
  582. stop
  583. </i>
  584. </quick-confirm>
  585. <i
  586. @click="showPlaylist(element._id)"
  587. class="material-icons edit-icon"
  588. content="Edit Playlist"
  589. v-tippy
  590. >edit</i
  591. >
  592. </template>
  593. </playlist-item>
  594. </template>
  595. </draggable>
  596. </div>
  597. <p v-else class="has-text-centered scrollable-list">
  598. You don't have any playlists!
  599. </p>
  600. </div>
  601. <div class="tab" v-show="childTab === 'current'">
  602. <div v-if="autoRequest.length > 0">
  603. <playlist-item
  604. v-for="playlist in autoRequest"
  605. :key="`key-${playlist._id}`"
  606. :playlist="playlist"
  607. :show-owner="true"
  608. >
  609. <template #item-icon>
  610. <i
  611. class="material-icons"
  612. content="This playlist is currently selected"
  613. v-tippy
  614. >
  615. radio
  616. </i>
  617. </template>
  618. <template #actions>
  619. <quick-confirm
  620. v-if="isOwnerOrAdmin()"
  621. @confirm="deselect(playlist._id)"
  622. >
  623. <i
  624. class="material-icons stop-icon"
  625. content="Stop playing songs from this playlist"
  626. v-tippy
  627. >
  628. stop
  629. </i>
  630. </quick-confirm>
  631. <quick-confirm
  632. v-if="isOwnerOrAdmin()"
  633. @confirm="blacklistPlaylist(playlist._id)"
  634. >
  635. <i
  636. class="material-icons stop-icon"
  637. content="Blacklist Playlist"
  638. v-tippy
  639. >block</i
  640. >
  641. </quick-confirm>
  642. <i
  643. v-if="playlist.createdBy === myUserId"
  644. @click="showPlaylist(playlist._id)"
  645. class="material-icons edit-icon"
  646. content="Edit Playlist"
  647. v-tippy
  648. >edit</i
  649. >
  650. <i
  651. v-if="
  652. playlist.createdBy !== myUserId &&
  653. (playlist.privacy === 'public' ||
  654. isAdmin())
  655. "
  656. @click="showPlaylist(playlist._id)"
  657. class="material-icons edit-icon"
  658. content="View Playlist"
  659. v-tippy
  660. >visibility</i
  661. >
  662. </template>
  663. </playlist-item>
  664. </div>
  665. <p v-else class="has-text-centered scrollable-list">
  666. No playlists currently being played.
  667. </p>
  668. </div>
  669. </div>
  670. </div>
  671. </div>
  672. </template>
  673. <script>
  674. import { mapActions, mapState, mapGetters } from "vuex";
  675. import Toast from "toasters";
  676. import ws from "@/ws";
  677. import QuickConfirm from "@/components/QuickConfirm.vue";
  678. import SongItem from "@/components/SongItem.vue";
  679. import PlaylistItem from "@/components/PlaylistItem.vue";
  680. import SearchQueryItem from "@/components/SearchQueryItem.vue";
  681. import SortablePlaylists from "@/mixins/SortablePlaylists.vue";
  682. import SearchYoutube from "@/mixins/SearchYoutube.vue";
  683. import SearchMusare from "@/mixins/SearchMusare.vue";
  684. export default {
  685. components: {
  686. QuickConfirm,
  687. SongItem,
  688. PlaylistItem,
  689. SearchQueryItem
  690. },
  691. mixins: [SortablePlaylists, SearchYoutube, SearchMusare],
  692. props: {
  693. sector: { type: String, default: "station" }
  694. },
  695. data() {
  696. return {
  697. tab: "songs",
  698. childTab: "search",
  699. playlistSearch: {
  700. query: "",
  701. searchedQuery: "",
  702. page: 0,
  703. count: 0,
  704. resultsLeft: 0,
  705. results: []
  706. },
  707. featuredPlaylists: []
  708. };
  709. },
  710. computed: {
  711. playlistResultsLeftCount() {
  712. return (
  713. this.playlistSearch.count - this.playlistSearch.results.length
  714. );
  715. },
  716. nextPagePlaylistSearchResultsCount() {
  717. return Math.min(
  718. this.playlistSearch.pageSize,
  719. this.playlistResultsLeftCount
  720. );
  721. },
  722. songsInQueue() {
  723. if (this.station.currentSong)
  724. return this.songsList
  725. .map(song => song.youtubeId)
  726. .concat(this.station.currentSong.youtubeId);
  727. return this.songsList.map(song => song.youtubeId);
  728. },
  729. currentUserQueueSongs() {
  730. return this.songsList.filter(
  731. queueSong => queueSong.requestedBy === this.userId
  732. ).length;
  733. },
  734. ...mapState("user", {
  735. loggedIn: state => state.auth.loggedIn,
  736. role: state => state.auth.role,
  737. userId: state => state.auth.userId
  738. }),
  739. ...mapState("station", {
  740. station: state => state.station,
  741. songsList: state => state.songsList,
  742. autoRequest: state => state.autoRequest,
  743. autoRequestLock: state => state.autoRequestLock,
  744. blacklist: state => state.blacklist
  745. }),
  746. ...mapGetters({
  747. socket: "websockets/getSocket"
  748. })
  749. },
  750. mounted() {
  751. this.showTab("songs");
  752. ws.onConnect(this.init);
  753. this.socket.on("event:station.queue.updated", () =>
  754. this.autoRequestSong()
  755. );
  756. },
  757. methods: {
  758. init() {
  759. this.socket.dispatch("playlists.indexMyPlaylists", res => {
  760. if (res.status === "success")
  761. this.setPlaylists(res.data.playlists);
  762. this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
  763. });
  764. this.socket.dispatch("playlists.indexFeaturedPlaylists", res => {
  765. if (res.status === "success")
  766. this.featuredPlaylists = res.data.playlists;
  767. });
  768. this.socket.dispatch(
  769. `stations.getStationBlacklistById`,
  770. this.station._id,
  771. res => {
  772. if (res.status === "success") {
  773. this.station.blacklist = res.data.playlists;
  774. }
  775. }
  776. );
  777. },
  778. showTab(tab) {
  779. this.$refs[`${tab}-tab`].scrollIntoView({ block: "nearest" });
  780. this.tab = tab;
  781. },
  782. showChildTab(tab) {
  783. this.$refs[`${tab}-child-tab`].scrollIntoView({ block: "nearest" });
  784. this.childTab = tab;
  785. },
  786. isOwner() {
  787. return (
  788. this.loggedIn &&
  789. this.station &&
  790. this.userId === this.station.owner
  791. );
  792. },
  793. isAdmin() {
  794. return this.loggedIn && this.role === "admin";
  795. },
  796. isOwnerOrAdmin() {
  797. return this.isOwner() || this.isAdmin();
  798. },
  799. showPlaylist(playlistId) {
  800. this.editPlaylist(playlistId);
  801. this.openModal("editPlaylist");
  802. },
  803. select(playlist) {
  804. if (!this.isSelected(playlist.id)) {
  805. this.autoRequest.push(playlist);
  806. this.autoRequestSong();
  807. new Toast(
  808. "Successfully selected playlist to auto request songs."
  809. );
  810. } else {
  811. new Toast("Error: Playlist already selected.");
  812. }
  813. },
  814. deselect(id) {
  815. return new Promise(resolve => {
  816. let selected = false;
  817. this.autoRequest.forEach((playlist, index) => {
  818. if (playlist._id === id) {
  819. selected = true;
  820. this.autoRequest.splice(index, 1);
  821. }
  822. });
  823. if (selected) {
  824. new Toast("Successfully deselected playlist.");
  825. resolve();
  826. } else {
  827. new Toast("Playlist not selected.");
  828. resolve();
  829. }
  830. });
  831. },
  832. removeBlacklistedPlaylist(id) {
  833. return new Promise(resolve => {
  834. this.socket.dispatch(
  835. "stations.removeBlacklistedPlaylist",
  836. this.station._id,
  837. id,
  838. res => {
  839. new Toast(res.message);
  840. resolve();
  841. }
  842. );
  843. });
  844. },
  845. isSelected(id) {
  846. let selected = false;
  847. this.autoRequest.forEach(playlist => {
  848. if (playlist._id === id) selected = true;
  849. });
  850. return selected;
  851. },
  852. isBlacklisted(id) {
  853. let selected = false;
  854. this.blacklist.forEach(playlist => {
  855. if (playlist._id === id) selected = true;
  856. });
  857. return selected;
  858. },
  859. searchForPlaylists(page) {
  860. if (
  861. this.playlistSearch.page >= page ||
  862. this.playlistSearch.searchedQuery !== this.playlistSearch.query
  863. ) {
  864. this.playlistSearch.results = [];
  865. this.playlistSearch.page = 0;
  866. this.playlistSearch.count = 0;
  867. this.playlistSearch.resultsLeft = 0;
  868. this.playlistSearch.pageSize = 0;
  869. }
  870. const { query } = this.playlistSearch;
  871. const action =
  872. this.station.type === "official"
  873. ? "playlists.searchOfficial"
  874. : "playlists.searchCommunity";
  875. this.playlistSearch.searchedQuery = this.playlistSearch.query;
  876. this.socket.dispatch(action, query, page, res => {
  877. const { data } = res;
  878. if (res.status === "success") {
  879. const { count, pageSize, playlists } = data;
  880. this.playlistSearch.results = [
  881. ...this.playlistSearch.results,
  882. ...playlists
  883. ];
  884. this.playlistSearch.page = page;
  885. this.playlistSearch.count = count;
  886. this.playlistSearch.resultsLeft =
  887. count - this.playlistSearch.results.length;
  888. this.playlistSearch.pageSize = pageSize;
  889. } else if (res.status === "error") {
  890. this.playlistSearch.results = [];
  891. this.playlistSearch.page = 0;
  892. this.playlistSearch.count = 0;
  893. this.playlistSearch.resultsLeft = 0;
  894. this.playlistSearch.pageSize = 0;
  895. new Toast(res.message);
  896. }
  897. });
  898. },
  899. async blacklistPlaylist(id) {
  900. // if (this.isIncluded(id)) await this.removeIncludedPlaylist(id);
  901. this.socket.dispatch(
  902. "stations.blacklistPlaylist",
  903. this.station._id,
  904. id,
  905. res => {
  906. new Toast(res.message);
  907. }
  908. );
  909. },
  910. autoRequestSong() {
  911. if (
  912. !this.autoRequestLock &&
  913. this.songsList.length < 50 &&
  914. this.currentUserQueueSongs <
  915. this.station.requests.limit * 0.5 &&
  916. this.autoRequest.length > 0
  917. ) {
  918. const selectedPlaylist =
  919. this.autoRequest[
  920. Math.floor(Math.random() * this.autoRequest.length)
  921. ];
  922. if (selectedPlaylist._id && selectedPlaylist.songs.length > 0) {
  923. const selectedSong =
  924. selectedPlaylist.songs[
  925. Math.floor(
  926. Math.random() * selectedPlaylist.songs.length
  927. )
  928. ];
  929. if (selectedSong.youtubeId) {
  930. this.updateAutoRequestLock(true);
  931. this.socket.dispatch(
  932. "stations.addToQueue",
  933. this.station._id,
  934. selectedSong.youtubeId,
  935. data => {
  936. this.updateAutoRequestLock(false);
  937. if (data.status !== "success")
  938. this.autoRequestSong();
  939. }
  940. );
  941. }
  942. }
  943. }
  944. },
  945. addSongToQueue(youtubeId, index) {
  946. this.socket.dispatch(
  947. "stations.addToQueue",
  948. this.station._id,
  949. youtubeId,
  950. res => {
  951. if (res.status !== "success")
  952. new Toast(`Error: ${res.message}`);
  953. else {
  954. if (index)
  955. this.youtubeSearch.songs.results[
  956. index
  957. ].isAddedToQueue = true;
  958. new Toast(res.message);
  959. }
  960. }
  961. );
  962. },
  963. ...mapActions("station", ["updateAutoRequest"]),
  964. ...mapActions("station", [
  965. "updateAutoRequest",
  966. "updateAutoRequestLock"
  967. ]),
  968. ...mapActions("modalVisibility", ["openModal"]),
  969. ...mapActions("user/playlists", ["editPlaylist", "setPlaylists"])
  970. }
  971. };
  972. </script>
  973. <style lang="less" scoped>
  974. .night-mode {
  975. .tabs-container .tab-selection .button {
  976. background: var(--dark-grey) !important;
  977. color: var(--white) !important;
  978. }
  979. }
  980. .blacklisted-icon {
  981. color: var(--dark-red);
  982. }
  983. .included-icon {
  984. color: var(--green);
  985. }
  986. .selected-icon {
  987. color: var(--purple);
  988. }
  989. #create-new-playlist-button {
  990. width: 100%;
  991. }
  992. .station-playlists {
  993. .tabs-container {
  994. .tab-selection {
  995. display: flex;
  996. overflow-x: auto;
  997. .button {
  998. border-radius: 0;
  999. border: 0;
  1000. text-transform: uppercase;
  1001. font-size: 14px;
  1002. color: var(--dark-grey-3);
  1003. background-color: var(--light-grey-2);
  1004. flex-grow: 1;
  1005. height: 32px;
  1006. &:not(:first-of-type) {
  1007. margin-left: 5px;
  1008. }
  1009. }
  1010. .selected {
  1011. background-color: var(--primary-color) !important;
  1012. color: var(--white) !important;
  1013. font-weight: 600;
  1014. }
  1015. }
  1016. & > div > .tab-selection .button {
  1017. margin-top: 5px;
  1018. font-size: 12px;
  1019. height: 28px;
  1020. }
  1021. .tab {
  1022. padding: 15px 0;
  1023. border-radius: 0;
  1024. .playlist-item:not(:last-of-type),
  1025. .item.item-draggable:not(:last-of-type) {
  1026. margin-bottom: 10px;
  1027. }
  1028. .load-more-button {
  1029. width: 100%;
  1030. margin-top: 10px;
  1031. }
  1032. }
  1033. }
  1034. }
  1035. .draggable-list-transition-move {
  1036. transition: transform 0.5s;
  1037. }
  1038. .draggable-list-ghost {
  1039. opacity: 0.5;
  1040. filter: brightness(95%);
  1041. }
  1042. </style>