ImportAlbum.vue 24 KB

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