Songs.vue 19 KB

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