Songs.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. <template>
  2. <div>
  3. <page-metadata title="Admin | Songs" />
  4. <div class="admin-tab">
  5. <div class="button-row">
  6. <button
  7. class="button is-primary"
  8. @click="toggleKeyboardShortcutsHelper"
  9. @dblclick="resetKeyboardShortcutsHelper"
  10. >
  11. Keyboard shortcuts helper
  12. </button>
  13. <button
  14. class="button is-primary"
  15. @click="openModal('requestSong')"
  16. >
  17. Request song
  18. </button>
  19. <button
  20. class="button is-primary"
  21. @click="openModal('importAlbum')"
  22. >
  23. Import album
  24. </button>
  25. <run-job-dropdown :jobs="jobs" />
  26. </div>
  27. <advanced-table
  28. :column-default="columnDefault"
  29. :columns="columns"
  30. :filters="filters"
  31. data-action="songs.getData"
  32. name="admin-songs"
  33. :events="events"
  34. >
  35. <template #column-options="slotProps">
  36. <div class="row-options">
  37. <button
  38. class="
  39. button
  40. is-primary
  41. icon-with-button
  42. material-icons
  43. "
  44. @click="editOne(slotProps.item)"
  45. :disabled="slotProps.item.removed"
  46. content="Edit Song"
  47. v-tippy
  48. >
  49. edit
  50. </button>
  51. <quick-confirm
  52. v-if="slotProps.item.status === 'verified'"
  53. @confirm="unverifyOne(slotProps.item._id)"
  54. >
  55. <button
  56. class="
  57. button
  58. is-danger
  59. icon-with-button
  60. material-icons
  61. "
  62. :disabled="slotProps.item.removed"
  63. content="Unverify Song"
  64. v-tippy
  65. >
  66. cancel
  67. </button>
  68. </quick-confirm>
  69. <button
  70. v-else
  71. class="
  72. button
  73. is-success
  74. icon-with-button
  75. material-icons
  76. "
  77. @click="verifyOne(slotProps.item._id)"
  78. :disabled="slotProps.item.removed"
  79. content="Verify Song"
  80. v-tippy
  81. >
  82. check_circle
  83. </button>
  84. <button
  85. class="
  86. button
  87. is-danger
  88. icon-with-button
  89. material-icons
  90. "
  91. @click.prevent="
  92. confirmAction({
  93. message:
  94. 'Removing this song will remove it from all playlists and cause a ratings recalculation.',
  95. action: 'deleteOne',
  96. params: slotProps.item._id
  97. })
  98. "
  99. content="Delete Song"
  100. v-tippy
  101. >
  102. delete_forever
  103. </button>
  104. </div>
  105. </template>
  106. <template #column-thumbnailImage="slotProps">
  107. <img
  108. class="song-thumbnail"
  109. :src="slotProps.item.thumbnail"
  110. onerror="this.src='/assets/notes-transparent.png'"
  111. loading="lazy"
  112. />
  113. </template>
  114. <template #column-thumbnailUrl="slotProps">
  115. <a :href="slotProps.item.thumbnail" target="_blank">
  116. {{ slotProps.item.thumbnail }}
  117. </a>
  118. </template>
  119. <template #column-title="slotProps">
  120. <span :title="slotProps.item.title">{{
  121. slotProps.item.title
  122. }}</span>
  123. </template>
  124. <template #column-artists="slotProps">
  125. <span :title="slotProps.item.artists.join(', ')">{{
  126. slotProps.item.artists.join(", ")
  127. }}</span>
  128. </template>
  129. <template #column-genres="slotProps">
  130. <span :title="slotProps.item.genres.join(', ')">{{
  131. slotProps.item.genres.join(", ")
  132. }}</span>
  133. </template>
  134. <template #column-likes="slotProps">
  135. <span :title="slotProps.item.likes">{{
  136. slotProps.item.likes
  137. }}</span>
  138. </template>
  139. <template #column-dislikes="slotProps">
  140. <span :title="slotProps.item.dislikes">{{
  141. slotProps.item.dislikes
  142. }}</span>
  143. </template>
  144. <template #column-_id="slotProps">
  145. <span :title="slotProps.item._id">{{
  146. slotProps.item._id
  147. }}</span>
  148. </template>
  149. <template #column-youtubeId="slotProps">
  150. <a
  151. :href="
  152. 'https://www.youtube.com/watch?v=' +
  153. `${slotProps.item.youtubeId}`
  154. "
  155. target="_blank"
  156. >
  157. {{ slotProps.item.youtubeId }}
  158. </a>
  159. </template>
  160. <template #column-status="slotProps">
  161. <span :title="slotProps.item.status">{{
  162. slotProps.item.status
  163. }}</span>
  164. </template>
  165. <template #column-duration="slotProps">
  166. <span :title="slotProps.item.duration">{{
  167. slotProps.item.duration
  168. }}</span>
  169. </template>
  170. <template #column-skipDuration="slotProps">
  171. <span :title="slotProps.item.skipDuration">{{
  172. slotProps.item.skipDuration
  173. }}</span>
  174. </template>
  175. <template #column-requestedBy="slotProps">
  176. <user-id-to-username
  177. :user-id="slotProps.item.requestedBy"
  178. :link="true"
  179. />
  180. </template>
  181. <template #column-requestedAt="slotProps">
  182. <span :title="new Date(slotProps.item.requestedAt)">{{
  183. getDateFormatted(slotProps.item.requestedAt)
  184. }}</span>
  185. </template>
  186. <template #column-verifiedBy="slotProps">
  187. <user-id-to-username
  188. :user-id="slotProps.item.verifiedBy"
  189. :link="true"
  190. />
  191. </template>
  192. <template #column-verifiedAt="slotProps">
  193. <span :title="new Date(slotProps.item.verifiedAt)">{{
  194. getDateFormatted(slotProps.item.verifiedAt)
  195. }}</span>
  196. </template>
  197. <template #bulk-actions="slotProps">
  198. <div class="bulk-actions">
  199. <i
  200. class="material-icons edit-songs-icon"
  201. @click.prevent="editMany(slotProps.item)"
  202. content="Edit Songs"
  203. v-tippy
  204. >
  205. edit
  206. </i>
  207. <i
  208. class="material-icons verify-songs-icon"
  209. @click.prevent="verifyMany(slotProps.item)"
  210. content="Verify Songs"
  211. v-tippy
  212. >
  213. check_circle
  214. </i>
  215. <quick-confirm
  216. placement="left"
  217. @confirm="unverifyMany(slotProps.item)"
  218. >
  219. <i
  220. class="material-icons unverify-songs-icon"
  221. content="Unverify Songs"
  222. v-tippy
  223. >
  224. cancel
  225. </i>
  226. </quick-confirm>
  227. <i
  228. class="material-icons tag-songs-icon"
  229. @click.prevent="tagMany(slotProps.item)"
  230. content="Tag Songs"
  231. v-tippy
  232. >
  233. local_offer
  234. </i>
  235. <i
  236. class="material-icons artists-songs-icon"
  237. @click.prevent="setArtists(slotProps.item)"
  238. content="Set Artists"
  239. v-tippy
  240. >
  241. group
  242. </i>
  243. <i
  244. class="material-icons genres-songs-icon"
  245. @click.prevent="setGenres(slotProps.item)"
  246. content="Set Genres"
  247. v-tippy
  248. >
  249. theater_comedy
  250. </i>
  251. <i
  252. class="material-icons delete-icon"
  253. @click.prevent="
  254. confirmAction({
  255. message:
  256. 'Removing these songs will remove them from all playlists and cause a ratings recalculation.',
  257. action: 'deleteMany',
  258. params: slotProps.item
  259. })
  260. "
  261. content="Delete Songs"
  262. v-tippy
  263. >
  264. delete_forever
  265. </i>
  266. </div>
  267. </template>
  268. </advanced-table>
  269. </div>
  270. <import-album v-if="modals.importAlbum" />
  271. <edit-song v-if="modals.editSong" song-type="songs" :key="song._id" />
  272. <report v-if="modals.report" />
  273. <request-song v-if="modals.requestSong" />
  274. <confirm
  275. v-if="modals.confirm"
  276. :confirm="confirm"
  277. @confirmed="handleConfirmed()"
  278. />
  279. <floating-box
  280. id="keyboardShortcutsHelper"
  281. ref="keyboardShortcutsHelper"
  282. >
  283. <template #body>
  284. <div>
  285. <div>
  286. <span class="biggest"
  287. ><b>Keyboard shortcuts helper</b></span
  288. >
  289. <span
  290. ><b>Ctrl + /</b> - Toggles this keyboard shortcuts
  291. helper</span
  292. >
  293. <span
  294. ><b>Ctrl + Shift + /</b> - Resets the position of
  295. this keyboard shortcuts helper</span
  296. >
  297. <hr />
  298. </div>
  299. <div>
  300. <span class="biggest"><b>Edit song modal</b></span>
  301. <span class="bigger"><b>Navigation</b></span>
  302. <span><b>Home</b> - Edit</span>
  303. <span><b>End</b> - Edit</span>
  304. <hr />
  305. </div>
  306. <div>
  307. <span class="bigger"><b>Player controls</b></span>
  308. <span class="bigger"
  309. ><i>Don't forget to turn off numlock!</i></span
  310. >
  311. <span><b>Numpad up/down</b> - Volume up/down 10%</span>
  312. <span
  313. ><b>Ctrl + Numpad up/down</b> - Volume up/down
  314. 1%</span
  315. >
  316. <span><b>Numpad center</b> - Pause/resume</span>
  317. <span><b>Ctrl + Numpad center</b> - Stop</span>
  318. <span
  319. ><b>Numpad Right</b> - Skip to last 10 seconds</span
  320. >
  321. <hr />
  322. </div>
  323. <div>
  324. <span class="bigger"><b>Form control</b></span>
  325. <span
  326. ><b>Enter</b> - Executes blue button in that
  327. input</span
  328. >
  329. <span
  330. ><b>Shift + Enter</b> - Executes purple/red button
  331. in that input</span
  332. >
  333. <span
  334. ><b>Ctrl + Alt + D</b> - Fill in all Discogs
  335. fields</span
  336. >
  337. <hr />
  338. </div>
  339. <div>
  340. <span class="bigger"><b>Modal control</b></span>
  341. <span><b>Ctrl + S</b> - Save</span>
  342. <span><b>Ctrl + Alt + S</b> - Save and close</span>
  343. <span
  344. ><b>Ctrl + Alt + V</b> - Save, verify and
  345. close</span
  346. >
  347. <span><b>F4</b> - Close without saving</span>
  348. <hr />
  349. </div>
  350. </div>
  351. </template>
  352. </floating-box>
  353. </div>
  354. </template>
  355. <script>
  356. import { mapState, mapActions, mapGetters } from "vuex";
  357. import { defineAsyncComponent } from "vue";
  358. import Toast from "toasters";
  359. import keyboardShortcuts from "@/keyboardShortcuts";
  360. import AdvancedTable from "@/components/AdvancedTable.vue";
  361. import UserIdToUsername from "@/components/UserIdToUsername.vue";
  362. import FloatingBox from "@/components/FloatingBox.vue";
  363. import QuickConfirm from "@/components/QuickConfirm.vue";
  364. import RunJobDropdown from "@/components/RunJobDropdown.vue";
  365. export default {
  366. components: {
  367. EditSong: defineAsyncComponent(() =>
  368. import("@/components/modals/EditSong")
  369. ),
  370. Report: defineAsyncComponent(() =>
  371. import("@/components/modals/Report.vue")
  372. ),
  373. ImportAlbum: defineAsyncComponent(() =>
  374. import("@/components/modals/ImportAlbum.vue")
  375. ),
  376. RequestSong: defineAsyncComponent(() =>
  377. import("@/components/modals/RequestSong.vue")
  378. ),
  379. Confirm: defineAsyncComponent(() =>
  380. import("@/components/modals/Confirm.vue")
  381. ),
  382. AdvancedTable,
  383. UserIdToUsername,
  384. FloatingBox,
  385. QuickConfirm,
  386. RunJobDropdown
  387. },
  388. data() {
  389. return {
  390. columnDefault: {
  391. sortable: true,
  392. hidable: true,
  393. defaultVisibility: "shown",
  394. draggable: true,
  395. resizable: true,
  396. minWidth: 200,
  397. maxWidth: 600
  398. },
  399. columns: [
  400. {
  401. name: "options",
  402. displayName: "Options",
  403. properties: ["_id", "status", "removed"],
  404. sortable: false,
  405. hidable: false,
  406. resizable: false,
  407. minWidth: 129,
  408. defaultWidth: 129
  409. },
  410. {
  411. name: "thumbnailImage",
  412. displayName: "Thumb",
  413. properties: ["thumbnail"],
  414. sortable: false,
  415. minWidth: 75,
  416. defaultWidth: 75,
  417. maxWidth: 75,
  418. resizable: false
  419. },
  420. {
  421. name: "title",
  422. displayName: "Title",
  423. properties: ["title"],
  424. sortProperty: "title"
  425. },
  426. {
  427. name: "artists",
  428. displayName: "Artists",
  429. properties: ["artists"],
  430. sortable: false
  431. },
  432. {
  433. name: "genres",
  434. displayName: "Genres",
  435. properties: ["genres"],
  436. sortable: false
  437. },
  438. {
  439. name: "likes",
  440. displayName: "Likes",
  441. properties: ["likes"],
  442. sortProperty: "likes",
  443. minWidth: 100,
  444. defaultWidth: 100,
  445. defaultVisibility: "hidden"
  446. },
  447. {
  448. name: "dislikes",
  449. displayName: "Dislikes",
  450. properties: ["dislikes"],
  451. sortProperty: "dislikes",
  452. minWidth: 100,
  453. defaultWidth: 100,
  454. defaultVisibility: "hidden"
  455. },
  456. {
  457. name: "_id",
  458. displayName: "Song ID",
  459. properties: ["_id"],
  460. sortProperty: "_id",
  461. minWidth: 215,
  462. defaultWidth: 215
  463. },
  464. {
  465. name: "youtubeId",
  466. displayName: "YouTube ID",
  467. properties: ["youtubeId"],
  468. sortProperty: "youtubeId",
  469. minWidth: 120,
  470. defaultWidth: 120
  471. },
  472. {
  473. name: "status",
  474. displayName: "Status",
  475. properties: ["status"],
  476. sortProperty: "status",
  477. defaultVisibility: "hidden",
  478. minWidth: 120,
  479. defaultWidth: 120
  480. },
  481. {
  482. name: "thumbnailUrl",
  483. displayName: "Thumbnail (URL)",
  484. properties: ["thumbnail"],
  485. sortProperty: "thumbnail",
  486. defaultVisibility: "hidden"
  487. },
  488. {
  489. name: "duration",
  490. displayName: "Duration",
  491. properties: ["duration"],
  492. sortProperty: "duration",
  493. defaultWidth: 200,
  494. defaultVisibility: "hidden"
  495. },
  496. {
  497. name: "skipDuration",
  498. displayName: "Skip Duration",
  499. properties: ["skipDuration"],
  500. sortProperty: "skipDuration",
  501. defaultWidth: 200,
  502. defaultVisibility: "hidden"
  503. },
  504. {
  505. name: "requestedBy",
  506. displayName: "Requested By",
  507. properties: ["requestedBy"],
  508. sortProperty: "requestedBy",
  509. defaultWidth: 200,
  510. defaultVisibility: "hidden"
  511. },
  512. {
  513. name: "requestedAt",
  514. displayName: "Requested At",
  515. properties: ["requestedAt"],
  516. sortProperty: "requestedAt",
  517. defaultWidth: 200,
  518. defaultVisibility: "hidden"
  519. },
  520. {
  521. name: "verifiedBy",
  522. displayName: "Verified By",
  523. properties: ["verifiedBy"],
  524. sortProperty: "verifiedBy",
  525. defaultWidth: 200,
  526. defaultVisibility: "hidden"
  527. },
  528. {
  529. name: "verifiedAt",
  530. displayName: "Verified At",
  531. properties: ["verifiedAt"],
  532. sortProperty: "verifiedAt",
  533. defaultWidth: 200,
  534. defaultVisibility: "hidden"
  535. }
  536. ],
  537. filters: [
  538. {
  539. name: "_id",
  540. displayName: "Song ID",
  541. property: "_id",
  542. filterTypes: ["exact"],
  543. defaultFilterType: "exact"
  544. },
  545. {
  546. name: "youtubeId",
  547. displayName: "YouTube ID",
  548. property: "youtubeId",
  549. filterTypes: ["contains", "exact", "regex"],
  550. defaultFilterType: "contains"
  551. },
  552. {
  553. name: "title",
  554. displayName: "Title",
  555. property: "title",
  556. filterTypes: ["contains", "exact", "regex"],
  557. defaultFilterType: "contains"
  558. },
  559. {
  560. name: "artists",
  561. displayName: "Artists",
  562. property: "artists",
  563. filterTypes: ["contains", "exact", "regex"],
  564. defaultFilterType: "contains",
  565. autosuggest: true,
  566. autosuggestDataAction: "songs.getArtists"
  567. },
  568. {
  569. name: "genres",
  570. displayName: "Genres",
  571. property: "genres",
  572. filterTypes: ["contains", "exact", "regex"],
  573. defaultFilterType: "contains",
  574. autosuggest: true,
  575. autosuggestDataAction: "songs.getGenres"
  576. },
  577. {
  578. name: "thumbnail",
  579. displayName: "Thumbnail",
  580. property: "thumbnail",
  581. filterTypes: ["contains", "exact", "regex"],
  582. defaultFilterType: "contains"
  583. },
  584. {
  585. name: "requestedBy",
  586. displayName: "Requested By",
  587. property: "requestedBy",
  588. filterTypes: ["contains", "exact", "regex"],
  589. defaultFilterType: "contains"
  590. },
  591. {
  592. name: "requestedAt",
  593. displayName: "Requested At",
  594. property: "requestedAt",
  595. filterTypes: ["datetimeBefore", "datetimeAfter"],
  596. defaultFilterType: "datetimeBefore"
  597. },
  598. {
  599. name: "verifiedBy",
  600. displayName: "Verified By",
  601. property: "verifiedBy",
  602. filterTypes: ["contains", "exact", "regex"],
  603. defaultFilterType: "contains"
  604. },
  605. {
  606. name: "verifiedAt",
  607. displayName: "Verified At",
  608. property: "verifiedAt",
  609. filterTypes: ["datetimeBefore", "datetimeAfter"],
  610. defaultFilterType: "datetimeBefore"
  611. },
  612. {
  613. name: "status",
  614. displayName: "Status",
  615. property: "status",
  616. filterTypes: ["exact", "regex"],
  617. defaultFilterType: "exact",
  618. filterValues: ["verified", "unverified"]
  619. },
  620. {
  621. name: "likes",
  622. displayName: "Likes",
  623. property: "likes",
  624. filterTypes: [
  625. "numberLesserEqual",
  626. "numberLesser",
  627. "numberGreater",
  628. "numberGreaterEqual",
  629. "numberEquals",
  630. "exact",
  631. "regex"
  632. ],
  633. defaultFilterType: "numberLesser"
  634. },
  635. {
  636. name: "dislikes",
  637. displayName: "Dislikes",
  638. property: "dislikes",
  639. filterTypes: [
  640. "numberLesserEqual",
  641. "numberLesser",
  642. "numberGreater",
  643. "numberGreaterEqual",
  644. "numberEquals",
  645. "exact",
  646. "regex"
  647. ],
  648. defaultFilterType: "numberLesser"
  649. },
  650. {
  651. name: "duration",
  652. displayName: "Duration",
  653. property: "duration",
  654. filterTypes: [
  655. "numberLesserEqual",
  656. "numberLesser",
  657. "numberGreater",
  658. "numberGreaterEqual",
  659. "numberEquals",
  660. "exact",
  661. "regex"
  662. ],
  663. defaultFilterType: "numberLesser"
  664. },
  665. {
  666. name: "skipDuration",
  667. displayName: "Skip Duration",
  668. property: "skipDuration",
  669. filterTypes: [
  670. "numberLesserEqual",
  671. "numberLesser",
  672. "numberGreater",
  673. "numberGreaterEqual",
  674. "numberEquals",
  675. "exact",
  676. "regex"
  677. ],
  678. defaultFilterType: "numberLesser"
  679. }
  680. ],
  681. events: {
  682. adminRoom: "songs",
  683. updated: {
  684. event: "admin.song.updated",
  685. id: "song._id",
  686. item: "song"
  687. },
  688. removed: {
  689. event: "admin.song.removed",
  690. id: "songId"
  691. }
  692. },
  693. jobs: [
  694. {
  695. name: "Update all songs",
  696. socket: "songs.updateAll"
  697. },
  698. {
  699. name: "Recalculate all song ratings",
  700. socket: "songs.recalculateAllRatings"
  701. }
  702. ],
  703. confirm: {
  704. message: "",
  705. action: "",
  706. params: null
  707. }
  708. };
  709. },
  710. computed: {
  711. ...mapState("modalVisibility", {
  712. modals: state => state.modals
  713. }),
  714. ...mapState("modals/editSong", {
  715. song: state => state.song
  716. }),
  717. ...mapGetters({
  718. socket: "websockets/getSocket"
  719. })
  720. },
  721. mounted() {
  722. if (this.$route.query.songId) {
  723. this.socket.dispatch(
  724. "songs.getSongFromSongId",
  725. this.$route.query.songId,
  726. res => {
  727. if (res.status === "success")
  728. this.editMany([res.data.song]);
  729. else new Toast("Song with that ID not found");
  730. }
  731. );
  732. }
  733. keyboardShortcuts.registerShortcut(
  734. "songs.toggleKeyboardShortcutsHelper",
  735. {
  736. keyCode: 191, // '/' key
  737. ctrl: true,
  738. preventDefault: true,
  739. handler: () => {
  740. this.toggleKeyboardShortcutsHelper();
  741. }
  742. }
  743. );
  744. keyboardShortcuts.registerShortcut(
  745. "songs.resetKeyboardShortcutsHelper",
  746. {
  747. keyCode: 191, // '/' key
  748. ctrl: true,
  749. shift: true,
  750. preventDefault: true,
  751. handler: () => {
  752. this.resetKeyboardShortcutsHelper();
  753. }
  754. }
  755. );
  756. },
  757. beforeUnmount() {
  758. const shortcutNames = [
  759. "songs.toggleKeyboardShortcutsHelper",
  760. "songs.resetKeyboardShortcutsHelper"
  761. ];
  762. shortcutNames.forEach(shortcutName => {
  763. keyboardShortcuts.unregisterShortcut(shortcutName);
  764. });
  765. },
  766. methods: {
  767. editOne(song) {
  768. this.editSong(song);
  769. this.openModal("editSong");
  770. },
  771. editMany(selectedRows) {
  772. if (selectedRows.length === 1) {
  773. this.editSong(selectedRows[0]);
  774. this.openModal("editSong");
  775. } else {
  776. new Toast("Bulk editing not yet implemented.");
  777. }
  778. },
  779. verifyOne(songId) {
  780. this.socket.dispatch("songs.verify", songId, res => {
  781. new Toast(res.message);
  782. });
  783. },
  784. verifyMany(selectedRows) {
  785. this.socket.dispatch(
  786. "songs.verifyMany",
  787. selectedRows.map(row => row._id),
  788. res => {
  789. new Toast(res.message);
  790. }
  791. );
  792. },
  793. unverifyOne(songId) {
  794. this.socket.dispatch("songs.unverify", songId, res => {
  795. new Toast(res.message);
  796. });
  797. },
  798. unverifyMany(selectedRows) {
  799. this.socket.dispatch(
  800. "songs.unverifyMany",
  801. selectedRows.map(row => row._id),
  802. res => {
  803. new Toast(res.message);
  804. }
  805. );
  806. },
  807. tagMany() {
  808. new Toast("Bulk tagging not yet implemented.");
  809. },
  810. setArtists() {
  811. new Toast("Bulk setting artists not yet implemented.");
  812. },
  813. setGenres() {
  814. new Toast("Bulk setting genres not yet implemented.");
  815. },
  816. deleteOne(songId) {
  817. this.socket.dispatch("songs.remove", songId, res => {
  818. new Toast(res.message);
  819. });
  820. },
  821. deleteMany(selectedRows) {
  822. this.socket.dispatch(
  823. "songs.removeMany",
  824. selectedRows.map(row => row._id),
  825. res => {
  826. new Toast(res.message);
  827. }
  828. );
  829. },
  830. toggleKeyboardShortcutsHelper() {
  831. this.$refs.keyboardShortcutsHelper.toggleBox();
  832. },
  833. resetKeyboardShortcutsHelper() {
  834. this.$refs.keyboardShortcutsHelper.resetBox();
  835. },
  836. getDateFormatted(createdAt) {
  837. const date = new Date(createdAt);
  838. const year = date.getFullYear();
  839. const month = `${date.getMonth() + 1}`.padStart(2, 0);
  840. const day = `${date.getDate()}`.padStart(2, 0);
  841. const hour = `${date.getHours()}`.padStart(2, 0);
  842. const minute = `${date.getMinutes()}`.padStart(2, 0);
  843. return `${year}-${month}-${day} ${hour}:${minute}`;
  844. },
  845. confirmAction(confirm) {
  846. this.confirm = confirm;
  847. this.updateConfirmMessage(confirm.message);
  848. this.openModal("confirm");
  849. },
  850. handleConfirmed() {
  851. const { action, params } = this.confirm;
  852. if (typeof this[action] === "function") {
  853. if (params) this[action](params);
  854. else this[action]();
  855. }
  856. this.confirm = {
  857. message: "",
  858. action: "",
  859. params: null
  860. };
  861. },
  862. ...mapActions("modals/editSong", ["editSong"]),
  863. ...mapActions("modals/confirm", ["updateConfirmMessage"]),
  864. ...mapActions("modalVisibility", ["openModal"])
  865. }
  866. };
  867. </script>
  868. <style lang="scss" scoped>
  869. #keyboardShortcutsHelper {
  870. .box-body {
  871. .biggest {
  872. font-size: 18px;
  873. }
  874. .bigger {
  875. font-size: 16px;
  876. }
  877. span {
  878. display: block;
  879. }
  880. }
  881. }
  882. .song-thumbnail {
  883. display: block;
  884. max-width: 50px;
  885. margin: 0 auto;
  886. }
  887. .bulk-popup .bulk-actions {
  888. .verify-songs-icon {
  889. color: var(--green);
  890. }
  891. .unverify-songs-icon {
  892. color: var(--dark-red);
  893. }
  894. }
  895. </style>