ImportAlbum.vue 24 KB

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