AddSongToQueue.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. <template>
  2. <modal title="Add Song To Queue">
  3. <div slot="body">
  4. <div class="vertical-padding">
  5. <!-- Choosing a song from youtube -->
  6. <h4 class="section-title">Choose a song</h4>
  7. <p class="section-description">
  8. Choose a song by searching or using a link from YouTube.
  9. </p>
  10. <br />
  11. <div class="control is-grouped input-with-button">
  12. <p class="control is-expanded">
  13. <input
  14. class="input"
  15. type="text"
  16. placeholder="Enter your YouTube query here..."
  17. v-model="querySearch"
  18. autofocus
  19. @keyup.enter="submitQuery()"
  20. />
  21. </p>
  22. <p class="control">
  23. <a
  24. class="button is-info"
  25. @click.prevent="submitQuery()"
  26. href="#"
  27. ><i class="material-icons icon-with-button"
  28. >search</i
  29. >Search</a
  30. >
  31. </p>
  32. </div>
  33. <!-- Choosing a song from youtube - query results -->
  34. <div id="song-query-results" v-if="queryResults.length > 0">
  35. <search-query-item
  36. v-for="(result, index) in queryResults"
  37. :key="index"
  38. :result="result"
  39. >
  40. <div slot="actions">
  41. <transition
  42. name="search-query-actions"
  43. mode="out-in"
  44. >
  45. <a
  46. class="button is-success"
  47. v-if="result.isAddedToQueue"
  48. href="#"
  49. key="added-to-playlist"
  50. >
  51. <i class="material-icons icon-with-button"
  52. >done</i
  53. >
  54. Added to queue
  55. </a>
  56. <a
  57. class="button is-dark"
  58. v-else
  59. @click.prevent="
  60. addSongToQueue(result.id, index)
  61. "
  62. href="#"
  63. key="add-to-queue"
  64. >
  65. <i class="material-icons icon-with-button"
  66. >add</i
  67. >
  68. Add to queue
  69. </a>
  70. </transition>
  71. </div>
  72. </search-query-item>
  73. </div>
  74. <!-- Import a playlist from youtube -->
  75. <div v-if="station.type === 'official'">
  76. <hr class="section-horizontal-rule" />
  77. <h4 class="section-title">
  78. Import a playlist
  79. </h4>
  80. <p class="section-description">
  81. Import a playlist by using a link from YouTube.
  82. </p>
  83. <br />
  84. <div class="control is-grouped input-with-button">
  85. <p class="control is-expanded">
  86. <input
  87. class="input"
  88. type="text"
  89. placeholder="YouTube Playlist URL"
  90. v-model="importQuery"
  91. @keyup.enter="importPlaylist()"
  92. />
  93. </p>
  94. <p class="control has-addons">
  95. <span class="select" id="playlist-import-type">
  96. <select
  97. v-model="isImportingOnlyMusicOfPlaylist"
  98. >
  99. <option :value="false">Import all</option>
  100. <option :value="true"
  101. >Import only music</option
  102. >
  103. </select>
  104. </span>
  105. <a
  106. class="button is-info"
  107. @click.prevent="importPlaylist()"
  108. href="#"
  109. ><i class="material-icons icon-with-button"
  110. >publish</i
  111. >Import</a
  112. >
  113. </p>
  114. </div>
  115. </div>
  116. <!-- Choose a playlist from your account -->
  117. <div
  118. v-if="
  119. loggedIn &&
  120. station.type === 'community' &&
  121. playlists.length > 0
  122. "
  123. >
  124. <hr class="section-horizontal-rule" />
  125. <aside id="playlist-to-queue-selection">
  126. <h4 class="section-title">Choose a playlist</h4>
  127. <p class="section-description">
  128. Choose one of your playlists to add to the queue.
  129. </p>
  130. <br />
  131. <div id="playlists">
  132. <div
  133. class="playlist"
  134. v-for="(playlist, index) in playlists"
  135. :key="index"
  136. >
  137. <playlist-item :playlist="playlist">
  138. <div slot="actions">
  139. <a
  140. class="button is-danger"
  141. href="#"
  142. @click.prevent="
  143. togglePlaylistSelection(
  144. playlist._id
  145. )
  146. "
  147. v-if="
  148. isPlaylistSelected(playlist._id)
  149. "
  150. >
  151. <i
  152. class="material-icons icon-with-button"
  153. >stop</i
  154. >
  155. Stop playing
  156. </a>
  157. <a
  158. class="button is-success"
  159. @click.prevent="
  160. togglePlaylistSelection(
  161. playlist._id
  162. )
  163. "
  164. href="#"
  165. v-else
  166. ><i
  167. class="material-icons icon-with-button"
  168. >play_arrow</i
  169. >Play in queue
  170. </a>
  171. </div>
  172. </playlist-item>
  173. </div>
  174. </div>
  175. </aside>
  176. </div>
  177. </div>
  178. </div>
  179. </modal>
  180. </template>
  181. <script>
  182. import { mapState, mapActions } from "vuex";
  183. import Toast from "toasters";
  184. import PlaylistItem from "../../components/ui/PlaylistItem.vue";
  185. import SearchQueryItem from "../../components/ui/SearchQueryItem.vue";
  186. import Modal from "../../components/Modal.vue";
  187. import io from "../../io";
  188. export default {
  189. components: { Modal, PlaylistItem, SearchQueryItem },
  190. data() {
  191. return {
  192. querySearch: "",
  193. queryResults: [],
  194. playlists: [],
  195. importQuery: "",
  196. isImportingOnlyMusicOfPlaylist: false
  197. };
  198. },
  199. computed: mapState({
  200. loggedIn: state => state.user.auth.loggedIn,
  201. station: state => state.station.station,
  202. privatePlaylistQueueSelected: state =>
  203. state.station.privatePlaylistQueueSelected
  204. }),
  205. mounted() {
  206. io.getSocket(socket => {
  207. this.socket = socket;
  208. this.socket.emit("playlists.indexMyPlaylists", true, res => {
  209. if (res.status === "success") this.playlists = res.data;
  210. });
  211. });
  212. },
  213. methods: {
  214. isPlaylistSelected(playlistId) {
  215. return this.privatePlaylistQueueSelected === playlistId;
  216. },
  217. togglePlaylistSelection(playlistId) {
  218. if (this.station.type === "community") {
  219. if (this.isPlaylistSelected(playlistId)) {
  220. this.updatePrivatePlaylistQueueSelected(null);
  221. } else {
  222. this.updatePrivatePlaylistQueueSelected(playlistId);
  223. this.$parent.addFirstPrivatePlaylistSongToQueue();
  224. console.log(this.isPlaylistSelected(playlistId));
  225. }
  226. }
  227. },
  228. addSongToQueue(songId, index) {
  229. if (this.station.type === "community") {
  230. this.socket.emit(
  231. "stations.addToQueue",
  232. this.station._id,
  233. songId,
  234. data => {
  235. if (data.status !== "success")
  236. new Toast({
  237. content: `Error: ${data.message}`,
  238. timeout: 8000
  239. });
  240. else {
  241. this.queryResults[index].isAddedToQueue = true;
  242. new Toast({
  243. content: `${data.message}`,
  244. timeout: 4000
  245. });
  246. }
  247. }
  248. );
  249. } else {
  250. this.socket.emit("queueSongs.add", songId, data => {
  251. if (data.status !== "success")
  252. new Toast({
  253. content: `Error: ${data.message}`,
  254. timeout: 8000
  255. });
  256. else {
  257. this.queryResults[index].isAddedToQueue = true;
  258. new Toast({
  259. content: `${data.message}`,
  260. timeout: 4000
  261. });
  262. }
  263. });
  264. }
  265. },
  266. importPlaylist() {
  267. let isImportingPlaylist = true;
  268. // import query is blank
  269. if (!this.importQuery)
  270. return new Toast({
  271. content: "Please enter a YouTube playlist URL.",
  272. timeout: 4000
  273. });
  274. // don't give starting import message instantly in case of instant error
  275. setTimeout(() => {
  276. if (isImportingPlaylist) {
  277. new Toast({
  278. content:
  279. "Starting to import your playlist. This can take some time to do.",
  280. timeout: 4000
  281. });
  282. }
  283. }, 750);
  284. return this.socket.emit(
  285. "queueSongs.addSetToQueue",
  286. this.importQuery,
  287. this.isImportingOnlyMusicOfPlaylist,
  288. res => {
  289. isImportingPlaylist = false;
  290. return new Toast({ content: res.message, timeout: 20000 });
  291. }
  292. );
  293. },
  294. submitQuery() {
  295. let query = this.querySearch;
  296. if (!this.querySearch)
  297. return new Toast({
  298. content: "Please input a search query or a YouTube link.",
  299. timeout: 4000
  300. });
  301. if (query.indexOf("&index=") !== -1) {
  302. query = query.split("&index=");
  303. query.pop();
  304. query = query.join("");
  305. }
  306. if (query.indexOf("&list=") !== -1) {
  307. query = query.split("&list=");
  308. query.pop();
  309. query = query.join("");
  310. }
  311. return this.socket.emit("apis.searchYoutube", query, res => {
  312. if (res.status === "failure")
  313. return new Toast({
  314. content: "Error searching on YouTube",
  315. timeout: 4000
  316. });
  317. const { data } = res;
  318. this.queryResults = [];
  319. console.log(res.data);
  320. for (let i = 0; i < data.items.length; i += 1) {
  321. this.queryResults.push({
  322. id: data.items[i].id.videoId,
  323. url: `https://www.youtube.com/watch?v=${this.id}`,
  324. title: data.items[i].snippet.title,
  325. thumbnail: data.items[i].snippet.thumbnails.default.url,
  326. isAddedToQueue: false
  327. });
  328. }
  329. return this.queryResults;
  330. });
  331. },
  332. ...mapActions("station", ["updatePrivatePlaylistQueueSelected"]),
  333. ...mapActions("user/playlists", ["editPlaylist"])
  334. }
  335. };
  336. </script>
  337. <style lang="scss" scoped>
  338. @import "../../styles/global.scss";
  339. .night-mode {
  340. div {
  341. color: #4d4d4d;
  342. }
  343. }
  344. .song-actions {
  345. .button {
  346. height: 36px;
  347. width: 140px;
  348. }
  349. }
  350. .song-thumbnail div {
  351. width: 96px;
  352. height: 54px;
  353. background-position: center;
  354. background-repeat: no-repeat;
  355. }
  356. .table {
  357. margin-bottom: 0;
  358. margin-top: 20px;
  359. }
  360. #playlist-to-queue-selection {
  361. margin-top: 0;
  362. #playlists {
  363. font-size: 18px;
  364. .playlist {
  365. .button {
  366. width: 150px;
  367. }
  368. i {
  369. color: #fff;
  370. }
  371. }
  372. .playlist:not(:last-of-type) {
  373. margin-bottom: 10px;
  374. }
  375. .radio {
  376. display: flex;
  377. flex-direction: row;
  378. align-items: center;
  379. input {
  380. transform: scale(1.25);
  381. }
  382. }
  383. }
  384. }
  385. #playlist-import-type {
  386. &:hover {
  387. z-index: initial;
  388. }
  389. select {
  390. border-radius: 0;
  391. }
  392. }
  393. .vertical-padding {
  394. padding: 20px;
  395. }
  396. #song-query-results {
  397. padding: 10px;
  398. max-height: 500px;
  399. overflow: auto;
  400. border: 1px solid $light-grey-2;
  401. border-radius: 3px;
  402. .search-query-item:not(:last-of-type) {
  403. margin-bottom: 10px;
  404. }
  405. }
  406. </style>