ImportAlbum.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  1. <script setup lang="ts">
  2. // TODO - Fix sortable
  3. import {
  4. defineAsyncComponent,
  5. ref,
  6. computed,
  7. onMounted,
  8. onBeforeUnmount
  9. } from "vue";
  10. import Toast from "toasters";
  11. import { Sortable } from "sortablejs-vue3";
  12. import { storeToRefs } from "pinia";
  13. import { useWebsocketsStore } from "@/stores/websockets";
  14. import { useModalsStore } from "@/stores/modals";
  15. import { useImportAlbumStore } from "@/stores/importAlbum";
  16. import ws from "@/ws";
  17. const SongItem = defineAsyncComponent(
  18. () => import("@/components/SongItem.vue")
  19. );
  20. const props = defineProps({
  21. modalUuid: { type: String, default: "" }
  22. });
  23. const { socket } = useWebsocketsStore();
  24. const importAlbumStore = useImportAlbumStore(props);
  25. const { discogsTab, discogsAlbum, prefillDiscogs, playlistSongs } =
  26. storeToRefs(importAlbumStore);
  27. const {
  28. toggleDiscogsAlbum,
  29. setPlaylistSongs,
  30. updatePlaylistSongs,
  31. selectDiscogsAlbum,
  32. resetPlaylistSongs,
  33. updatePlaylistSong
  34. } = importAlbumStore;
  35. const { openModal } = useModalsStore();
  36. const isImportingPlaylist = ref(false);
  37. const trackSongs = ref([]);
  38. const songsToEdit = ref([]);
  39. const search = ref({
  40. playlist: {
  41. query: ""
  42. }
  43. });
  44. const discogsQuery = ref("");
  45. const discogs = ref({
  46. apiResults: [],
  47. page: 1,
  48. pages: 1,
  49. disableLoadMore: false
  50. });
  51. const discogsTabs = ref([]);
  52. const sortableUpdateNumber = ref(0);
  53. // TODO might not not be needed anymore, might be able to directly edit prefillDiscogs
  54. const localPrefillDiscogs = computed({
  55. get: () => importAlbumStore.prefillDiscogs,
  56. set: value => {
  57. importAlbumStore.updatePrefillDiscogs(value);
  58. }
  59. });
  60. const showDiscogsTab = tab => {
  61. if (discogsTabs.value[`discogs-${tab}-tab`])
  62. discogsTabs.value[`discogs-${tab}-tab`].scrollIntoView({
  63. block: "nearest"
  64. });
  65. return importAlbumStore.showDiscogsTab(tab);
  66. };
  67. const init = () => {
  68. socket.dispatch("apis.joinRoom", "import-album");
  69. };
  70. const startEditingSongs = () => {
  71. songsToEdit.value = [];
  72. trackSongs.value.forEach((songs, index) => {
  73. songs.forEach(song => {
  74. const album = JSON.parse(JSON.stringify(discogsAlbum.value));
  75. album.track = album.tracks[index];
  76. delete album.tracks;
  77. delete album.expanded;
  78. delete album.gotMoreInfo;
  79. const songToEdit = <
  80. {
  81. youtubeId: string;
  82. prefill: {
  83. discogs: typeof album;
  84. title?: string;
  85. thumbnail?: string;
  86. genres?: string[];
  87. artists?: string[];
  88. };
  89. }
  90. >{
  91. youtubeId: song.youtubeId,
  92. prefill: {
  93. discogs: album
  94. }
  95. };
  96. if (prefillDiscogs.value) {
  97. songToEdit.prefill.title = album.track.title;
  98. songToEdit.prefill.thumbnail =
  99. discogsAlbum.value.album.albumArt;
  100. songToEdit.prefill.genres = JSON.parse(
  101. JSON.stringify(album.album.genres)
  102. );
  103. songToEdit.prefill.artists = JSON.parse(
  104. JSON.stringify(album.album.artists)
  105. );
  106. }
  107. songsToEdit.value.push(songToEdit);
  108. });
  109. });
  110. if (songsToEdit.value.length === 0) new Toast("You can't edit 0 songs.");
  111. else {
  112. openModal({
  113. modal: "editSongs",
  114. data: { songs: songsToEdit.value }
  115. });
  116. }
  117. };
  118. const tryToAutoMove = () => {
  119. const { tracks } = discogsAlbum.value;
  120. const songs = JSON.parse(JSON.stringify(playlistSongs.value));
  121. tracks.forEach((track, index) => {
  122. songs.forEach(playlistSong => {
  123. if (
  124. playlistSong.title
  125. .toLowerCase()
  126. .trim()
  127. .indexOf(track.title.toLowerCase().trim()) !== -1
  128. ) {
  129. songs.splice(songs.indexOf(playlistSong), 1);
  130. trackSongs.value[index].push(playlistSong);
  131. }
  132. });
  133. });
  134. updatePlaylistSongs(songs);
  135. };
  136. const importPlaylist = () => {
  137. if (isImportingPlaylist.value)
  138. return new Toast("A playlist is already importing.");
  139. isImportingPlaylist.value = true;
  140. // import query is blank
  141. if (!search.value.playlist.query)
  142. return new Toast("Please enter a YouTube playlist URL.");
  143. const regex = /[\\?&]list=([^&#]*)/;
  144. const splitQuery = regex.exec(search.value.playlist.query);
  145. if (!splitQuery) {
  146. return new Toast({
  147. content: "Please enter a valid YouTube playlist URL.",
  148. timeout: 4000
  149. });
  150. }
  151. // don't give starting import message instantly in case of instant error
  152. setTimeout(() => {
  153. if (isImportingPlaylist.value) {
  154. new Toast(
  155. "Starting to import your playlist. This can take some time to do."
  156. );
  157. }
  158. }, 750);
  159. return socket.dispatch(
  160. "youtube.requestSet",
  161. search.value.playlist.query,
  162. false,
  163. true,
  164. res => {
  165. isImportingPlaylist.value = false;
  166. const youtubeIds = res.videos.map(video => video.youtubeId);
  167. socket.dispatch("songs.getSongsFromYoutubeIds", youtubeIds, res => {
  168. if (res.status === "success") {
  169. const songs = res.data.songs.filter(song => !song.verified);
  170. const songsAlreadyVerified =
  171. res.data.songs.length - songs.length;
  172. setPlaylistSongs(songs);
  173. if (discogsAlbum.value.tracks) {
  174. trackSongs.value = discogsAlbum.value.tracks.map(
  175. () => []
  176. );
  177. tryToAutoMove();
  178. }
  179. if (songsAlreadyVerified > 0)
  180. new Toast(
  181. `${songsAlreadyVerified} songs were already verified, skipping those.`
  182. );
  183. }
  184. new Toast("Could not get songs.");
  185. });
  186. return new Toast({ content: res.message, timeout: 20000 });
  187. }
  188. );
  189. };
  190. const resetTrackSongs = () => {
  191. resetPlaylistSongs();
  192. trackSongs.value = discogsAlbum.value.tracks.map(() => []);
  193. };
  194. const selectAlbum = result => {
  195. selectDiscogsAlbum(result);
  196. trackSongs.value = discogsAlbum.value.tracks.map(() => []);
  197. if (playlistSongs.value.length > 0) tryToAutoMove();
  198. // clearDiscogsResults();
  199. showDiscogsTab("selected");
  200. };
  201. const toggleAPIResult = index => {
  202. const apiResult = discogs.value.apiResults[index];
  203. if (apiResult.expanded === true) apiResult.expanded = false;
  204. else if (apiResult.gotMoreInfo === true) apiResult.expanded = true;
  205. else {
  206. fetch(apiResult.album.resourceUrl)
  207. .then(response => response.json())
  208. .then(data => {
  209. apiResult.album.artists = [];
  210. apiResult.album.artistIds = [];
  211. const artistRegex = /\\([0-9]+\\)$/;
  212. apiResult.dataQuality = data.data_quality;
  213. data.artists.forEach(artist => {
  214. apiResult.album.artists.push(
  215. artist.name.replace(artistRegex, "")
  216. );
  217. apiResult.album.artistIds.push(artist.id);
  218. });
  219. apiResult.tracks = data.tracklist.map(track => ({
  220. position: track.position,
  221. title: track.title
  222. }));
  223. apiResult.expanded = true;
  224. apiResult.gotMoreInfo = true;
  225. });
  226. }
  227. };
  228. const clearDiscogsResults = () => {
  229. discogs.value.apiResults = [];
  230. discogs.value.page = 1;
  231. discogs.value.pages = 1;
  232. discogs.value.disableLoadMore = false;
  233. };
  234. const searchDiscogsForPage = page => {
  235. const query = discogsQuery.value;
  236. socket.dispatch("apis.searchDiscogs", query, page, res => {
  237. if (res.status === "success") {
  238. if (page === 1)
  239. new Toast(
  240. `Successfully searched. Got ${res.data.results.length} results.`
  241. );
  242. else
  243. new Toast(
  244. `Successfully got ${res.data.results.length} more results.`
  245. );
  246. if (page === 1) {
  247. discogs.value.apiResults = [];
  248. }
  249. discogs.value.pages = res.data.pages;
  250. discogs.value.apiResults = discogs.value.apiResults.concat(
  251. res.data.results.map(result => {
  252. const type =
  253. result.type.charAt(0).toUpperCase() +
  254. result.type.slice(1);
  255. return {
  256. expanded: false,
  257. gotMoreInfo: false,
  258. album: {
  259. id: result.id,
  260. title: result.title,
  261. type,
  262. year: result.year,
  263. genres: result.genre,
  264. albumArt: result.cover_image,
  265. resourceUrl: result.resource_url
  266. }
  267. };
  268. })
  269. );
  270. discogs.value.page = page;
  271. discogs.value.disableLoadMore = false;
  272. } else new Toast(res.message);
  273. });
  274. };
  275. const loadNextDiscogsPage = () => {
  276. discogs.value.disableLoadMore = true;
  277. searchDiscogsForPage(discogs.value.page + 1);
  278. };
  279. const onDiscogsQueryChange = () => {
  280. discogs.value.page = 1;
  281. discogs.value.pages = 1;
  282. discogs.value.apiResults = [];
  283. discogs.value.disableLoadMore = false;
  284. };
  285. const updateTrackSong = updatedSong => {
  286. updatePlaylistSong(updatedSong);
  287. trackSongs.value.forEach((songs, indexA) => {
  288. songs.forEach((song, indexB) => {
  289. if (song._id === updatedSong._id)
  290. trackSongs.value[indexA][indexB] = updatedSong;
  291. });
  292. });
  293. };
  294. const updatePlaylistSongPosition = ({ oldIndex, newIndex }) => {
  295. if (oldIndex === newIndex) return;
  296. const oldSongs = playlistSongs.value;
  297. oldSongs.splice(newIndex, 0, oldSongs.splice(oldIndex, 1)[0]);
  298. playlistSongs.value = oldSongs;
  299. };
  300. const updateTrackSongPosition = (trackIndex, { oldIndex, newIndex }) => {
  301. if (oldIndex === newIndex) return;
  302. const oldSongs = trackSongs.value[trackIndex];
  303. oldSongs.splice(newIndex, 0, oldSongs.splice(oldIndex, 1)[0]);
  304. trackSongs.value[trackIndex] = oldSongs;
  305. };
  306. const playlistSongAdded = event => {
  307. const fromTrack = event.from;
  308. const fromTrackIndex = Number(fromTrack.dataset.trackIndex);
  309. const song = trackSongs.value[fromTrackIndex][event.oldIndex];
  310. const newPlaylistSongs = JSON.parse(JSON.stringify(playlistSongs.value));
  311. newPlaylistSongs.splice(event.newIndex, 0, song);
  312. playlistSongs.value = newPlaylistSongs;
  313. sortableUpdateNumber.value += 1;
  314. };
  315. const playlistSongRemoved = event => {
  316. playlistSongs.value.splice(event.oldIndex, 1);
  317. sortableUpdateNumber.value += 1;
  318. };
  319. const trackSongAdded = (trackIndex, event) => {
  320. const fromElement = event.from;
  321. let song = null;
  322. if (fromElement.dataset.trackIndex) {
  323. const fromTrackIndex = Number(fromElement.dataset.trackIndex);
  324. song = trackSongs.value[fromTrackIndex][event.oldIndex];
  325. } else {
  326. song = playlistSongs.value[event.oldIndex];
  327. }
  328. const newTrackSongs = JSON.parse(
  329. JSON.stringify(trackSongs.value[trackIndex])
  330. );
  331. newTrackSongs.splice(event.newIndex, 0, song);
  332. trackSongs.value[trackIndex] = newTrackSongs;
  333. sortableUpdateNumber.value += 1;
  334. };
  335. const trackSongRemoved = (trackIndex, event) => {
  336. trackSongs.value[trackIndex].splice(event.oldIndex, 1);
  337. sortableUpdateNumber.value += 1;
  338. };
  339. onMounted(() => {
  340. ws.onConnect(init);
  341. socket.on("event:admin.song.updated", res => {
  342. updateTrackSong(res.data.song);
  343. });
  344. });
  345. onBeforeUnmount(() => {
  346. selectDiscogsAlbum({});
  347. setPlaylistSongs([]);
  348. showDiscogsTab("search");
  349. socket.dispatch("apis.leaveRoom", "import-album");
  350. // Delete the Pinia store that was created for this modal, after all other cleanup tasks are performed
  351. importAlbumStore.$dispose();
  352. });
  353. </script>
  354. <template>
  355. <div>
  356. <modal title="Import Album" class="import-album-modal">
  357. <template #body>
  358. <div class="tabs-container discogs-container">
  359. <div class="tab-selection">
  360. <button
  361. class="button is-default"
  362. :class="{ selected: discogsTab === 'search' }"
  363. :ref="
  364. el => (discogsTabs['discogs-search-tab'] = el)
  365. "
  366. @click="showDiscogsTab('search')"
  367. >
  368. Search
  369. </button>
  370. <button
  371. v-if="discogsAlbum && discogsAlbum.album"
  372. class="button is-default"
  373. :class="{ selected: discogsTab === 'selected' }"
  374. :ref="
  375. el => (discogsTabs['discogs-selected-tab'] = el)
  376. "
  377. @click="showDiscogsTab('selected')"
  378. >
  379. Selected
  380. </button>
  381. <button
  382. v-else
  383. class="button is-default"
  384. content="No album selected"
  385. v-tippy="{ theme: 'info' }"
  386. >
  387. Selected
  388. </button>
  389. </div>
  390. <div
  391. class="tab search-discogs-album"
  392. v-show="discogsTab === 'search'"
  393. >
  394. <p class="control is-expanded">
  395. <label class="label">Search query</label>
  396. <input
  397. class="input"
  398. type="text"
  399. v-model="discogsQuery"
  400. @keyup.enter="searchDiscogsForPage(1)"
  401. @change="onDiscogsQueryChange"
  402. v-focus
  403. />
  404. </p>
  405. <button
  406. class="button is-fullwidth is-info"
  407. @click="searchDiscogsForPage(1)"
  408. >
  409. Search
  410. </button>
  411. <button
  412. class="button is-fullwidth is-danger"
  413. @click="clearDiscogsResults()"
  414. >
  415. Clear
  416. </button>
  417. <label
  418. class="label"
  419. v-if="discogs.apiResults.length > 0"
  420. >API results</label
  421. >
  422. <div
  423. class="api-results-container"
  424. v-if="discogs.apiResults.length > 0"
  425. >
  426. <div
  427. class="api-result"
  428. v-for="(result, index) in discogs.apiResults"
  429. :key="result.album.id"
  430. tabindex="0"
  431. @keydown.space.prevent
  432. @keyup.enter="toggleAPIResult(index)"
  433. >
  434. <div class="top-container">
  435. <img :src="result.album.albumArt" />
  436. <div class="right-container">
  437. <p class="album-title">
  438. {{ result.album.title }}
  439. </p>
  440. <div class="bottom-row">
  441. <img
  442. src="/assets/arrow_up.svg"
  443. v-if="result.expanded"
  444. @click="toggleAPIResult(index)"
  445. />
  446. <img
  447. src="/assets/arrow_down.svg"
  448. v-if="!result.expanded"
  449. @click="toggleAPIResult(index)"
  450. />
  451. <p class="type-year">
  452. <span>{{
  453. result.album.type
  454. }}</span>
  455. <span>{{
  456. result.album.year
  457. }}</span>
  458. </p>
  459. </div>
  460. </div>
  461. </div>
  462. <div
  463. class="bottom-container"
  464. v-if="result.expanded"
  465. >
  466. <p class="bottom-container-field">
  467. Artists:
  468. <span>{{
  469. result.album.artists.join(", ")
  470. }}</span>
  471. </p>
  472. <p class="bottom-container-field">
  473. Genres:
  474. <span>{{
  475. result.album.genres.join(", ")
  476. }}</span>
  477. </p>
  478. <p class="bottom-container-field">
  479. Data quality:
  480. <span>{{ result.dataQuality }}</span>
  481. </p>
  482. <button
  483. class="button is-primary"
  484. @click="selectAlbum(result)"
  485. >
  486. Import album
  487. </button>
  488. <div class="tracks">
  489. <div
  490. class="track"
  491. v-for="track in result.tracks"
  492. :key="`${track.position}-${track.title}`"
  493. >
  494. <span>{{ track.position }}.</span>
  495. <p>{{ track.title }}</p>
  496. </div>
  497. </div>
  498. </div>
  499. </div>
  500. </div>
  501. <button
  502. v-if="
  503. discogs.apiResults.length > 0 &&
  504. !discogs.disableLoadMore &&
  505. discogs.page < discogs.pages
  506. "
  507. class="button is-fullwidth is-info discogs-load-more"
  508. @click="loadNextDiscogsPage()"
  509. >
  510. Load more...
  511. </button>
  512. </div>
  513. <div
  514. v-if="discogsAlbum && discogsAlbum.album"
  515. class="tab discogs-album"
  516. v-show="discogsTab === 'selected'"
  517. >
  518. <div class="top-container">
  519. <img :src="discogsAlbum.album.albumArt" />
  520. <div class="right-container">
  521. <p class="album-title">
  522. {{ discogsAlbum.album.title }}
  523. </p>
  524. <div class="bottom-row">
  525. <img
  526. src="/assets/arrow_up.svg"
  527. v-if="discogsAlbum.expanded"
  528. @click="toggleDiscogsAlbum()"
  529. />
  530. <img
  531. src="/assets/arrow_down.svg"
  532. v-if="!discogsAlbum.expanded"
  533. @click="toggleDiscogsAlbum()"
  534. />
  535. <p class="type-year">
  536. <span>{{
  537. discogsAlbum.album.type
  538. }}</span>
  539. <span>{{
  540. discogsAlbum.album.year
  541. }}</span>
  542. </p>
  543. </div>
  544. </div>
  545. </div>
  546. <div
  547. class="bottom-container"
  548. v-if="discogsAlbum.expanded"
  549. >
  550. <p class="bottom-container-field">
  551. Artists:
  552. <span>{{
  553. discogsAlbum.album.artists.join(", ")
  554. }}</span>
  555. </p>
  556. <p class="bottom-container-field">
  557. Genres:
  558. <span>{{
  559. discogsAlbum.album.genres.join(", ")
  560. }}</span>
  561. </p>
  562. <p class="bottom-container-field">
  563. Data quality:
  564. <span>{{ discogsAlbum.dataQuality }}</span>
  565. </p>
  566. <div class="tracks">
  567. <div
  568. class="track"
  569. tabindex="0"
  570. v-for="track in discogsAlbum.tracks"
  571. :key="`${track.position}-${track.title}`"
  572. >
  573. <span>{{ track.position }}.</span>
  574. <p>{{ track.title }}</p>
  575. </div>
  576. </div>
  577. </div>
  578. </div>
  579. </div>
  580. <div class="import-youtube-playlist">
  581. <p class="control is-expanded">
  582. <input
  583. class="input"
  584. type="text"
  585. placeholder="Enter YouTube Playlist URL here..."
  586. v-model="search.playlist.query"
  587. @keyup.enter="importPlaylist()"
  588. />
  589. </p>
  590. <button
  591. class="button is-fullwidth is-info"
  592. @click="importPlaylist()"
  593. >
  594. <i class="material-icons icon-with-button">publish</i
  595. >Import
  596. </button>
  597. <button
  598. class="button is-fullwidth is-danger"
  599. @click="resetTrackSongs()"
  600. >
  601. Reset
  602. </button>
  603. <sortable
  604. :key="`${sortableUpdateNumber}-playlistSongs`"
  605. v-if="playlistSongs.length > 0"
  606. :list="playlistSongs"
  607. item-key="_id"
  608. :options="{ group: 'songs' }"
  609. @update="updatePlaylistSongPosition"
  610. @add="playlistSongAdded"
  611. @remove="playlistSongRemoved"
  612. >
  613. <template #item="{ element }">
  614. <song-item
  615. :key="`playlist-song-${element._id}`"
  616. :song="element"
  617. >
  618. </song-item>
  619. </template>
  620. </sortable>
  621. </div>
  622. <div
  623. class="track-boxes"
  624. v-if="discogsAlbum && discogsAlbum.album"
  625. >
  626. <div
  627. class="track-box"
  628. v-for="(track, index) in discogsAlbum.tracks"
  629. :key="`${track.position}-${track.title}`"
  630. >
  631. <div class="track-position-title">
  632. <span>{{ track.position }}.</span>
  633. <p>{{ track.title }}</p>
  634. </div>
  635. <sortable
  636. :key="`${sortableUpdateNumber}-${index}-playlistSongs`"
  637. class="track-box-songs-drag-area"
  638. :list="trackSongs[index]"
  639. :data-track-index="index"
  640. item-key="_id"
  641. :options="{ group: 'songs' }"
  642. @update="updateTrackSongPosition(index, $event)"
  643. @add="trackSongAdded(index, $event)"
  644. @remove="trackSongRemoved(index, $event)"
  645. >
  646. <template #item="{ element }">
  647. <song-item
  648. :key="`track-song-${element._id}`"
  649. :song="element"
  650. >
  651. </song-item>
  652. </template>
  653. </sortable>
  654. </div>
  655. </div>
  656. </template>
  657. <template #footer>
  658. <button class="button is-primary" @click="tryToAutoMove()">
  659. Try to auto move
  660. </button>
  661. <button class="button is-primary" @click="startEditingSongs()">
  662. Edit songs
  663. </button>
  664. <p class="is-expanded checkbox-control">
  665. <label class="switch">
  666. <input
  667. type="checkbox"
  668. id="prefill-discogs"
  669. v-model="localPrefillDiscogs"
  670. />
  671. <span class="slider round"></span>
  672. </label>
  673. <label for="prefill-discogs">
  674. <p>Prefill Discogs</p>
  675. </label>
  676. </p>
  677. </template>
  678. </modal>
  679. </div>
  680. </template>
  681. <style lang="less">
  682. .night-mode {
  683. .search-discogs-album,
  684. .discogs-album,
  685. .import-youtube-playlist,
  686. .track-boxes,
  687. #tabs-container {
  688. background-color: var(--dark-grey-3) !important;
  689. border: 0 !important;
  690. .tab {
  691. border: 0 !important;
  692. }
  693. }
  694. #tabs-container #tab-selection .button {
  695. background: var(--dark-grey) !important;
  696. color: var(--white) !important;
  697. }
  698. .api-result {
  699. background-color: var(--dark-grey-3) !important;
  700. }
  701. .api-result .tracks .track:hover,
  702. .api-result .tracks .track:focus,
  703. .discogs-album .tracks .track:hover,
  704. .discogs-album .tracks .track:focus {
  705. background-color: var(--dark-grey-2) !important;
  706. }
  707. .api-result .bottom-row img,
  708. .discogs-album .bottom-row img {
  709. filter: invert(100%);
  710. }
  711. .label,
  712. p,
  713. strong {
  714. color: var(--light-grey-2);
  715. }
  716. }
  717. .import-album-modal {
  718. .modal-card-title {
  719. text-align: center;
  720. margin-left: 24px;
  721. }
  722. .modal-card {
  723. width: 100%;
  724. height: 100%;
  725. .modal-card-body {
  726. padding: 16px;
  727. display: flex;
  728. flex-direction: row;
  729. flex-wrap: wrap;
  730. justify-content: space-evenly;
  731. }
  732. .modal-card-foot {
  733. .button {
  734. margin: 0;
  735. &:not(:first-of-type) {
  736. margin-left: 5px;
  737. }
  738. }
  739. div div {
  740. margin-right: 5px;
  741. }
  742. .right {
  743. display: flex;
  744. margin-left: auto;
  745. margin-right: 0;
  746. }
  747. }
  748. }
  749. }
  750. </style>
  751. <style lang="less" scoped>
  752. .break {
  753. flex-basis: 100%;
  754. height: 0;
  755. border: 1px solid var(--dark-grey);
  756. margin-top: 16px;
  757. margin-bottom: 16px;
  758. }
  759. .tabs-container {
  760. max-width: 376px;
  761. height: 100%;
  762. display: flex;
  763. flex-direction: column;
  764. flex-grow: 1;
  765. .tab-selection {
  766. display: flex;
  767. overflow-x: auto;
  768. .button {
  769. border-radius: @border-radius @border-radius 0 0;
  770. border: 0;
  771. text-transform: uppercase;
  772. font-size: 14px;
  773. color: var(--dark-grey-3);
  774. background-color: var(--light-grey-2);
  775. flex-grow: 1;
  776. height: 32px;
  777. &:not(:first-of-type) {
  778. margin-left: 5px;
  779. }
  780. }
  781. .selected {
  782. background-color: var(--primary-color) !important;
  783. color: var(--white) !important;
  784. font-weight: 600;
  785. }
  786. }
  787. .tab {
  788. border: 1px solid var(--light-grey-3);
  789. border-radius: 0 0 @border-radius @border-radius;
  790. padding: 15px;
  791. height: calc(100% - 32px);
  792. overflow: auto;
  793. }
  794. }
  795. .tabs-container.discogs-container {
  796. --primary-color: var(--purple);
  797. .search-discogs-album {
  798. background-color: var(--light-grey);
  799. border: 1px rgba(143, 40, 140, 0.75) solid;
  800. > label {
  801. margin-top: 12px;
  802. }
  803. .top-container {
  804. display: flex;
  805. img {
  806. height: 85px;
  807. width: 85px;
  808. }
  809. .right-container {
  810. padding: 8px;
  811. display: flex;
  812. flex-direction: column;
  813. flex: 1;
  814. .album-title {
  815. flex: 1;
  816. font-weight: 600;
  817. }
  818. .bottom-row {
  819. display: flex;
  820. flex-flow: row;
  821. line-height: 15px;
  822. img {
  823. height: 15px;
  824. align-self: end;
  825. flex: 1;
  826. user-select: none;
  827. -moz-user-select: none;
  828. -ms-user-select: none;
  829. -webkit-user-select: none;
  830. cursor: pointer;
  831. }
  832. p {
  833. text-align: right;
  834. }
  835. .type-year {
  836. font-size: 13px;
  837. align-self: end;
  838. }
  839. }
  840. }
  841. }
  842. .bottom-container {
  843. padding: 12px;
  844. .bottom-container-field {
  845. line-height: 16px;
  846. margin-bottom: 8px;
  847. font-weight: 600;
  848. span {
  849. font-weight: 400;
  850. }
  851. }
  852. .bottom-container-field:last-of-type {
  853. margin-bottom: 8px;
  854. }
  855. }
  856. .api-result {
  857. background-color: var(--white);
  858. border: 0.5px solid var(--primary-color);
  859. border-radius: @border-radius;
  860. margin-bottom: 16px;
  861. }
  862. button {
  863. margin: 5px 0;
  864. &:focus,
  865. &:hover {
  866. filter: contrast(0.75);
  867. }
  868. }
  869. .tracks {
  870. margin-top: 12px;
  871. .track:first-child {
  872. margin-top: 0;
  873. border-radius: @border-radius @border-radius 0 0;
  874. }
  875. .track:last-child {
  876. border-radius: 0 0 @border-radius @border-radius;
  877. }
  878. .track {
  879. border: 0.5px solid var(--black);
  880. margin-top: -1px;
  881. line-height: 16px;
  882. display: flex;
  883. span {
  884. font-weight: 600;
  885. display: inline-block;
  886. margin-top: 7px;
  887. margin-bottom: 7px;
  888. margin-left: 7px;
  889. }
  890. p {
  891. display: inline-block;
  892. margin: 7px;
  893. flex: 1;
  894. }
  895. }
  896. }
  897. .discogs-load-more {
  898. margin-bottom: 8px;
  899. }
  900. }
  901. .discogs-album {
  902. background-color: var(--light-grey);
  903. border: 1px rgba(143, 40, 140, 0.75) solid;
  904. .top-container {
  905. display: flex;
  906. img {
  907. height: 85px;
  908. width: 85px;
  909. }
  910. .right-container {
  911. padding: 8px;
  912. display: flex;
  913. flex-direction: column;
  914. flex: 1;
  915. .album-title {
  916. flex: 1;
  917. font-weight: 600;
  918. }
  919. .bottom-row {
  920. display: flex;
  921. flex-flow: row;
  922. line-height: 15px;
  923. img {
  924. height: 15px;
  925. align-self: end;
  926. flex: 1;
  927. user-select: none;
  928. -moz-user-select: none;
  929. -ms-user-select: none;
  930. -webkit-user-select: none;
  931. cursor: pointer;
  932. }
  933. p {
  934. text-align: right;
  935. }
  936. .type-year {
  937. font-size: 13px;
  938. align-self: end;
  939. }
  940. }
  941. }
  942. }
  943. .bottom-container {
  944. padding: 12px;
  945. .bottom-container-field {
  946. line-height: 16px;
  947. margin-bottom: 8px;
  948. font-weight: 600;
  949. span {
  950. font-weight: 400;
  951. }
  952. }
  953. .bottom-container-field:last-of-type {
  954. margin-bottom: 0;
  955. }
  956. .tracks {
  957. margin-top: 12px;
  958. .track:first-child {
  959. margin-top: 0;
  960. border-radius: @border-radius @border-radius 0 0;
  961. }
  962. .track:last-child {
  963. border-radius: 0 0 @border-radius @border-radius;
  964. }
  965. .track {
  966. border: 0.5px solid var(--black);
  967. margin-top: -1px;
  968. line-height: 16px;
  969. display: flex;
  970. span {
  971. font-weight: 600;
  972. display: inline-block;
  973. margin-top: 7px;
  974. margin-bottom: 7px;
  975. margin-left: 7px;
  976. }
  977. p {
  978. display: inline-block;
  979. margin: 7px;
  980. flex: 1;
  981. }
  982. }
  983. .track:hover,
  984. .track:focus {
  985. background-color: var(--light-grey);
  986. }
  987. }
  988. }
  989. }
  990. }
  991. .import-youtube-playlist {
  992. width: 376px;
  993. background-color: var(--light-grey);
  994. border: 1px rgba(163, 224, 255, 0.75) solid;
  995. border-radius: @border-radius;
  996. padding: 16px;
  997. overflow: auto;
  998. height: 100%;
  999. button {
  1000. margin: 5px 0;
  1001. }
  1002. }
  1003. .track-boxes {
  1004. width: 376px;
  1005. background-color: var(--light-grey);
  1006. border: 1px rgba(163, 224, 255, 0.75) solid;
  1007. border-radius: @border-radius;
  1008. padding: 16px;
  1009. overflow: auto;
  1010. height: 100%;
  1011. .track-box:first-child {
  1012. margin-top: 0;
  1013. border-radius: @border-radius @border-radius 0 0;
  1014. }
  1015. .track-box:last-child {
  1016. border-radius: 0 0 @border-radius @border-radius;
  1017. }
  1018. .track-box {
  1019. border: 0.5px solid var(--black);
  1020. margin-top: -1px;
  1021. line-height: 16px;
  1022. display: flex;
  1023. flex-flow: column;
  1024. .track-position-title {
  1025. display: flex;
  1026. span {
  1027. font-weight: 600;
  1028. display: inline-block;
  1029. margin-top: 7px;
  1030. margin-bottom: 7px;
  1031. margin-left: 7px;
  1032. }
  1033. p {
  1034. display: inline-block;
  1035. margin: 7px;
  1036. flex: 1;
  1037. }
  1038. }
  1039. .track-box-songs-drag-area {
  1040. flex: 1;
  1041. min-height: 100px;
  1042. }
  1043. }
  1044. }
  1045. </style>