ImportAlbum.vue 24 KB

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