Discogs.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. <script setup lang="ts">
  2. import { ref, onMounted } from "vue";
  3. import { storeToRefs } from "pinia";
  4. import Toast from "toasters";
  5. import keyboardShortcuts from "@/keyboardShortcuts";
  6. import { useEditSongStore } from "@/stores/editSong";
  7. import { useWebsocketsStore } from "@/stores/websockets";
  8. import { useUserAuthStore } from "@/stores/userAuth";
  9. const props = defineProps({
  10. modalUuid: { type: String, required: true },
  11. modalModulePath: {
  12. type: String,
  13. default: "modals/editSong/MODAL_UUID"
  14. },
  15. bulk: { type: Boolean, default: false }
  16. });
  17. const editSongStore = useEditSongStore({ modalUuid: props.modalUuid });
  18. const { socket } = useWebsocketsStore();
  19. const { form } = storeToRefs(editSongStore);
  20. const { selectDiscogsInfo } = editSongStore;
  21. const { hasPermission } = useUserAuthStore();
  22. const discogs = ref({
  23. apiResults: [],
  24. page: 1,
  25. pages: 1,
  26. disableLoadMore: false
  27. });
  28. const discogsQuery = ref("");
  29. const discogsInput = ref();
  30. const toggleAPIResult = index => {
  31. const apiResult = discogs.value.apiResults[index];
  32. if (apiResult.expanded === true) apiResult.expanded = false;
  33. else if (apiResult.gotMoreInfo === true) apiResult.expanded = true;
  34. else {
  35. fetch(apiResult.album.resourceUrl)
  36. .then(response => response.json())
  37. .then(data => {
  38. apiResult.album.artists = [];
  39. apiResult.album.artistIds = [];
  40. const artistRegex = /\\([0-9]+\\)$/;
  41. apiResult.dataQuality = data.data_quality;
  42. data.artists.forEach(artist => {
  43. apiResult.album.artists.push(
  44. artist.name.replace(artistRegex, "")
  45. );
  46. apiResult.album.artistIds.push(artist.id);
  47. });
  48. apiResult.tracks = data.tracklist.map(track => ({
  49. position: track.position,
  50. title: track.title
  51. }));
  52. apiResult.expanded = true;
  53. apiResult.gotMoreInfo = true;
  54. });
  55. }
  56. };
  57. const searchDiscogsForPage = page => {
  58. const query = discogsQuery.value;
  59. socket.dispatch("apis.searchDiscogs", query, page, res => {
  60. if (res.status === "success") {
  61. if (page === 1)
  62. new Toast(
  63. `Successfully searched. Got ${res.data.results.length} results.`
  64. );
  65. else
  66. new Toast(
  67. `Successfully got ${res.data.results.length} more results.`
  68. );
  69. if (page === 1) {
  70. discogs.value.apiResults = [];
  71. }
  72. discogs.value.pages = res.data.pages;
  73. discogs.value.apiResults = discogs.value.apiResults.concat(
  74. res.data.results.map(result => {
  75. const type =
  76. result.type.charAt(0).toUpperCase() +
  77. result.type.slice(1);
  78. return {
  79. expanded: false,
  80. gotMoreInfo: false,
  81. album: {
  82. id: result.id,
  83. title: result.title,
  84. type,
  85. year: result.year,
  86. genres: result.genre,
  87. albumArt: result.cover_image,
  88. resourceUrl: result.resource_url
  89. }
  90. };
  91. })
  92. );
  93. discogs.value.page = page;
  94. discogs.value.disableLoadMore = false;
  95. } else new Toast(res.message);
  96. });
  97. };
  98. const loadNextDiscogsPage = () => {
  99. discogs.value.disableLoadMore = true;
  100. searchDiscogsForPage(discogs.value.page + 1);
  101. };
  102. const onDiscogsQueryChange = () => {
  103. discogs.value.page = 1;
  104. discogs.value.pages = 1;
  105. discogs.value.apiResults = [];
  106. discogs.value.disableLoadMore = false;
  107. };
  108. const selectTrack = (apiResultIndex, trackIndex) => {
  109. const apiResult = JSON.parse(
  110. JSON.stringify(discogs.value.apiResults[apiResultIndex])
  111. );
  112. apiResult.track = apiResult.tracks[trackIndex];
  113. delete apiResult.tracks;
  114. delete apiResult.expanded;
  115. delete apiResult.gotMoreInfo;
  116. selectDiscogsInfo(apiResult);
  117. };
  118. onMounted(() => {
  119. discogsQuery.value = form.value.inputs.title.value;
  120. keyboardShortcuts.registerShortcut("editSong.focusDiscogs", {
  121. keyCode: 35,
  122. preventDefault: true,
  123. handler: () => {
  124. discogsInput.value.focus();
  125. }
  126. });
  127. });
  128. </script>
  129. <template>
  130. <div class="discogs-tab">
  131. <div
  132. class="selected-discogs-info"
  133. v-if="form.inputs.discogs.value && form.inputs.discogs.value.album"
  134. >
  135. <div class="top-container">
  136. <img :src="form.inputs.discogs.value.album.albumArt" />
  137. <div class="right-container">
  138. <p class="album-title">
  139. {{ form.inputs.discogs.value.album.title }}
  140. </p>
  141. <div class="bottom-row">
  142. <p class="type-year">
  143. <span>{{
  144. form.inputs.discogs.value.album.type
  145. }}</span>
  146. <span>{{
  147. form.inputs.discogs.value.album.year
  148. }}</span>
  149. </p>
  150. </div>
  151. </div>
  152. </div>
  153. <div class="bottom-container">
  154. <p class="bottom-container-field">
  155. Artists:
  156. <span>{{
  157. form.inputs.discogs.value.album.artists.join(", ")
  158. }}</span>
  159. </p>
  160. <p class="bottom-container-field">
  161. Genres:
  162. <span>{{
  163. form.inputs.discogs.value.album.genres.join(", ")
  164. }}</span>
  165. </p>
  166. <p class="bottom-container-field">
  167. Data quality:
  168. <span>{{ form.inputs.discogs.value.dataQuality }}</span>
  169. </p>
  170. <p class="bottom-container-field">
  171. Track:
  172. <span
  173. >{{ form.inputs.discogs.value.track.position }}.
  174. {{ form.inputs.discogs.value.track.title }}</span
  175. >
  176. </p>
  177. </div>
  178. </div>
  179. <div class="selected-discogs-info" v-else>
  180. <p class="selected-discogs-info-none">None</p>
  181. </div>
  182. <template v-if="hasPermission('apis.searchDiscogs')">
  183. <label class="label"> Search for a song from Discogs </label>
  184. <div class="control is-grouped input-with-button">
  185. <p class="control is-expanded">
  186. <input
  187. class="input"
  188. type="text"
  189. placeholder="Enter your Discogs query here..."
  190. ref="discogsInput"
  191. v-model="discogsQuery"
  192. @keyup.enter="searchDiscogsForPage(1)"
  193. @change="onDiscogsQueryChange"
  194. v-focus
  195. />
  196. </p>
  197. <p class="control">
  198. <button
  199. class="button is-info"
  200. @click="searchDiscogsForPage(1)"
  201. >
  202. <i class="material-icons icon-with-button">search</i
  203. >Search
  204. </button>
  205. </p>
  206. </div>
  207. <label class="label" v-if="discogs.apiResults.length > 0"
  208. >API results</label
  209. >
  210. <div
  211. class="api-results-container"
  212. v-if="discogs.apiResults.length > 0"
  213. >
  214. <div
  215. class="api-result"
  216. v-for="(result, index) in discogs.apiResults"
  217. :key="result.album.id"
  218. tabindex="0"
  219. @keydown.space.prevent
  220. @keyup.enter="toggleAPIResult(index)"
  221. >
  222. <div class="top-container">
  223. <img :src="result.album.albumArt" />
  224. <div class="right-container">
  225. <p class="album-title">
  226. {{ result.album.title }}
  227. </p>
  228. <div class="bottom-row">
  229. <img
  230. src="/assets/arrow_up.svg"
  231. v-if="result.expanded"
  232. @click="toggleAPIResult(index)"
  233. />
  234. <img
  235. src="/assets/arrow_down.svg"
  236. v-if="!result.expanded"
  237. @click="toggleAPIResult(index)"
  238. />
  239. <p class="type-year">
  240. <span>{{ result.album.type }}</span>
  241. <span>{{ result.album.year }}</span>
  242. </p>
  243. </div>
  244. </div>
  245. </div>
  246. <div class="bottom-container" v-if="result.expanded">
  247. <p class="bottom-container-field">
  248. Artists:
  249. <span>{{ result.album.artists.join(", ") }}</span>
  250. </p>
  251. <p class="bottom-container-field">
  252. Genres:
  253. <span>{{ result.album.genres.join(", ") }}</span>
  254. </p>
  255. <p class="bottom-container-field">
  256. Data quality:
  257. <span>{{ result.dataQuality }}</span>
  258. </p>
  259. <div class="tracks">
  260. <div
  261. class="track"
  262. tabindex="0"
  263. v-for="(track, trackIndex) in result.tracks"
  264. :key="`${track.position}-${track.title}`"
  265. @click="selectTrack(index, trackIndex)"
  266. @keyup.enter="selectTrack(index, trackIndex)"
  267. >
  268. <span>{{ track.position }}.</span>
  269. <p>{{ track.title }}</p>
  270. </div>
  271. </div>
  272. </div>
  273. </div>
  274. <button
  275. v-if="
  276. discogs.apiResults.length > 0 &&
  277. !discogs.disableLoadMore &&
  278. discogs.page < discogs.pages
  279. "
  280. class="button is-fullwidth is-info discogs-load-more"
  281. @click="loadNextDiscogsPage()"
  282. >
  283. Load more...
  284. </button>
  285. </div>
  286. </template>
  287. </div>
  288. </template>
  289. <style lang="less" scoped>
  290. .night-mode {
  291. .api-section,
  292. .api-result {
  293. background-color: var(--dark-grey-3) !important;
  294. }
  295. .api-result .tracks .track:hover,
  296. .api-result .tracks .track:focus,
  297. .selected-discogs-info {
  298. background-color: var(--dark-grey-2) !important;
  299. }
  300. .label,
  301. p,
  302. strong {
  303. color: var(--light-grey-2);
  304. }
  305. .discogs-tab .top-container .right-container .bottom-row img {
  306. filter: invert(100%);
  307. }
  308. }
  309. .discogs-tab {
  310. > label {
  311. margin-top: 12px;
  312. }
  313. .top-container {
  314. display: flex;
  315. img {
  316. height: 85px;
  317. width: 85px;
  318. }
  319. .right-container {
  320. padding: 8px;
  321. display: flex;
  322. flex-direction: column;
  323. flex: 1;
  324. .album-title {
  325. flex: 1;
  326. font-weight: 600;
  327. }
  328. .bottom-row {
  329. display: flex;
  330. flex-flow: row;
  331. line-height: 15px;
  332. img {
  333. height: 15px;
  334. align-self: end;
  335. flex: 1;
  336. user-select: none;
  337. -moz-user-select: none;
  338. -ms-user-select: none;
  339. -webkit-user-select: none;
  340. cursor: pointer;
  341. }
  342. p {
  343. text-align: right;
  344. }
  345. .type-year {
  346. font-size: 13px;
  347. align-self: end;
  348. }
  349. }
  350. }
  351. }
  352. .bottom-container {
  353. padding: 12px;
  354. .bottom-container-field {
  355. line-height: 16px;
  356. margin-bottom: 8px;
  357. font-weight: 600;
  358. span {
  359. font-weight: 400;
  360. }
  361. }
  362. .bottom-container-field:last-of-type {
  363. margin-bottom: 8px;
  364. }
  365. }
  366. .selected-discogs-info {
  367. background-color: var(--white);
  368. border: 1px solid var(--light-grey-3);
  369. border-radius: @border-radius;
  370. margin-bottom: 16px;
  371. .selected-discogs-info-none {
  372. font-size: 18px;
  373. text-align: center;
  374. }
  375. .bottom-row > p {
  376. flex: 1;
  377. }
  378. }
  379. .api-results-container {
  380. .api-result {
  381. background-color: var(--white);
  382. border: 0.5px solid var(--light-grey-3);
  383. border-radius: @border-radius;
  384. margin-bottom: 16px;
  385. }
  386. }
  387. button {
  388. background-color: var(--primary-color) !important;
  389. &:focus,
  390. &:hover {
  391. filter: contrast(0.75);
  392. }
  393. }
  394. .tracks {
  395. margin-top: 12px;
  396. .track:first-child {
  397. margin-top: 0;
  398. border-radius: @border-radius @border-radius 0 0;
  399. }
  400. .track:last-child {
  401. border-radius: 0 0 @border-radius @border-radius;
  402. }
  403. .track {
  404. border: 0.5px solid var(--black);
  405. margin-top: -1px;
  406. line-height: 16px;
  407. display: flex;
  408. cursor: pointer;
  409. span {
  410. font-weight: 600;
  411. display: inline-block;
  412. margin-top: 7px;
  413. margin-bottom: 7px;
  414. margin-left: 7px;
  415. }
  416. p {
  417. display: inline-block;
  418. margin: 7px;
  419. flex: 1;
  420. }
  421. }
  422. .track:hover,
  423. .track:focus {
  424. background-color: var(--light-grey);
  425. }
  426. }
  427. .discogs-load-more {
  428. margin-bottom: 8px;
  429. }
  430. }
  431. </style>