AddSongToQueue.vue 9.9 KB

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