VerifiedSongs.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. <template>
  2. <div>
  3. <page-metadata title="Admin | Songs" />
  4. <div class="container">
  5. <p>
  6. <span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
  7. <br />
  8. <span>Loaded songs: {{ songs.length }}</span>
  9. </p>
  10. <input
  11. v-model="searchQuery"
  12. type="text"
  13. class="input"
  14. placeholder="Search for Songs"
  15. />
  16. <button
  17. v-if="!loadAllSongs"
  18. class="button is-primary"
  19. @click="loadAll()"
  20. >
  21. Load all
  22. </button>
  23. <button
  24. class="button is-primary"
  25. @click="toggleKeyboardShortcutsHelper"
  26. @dblclick="resetKeyboardShortcutsHelper"
  27. >
  28. Keyboard shortcuts helper
  29. </button>
  30. <button class="button is-primary" @click="openModal('requestSong')">
  31. Request song
  32. </button>
  33. <button class="button is-primary" @click="openModal('importAlbum')">
  34. Import album
  35. </button>
  36. <confirm placement="bottom" @confirm="updateAllSongs()">
  37. <button
  38. class="button is-danger"
  39. content="Update all songs"
  40. v-tippy
  41. >
  42. Update all songs
  43. </button>
  44. </confirm>
  45. <br />
  46. <div>
  47. <input
  48. type="text"
  49. placeholder="Filter artist checkboxes"
  50. v-model="artistFilterQuery"
  51. />
  52. <label v-for="artist in filteredArtists" :key="artist">
  53. <input
  54. type="checkbox"
  55. :checked="artistFilterSelected.indexOf(artist) !== -1"
  56. @click="toggleArtistSelected(artist)"
  57. />
  58. <span>{{ artist }}</span>
  59. </label>
  60. </div>
  61. <div>
  62. <input
  63. type="text"
  64. placeholder="Filter genre checkboxes"
  65. v-model="genreFilterQuery"
  66. />
  67. <label v-for="genre in filteredGenres" :key="genre">
  68. <input
  69. type="checkbox"
  70. :checked="genreFilterSelected.indexOf(genre) !== -1"
  71. @click="toggleGenreSelected(genre)"
  72. />
  73. <span>{{ genre }}</span>
  74. </label>
  75. </div>
  76. <br />
  77. <table class="table is-striped">
  78. <thead>
  79. <tr>
  80. <td>Thumbnail</td>
  81. <td>Title</td>
  82. <td>Artists</td>
  83. <td>Genres</td>
  84. <td class="likesColumn">
  85. <i class="material-icons thumbLike">thumb_up</i>
  86. </td>
  87. <td class="dislikesColumn">
  88. <i class="material-icons thumbDislike"
  89. >thumb_down</i
  90. >
  91. </td>
  92. <td>ID / Youtube ID</td>
  93. <td>Requested By</td>
  94. <td>Options</td>
  95. </tr>
  96. </thead>
  97. <tbody>
  98. <tr v-for="song in filteredSongs" :key="song._id">
  99. <td>
  100. <img
  101. class="song-thumbnail"
  102. :src="song.thumbnail"
  103. onerror="this.src='/assets/notes-transparent.png'"
  104. />
  105. </td>
  106. <td>
  107. <strong>{{ song.title }}</strong>
  108. </td>
  109. <td>{{ song.artists.join(", ") }}</td>
  110. <td>{{ song.genres.join(", ") }}</td>
  111. <td>{{ song.likes }}</td>
  112. <td>{{ song.dislikes }}</td>
  113. <td>
  114. {{ song._id }}
  115. <br />
  116. <a
  117. :href="
  118. 'https://www.youtube.com/watch?v=' +
  119. `${song.youtubeId}`
  120. "
  121. target="_blank"
  122. >
  123. {{ song.youtubeId }}</a
  124. >
  125. </td>
  126. <td>
  127. <user-id-to-username
  128. :user-id="song.requestedBy"
  129. :link="true"
  130. />
  131. </td>
  132. <td class="optionsColumn">
  133. <div>
  134. <button
  135. class="button is-primary"
  136. @click="edit(song)"
  137. content="Edit Song"
  138. v-tippy
  139. >
  140. <i class="material-icons">edit</i>
  141. </button>
  142. <confirm
  143. placement="left"
  144. @confirm="unverify(song._id)"
  145. >
  146. <button
  147. class="button is-danger"
  148. content="Unverify Song"
  149. v-tippy
  150. >
  151. <i class="material-icons">cancel</i>
  152. </button>
  153. </confirm>
  154. </div>
  155. </td>
  156. </tr>
  157. </tbody>
  158. </table>
  159. </div>
  160. <import-album v-if="modals.importAlbum" />
  161. <edit-song v-if="modals.editSong" song-type="songs" />
  162. <request-song v-if="modals.requestSong" />
  163. <floating-box
  164. id="keyboardShortcutsHelper"
  165. ref="keyboardShortcutsHelper"
  166. >
  167. <template #body>
  168. <div>
  169. <div>
  170. <span class="biggest"
  171. ><b>Keyboard shortcuts helper</b></span
  172. >
  173. <span
  174. ><b>Ctrl + /</b> - Toggles this keyboard shortcuts
  175. helper</span
  176. >
  177. <span
  178. ><b>Ctrl + Shift + /</b> - Resets the position of
  179. this keyboard shortcuts helper</span
  180. >
  181. <hr />
  182. </div>
  183. <!-- <div>
  184. <span class="biggest"><b>Songs page</b></span>
  185. <span
  186. ><b>Arrow keys up/down</b> - Moves between
  187. songs</span
  188. >
  189. <span><b>E</b> - Edit selected song</span>
  190. <span><b>A</b> - Add selected song</span>
  191. <span><b>X</b> - Delete selected song</span>
  192. <hr />
  193. </div> -->
  194. <div>
  195. <span class="biggest"><b>Edit song modal</b></span>
  196. <span class="bigger"><b>Navigation</b></span>
  197. <span><b>Home</b> - Edit</span>
  198. <span><b>End</b> - Edit</span>
  199. <hr />
  200. </div>
  201. <div>
  202. <span class="bigger"><b>Player controls</b></span>
  203. <span class="bigger"
  204. ><i>Don't forget to turn off numlock!</i></span
  205. >
  206. <span><b>Numpad up/down</b> - Volume up/down 10%</span>
  207. <span
  208. ><b>Ctrl + Numpad up/down</b> - Volume up/down
  209. 1%</span
  210. >
  211. <span><b>Numpad center</b> - Pause/resume</span>
  212. <span><b>Ctrl + Numpad center</b> - Stop</span>
  213. <span
  214. ><b>Numpad Right</b> - Skip to last 10 seconds</span
  215. >
  216. <hr />
  217. </div>
  218. <div>
  219. <span class="bigger"><b>Form control</b></span>
  220. <span
  221. ><b>Enter</b> - Executes blue button in that
  222. input</span
  223. >
  224. <span
  225. ><b>Shift + Enter</b> - Executes purple/red button
  226. in that input</span
  227. >
  228. <span
  229. ><b>Ctrl + Alt + D</b> - Fill in all Discogs
  230. fields</span
  231. >
  232. <hr />
  233. </div>
  234. <div>
  235. <span class="bigger"><b>Modal control</b></span>
  236. <span><b>Ctrl + S</b> - Save</span>
  237. <span><b>Ctrl + Alt + S</b> - Save and close</span>
  238. <span
  239. ><b>Ctrl + Alt + V</b> - Save, verify and
  240. close</span
  241. >
  242. <span><b>F4</b> - Close without saving</span>
  243. <hr />
  244. </div>
  245. </div>
  246. </template>
  247. </floating-box>
  248. </div>
  249. </template>
  250. <script>
  251. import { mapState, mapActions, mapGetters } from "vuex";
  252. import { defineAsyncComponent } from "vue";
  253. import Toast from "toasters";
  254. import keyboardShortcuts from "@/keyboardShortcuts";
  255. import UserIdToUsername from "@/components/UserIdToUsername.vue";
  256. import FloatingBox from "@/components/FloatingBox.vue";
  257. import Confirm from "@/components/Confirm.vue";
  258. import ScrollAndFetchHandler from "@/mixins/ScrollAndFetchHandler.vue";
  259. import ws from "@/ws";
  260. export default {
  261. components: {
  262. EditSong: defineAsyncComponent(() =>
  263. import("@/components/modals/EditSong")
  264. ),
  265. ImportAlbum: defineAsyncComponent(() =>
  266. import("@/components/modals/ImportAlbum.vue")
  267. ),
  268. RequestSong: defineAsyncComponent(() =>
  269. import("@/components/modals/RequestSong.vue")
  270. ),
  271. UserIdToUsername,
  272. FloatingBox,
  273. Confirm
  274. },
  275. mixins: [ScrollAndFetchHandler],
  276. data() {
  277. return {
  278. searchQuery: "",
  279. artistFilterQuery: "",
  280. artistFilterSelected: [],
  281. genreFilterQuery: "",
  282. genreFilterSelected: [],
  283. editing: {
  284. index: 0,
  285. song: {}
  286. }
  287. };
  288. },
  289. computed: {
  290. filteredSongs() {
  291. return this.songs.filter(
  292. song =>
  293. JSON.stringify(Object.values(song))
  294. .toLowerCase()
  295. .indexOf(this.searchQuery.toLowerCase()) !== -1 &&
  296. (this.artistFilterSelected.length === 0 ||
  297. song.artists.some(
  298. artist =>
  299. this.artistFilterSelected
  300. .map(artistFilterSelected =>
  301. artistFilterSelected.toLowerCase()
  302. )
  303. .indexOf(artist.toLowerCase()) !== -1
  304. )) &&
  305. (this.genreFilterSelected.length === 0 ||
  306. song.genres.some(
  307. genre =>
  308. this.genreFilterSelected
  309. .map(genreFilterSelected =>
  310. genreFilterSelected.toLowerCase()
  311. )
  312. .indexOf(genre.toLowerCase()) !== -1
  313. ))
  314. );
  315. },
  316. artists() {
  317. const artists = [];
  318. this.songs.forEach(song => {
  319. song.artists.forEach(artist => {
  320. if (artists.indexOf(artist) === -1) artists.push(artist);
  321. });
  322. });
  323. return artists.sort();
  324. },
  325. filteredArtists() {
  326. return this.artists
  327. .filter(
  328. artist =>
  329. this.artistFilterSelected.indexOf(artist) !== -1 ||
  330. artist
  331. .toLowerCase()
  332. .indexOf(this.artistFilterQuery.toLowerCase()) !==
  333. -1
  334. )
  335. .sort(
  336. (a, b) =>
  337. (this.artistFilterSelected.indexOf(a) === -1 ? 1 : 0) -
  338. (this.artistFilterSelected.indexOf(b) === -1 ? 1 : 0)
  339. );
  340. },
  341. genres() {
  342. const genres = [];
  343. this.songs.forEach(song => {
  344. song.genres.forEach(genre => {
  345. if (genres.indexOf(genre) === -1) genres.push(genre);
  346. });
  347. });
  348. return genres.sort();
  349. },
  350. filteredGenres() {
  351. return this.genres
  352. .filter(
  353. genre =>
  354. this.genreFilterSelected.indexOf(genre) !== -1 ||
  355. genre
  356. .toLowerCase()
  357. .indexOf(this.genreFilterQuery.toLowerCase()) !== -1
  358. )
  359. .sort(
  360. (a, b) =>
  361. (this.genreFilterSelected.indexOf(a) === -1 ? 1 : 0) -
  362. (this.genreFilterSelected.indexOf(b) === -1 ? 1 : 0)
  363. );
  364. },
  365. ...mapState("modalVisibility", {
  366. modals: state => state.modals
  367. }),
  368. ...mapState("admin/verifiedSongs", {
  369. songs: state => state.songs
  370. }),
  371. ...mapGetters({
  372. socket: "websockets/getSocket"
  373. })
  374. },
  375. mounted() {
  376. this.socket.on("event:admin.verifiedSong.created", res =>
  377. this.addSong(res.data.song)
  378. );
  379. this.socket.on("event:admin.verifiedSong.deleted", res =>
  380. this.removeSong(res.data.songId)
  381. );
  382. this.socket.on("event:admin.verifiedSong.updated", res =>
  383. this.updateSong(res.data.song)
  384. );
  385. if (this.socket.readyState === 1) this.init();
  386. ws.onConnect(() => this.init());
  387. if (this.$route.query.songId) {
  388. this.socket.dispatch(
  389. "songs.getSongFromSongId",
  390. this.$route.query.songId,
  391. res => {
  392. if (res.status === "success") this.edit(res.data.song);
  393. else new Toast("Song with that ID not found");
  394. }
  395. );
  396. }
  397. keyboardShortcuts.registerShortcut(
  398. "verifiedSongs.toggleKeyboardShortcutsHelper",
  399. {
  400. keyCode: 191, // '/' key
  401. ctrl: true,
  402. preventDefault: true,
  403. handler: () => {
  404. this.toggleKeyboardShortcutsHelper();
  405. }
  406. }
  407. );
  408. keyboardShortcuts.registerShortcut(
  409. "verifiedSongs.resetKeyboardShortcutsHelper",
  410. {
  411. keyCode: 191, // '/' key
  412. ctrl: true,
  413. shift: true,
  414. preventDefault: true,
  415. handler: () => {
  416. this.resetKeyboardShortcutsHelper();
  417. }
  418. }
  419. );
  420. },
  421. beforeUnmount() {
  422. const shortcutNames = [
  423. "verifiedSongs.toggleKeyboardShortcutsHelper",
  424. "verifiedSongs.resetKeyboardShortcutsHelper"
  425. ];
  426. shortcutNames.forEach(shortcutName => {
  427. keyboardShortcuts.unregisterShortcut(shortcutName);
  428. });
  429. },
  430. methods: {
  431. edit(song) {
  432. this.editSong(song);
  433. this.openModal("editSong");
  434. },
  435. unverify(id) {
  436. this.socket.dispatch("songs.unverify", id, res => {
  437. new Toast(res.message);
  438. });
  439. },
  440. updateAllSongs() {
  441. new Toast("Updating all songs, this could take a very long time.");
  442. this.socket.dispatch("songs.updateAll", res => {
  443. if (res.status === "success") new Toast(res.message);
  444. else new Toast(res.message);
  445. });
  446. },
  447. getSet() {
  448. if (this.isGettingSet) return;
  449. if (this.position >= this.maxPosition) return;
  450. this.isGettingSet = true;
  451. this.socket.dispatch(
  452. "songs.getSet",
  453. this.position,
  454. "verified",
  455. res => {
  456. if (res.status === "success") {
  457. res.data.songs.forEach(song => {
  458. this.addSong(song);
  459. });
  460. this.position += 1;
  461. this.isGettingSet = false;
  462. }
  463. }
  464. );
  465. },
  466. toggleArtistSelected(artist) {
  467. if (this.artistFilterSelected.indexOf(artist) === -1)
  468. this.artistFilterSelected.push(artist);
  469. else
  470. this.artistFilterSelected.splice(
  471. this.artistFilterSelected.indexOf(artist),
  472. 1
  473. );
  474. },
  475. toggleGenreSelected(genre) {
  476. if (this.genreFilterSelected.indexOf(genre) === -1)
  477. this.genreFilterSelected.push(genre);
  478. else
  479. this.genreFilterSelected.splice(
  480. this.genreFilterSelected.indexOf(genre),
  481. 1
  482. );
  483. },
  484. toggleKeyboardShortcutsHelper() {
  485. this.$refs.keyboardShortcutsHelper.toggleBox();
  486. },
  487. resetKeyboardShortcutsHelper() {
  488. this.$refs.keyboardShortcutsHelper.resetBox();
  489. },
  490. init() {
  491. this.position = 1;
  492. this.maxPosition = 1;
  493. this.resetSongs();
  494. if (this.songs.length > 0)
  495. this.position = Math.ceil(this.songs.length / 15) + 1;
  496. this.socket.dispatch("songs.length", "verified", res => {
  497. if (res.status === "success") {
  498. this.maxPosition = Math.ceil(res.data.length / 15) + 1;
  499. this.getSet();
  500. }
  501. });
  502. this.socket.dispatch("apis.joinAdminRoom", "songs", () => {});
  503. },
  504. ...mapActions("admin/verifiedSongs", [
  505. // "stopVideo",
  506. "resetSongs",
  507. "addSong",
  508. "removeSong",
  509. "updateSong"
  510. ]),
  511. ...mapActions("modals/editSong", ["editSong"]),
  512. ...mapActions("modalVisibility", ["openModal", "closeModal"])
  513. }
  514. };
  515. </script>
  516. <style lang="scss" scoped>
  517. .night-mode {
  518. .table {
  519. color: var(--light-grey-2);
  520. background-color: var(--dark-grey-3);
  521. thead tr {
  522. background: var(--dark-grey-3);
  523. td {
  524. color: var(--white);
  525. }
  526. }
  527. tbody tr:hover {
  528. background-color: var(--dark-grey-4) !important;
  529. }
  530. tbody tr:nth-child(even) {
  531. background-color: var(--dark-grey-2);
  532. }
  533. strong {
  534. color: var(--light-grey-2);
  535. }
  536. }
  537. }
  538. body {
  539. font-family: "Hind", sans-serif;
  540. }
  541. .optionsColumn {
  542. width: 100px;
  543. div {
  544. button {
  545. width: 35px;
  546. &:not(:last-child) {
  547. margin-right: 5px;
  548. }
  549. }
  550. }
  551. }
  552. .likesColumn,
  553. .dislikesColumn {
  554. width: 40px;
  555. i {
  556. font-size: 20px;
  557. }
  558. .thumbLike {
  559. color: var(--green) !important;
  560. }
  561. .thumbDislike {
  562. color: var(--red) !important;
  563. }
  564. }
  565. .song-thumbnail {
  566. display: block;
  567. max-width: 50px;
  568. margin: 0 auto;
  569. }
  570. td {
  571. vertical-align: middle;
  572. & > div {
  573. display: inline-flex;
  574. }
  575. }
  576. #keyboardShortcutsHelper {
  577. .box-body {
  578. b {
  579. color: var(--black);
  580. }
  581. .biggest {
  582. font-size: 18px;
  583. }
  584. .bigger {
  585. font-size: 16px;
  586. }
  587. span {
  588. display: block;
  589. }
  590. }
  591. }
  592. .is-primary:focus {
  593. background-color: var(--primary-color) !important;
  594. }
  595. </style>