Songs.vue 19 KB

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