ImportAlbum.vue 24 KB

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