ImportAlbum.vue 24 KB

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