index.vue 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059
  1. <template>
  2. <div>
  3. <modal
  4. title="Edit Song"
  5. class="song-modal"
  6. :size="'wide'"
  7. :split="true"
  8. >
  9. <template #sidebar>
  10. <slot name="sidebar" />
  11. </template>
  12. <template #body>
  13. <div v-if="!songId" class="notice-container">
  14. <h4>No song has been selected</h4>
  15. </div>
  16. <div v-if="songId && !songDataLoaded" class="notice-container">
  17. <h4>Song hasn't loaded yet</h4>
  18. </div>
  19. <div class="left-section" v-show="songDataLoaded">
  20. <div class="top-section">
  21. <div class="player-section">
  22. <div id="editSongPlayer" />
  23. <div v-show="youtubeError" class="player-error">
  24. <h2>{{ youtubeErrorMessage }}</h2>
  25. </div>
  26. <canvas
  27. ref="durationCanvas"
  28. id="durationCanvas"
  29. v-show="!youtubeError"
  30. height="20"
  31. width="530"
  32. />
  33. <div class="player-footer">
  34. <div class="player-footer-left">
  35. <button
  36. class="button is-primary"
  37. @click="play()"
  38. @keyup.enter="play()"
  39. v-if="video.paused"
  40. content="Unpause Playback"
  41. v-tippy
  42. >
  43. <i class="material-icons">play_arrow</i>
  44. </button>
  45. <button
  46. class="button is-primary"
  47. @click="settings('pause')"
  48. @keyup.enter="settings('pause')"
  49. v-else
  50. content="Pause Playback"
  51. v-tippy
  52. >
  53. <i class="material-icons">pause</i>
  54. </button>
  55. <button
  56. class="button is-danger"
  57. @click="settings('stop')"
  58. @keyup.enter="settings('stop')"
  59. content="Stop Playback"
  60. v-tippy
  61. >
  62. <i class="material-icons">stop</i>
  63. </button>
  64. <button
  65. class="button is-success"
  66. @click="settings('skipToLast10Secs')"
  67. @keyup.enter="
  68. settings('skipToLast10Secs')
  69. "
  70. content="Skip to last 10 secs"
  71. v-tippy
  72. >
  73. <i class="material-icons"
  74. >fast_forward</i
  75. >
  76. </button>
  77. </div>
  78. <div class="player-footer-center">
  79. <span>
  80. <span>
  81. {{ youtubeVideoCurrentTime }}
  82. </span>
  83. /
  84. <span>
  85. {{ youtubeVideoDuration }}
  86. {{ youtubeVideoNote }}
  87. </span>
  88. </span>
  89. </div>
  90. <div class="player-footer-right">
  91. <p id="volume-control">
  92. <i
  93. v-if="muted"
  94. class="material-icons"
  95. @click="toggleMute()"
  96. content="Unmute"
  97. v-tippy
  98. >volume_mute</i
  99. >
  100. <i
  101. v-else
  102. class="material-icons"
  103. @click="toggleMute()"
  104. content="Mute"
  105. v-tippy
  106. >volume_down</i
  107. >
  108. <input
  109. v-model="volumeSliderValue"
  110. type="range"
  111. min="0"
  112. max="10000"
  113. class="volume-slider active"
  114. @change="changeVolume()"
  115. @input="changeVolume()"
  116. />
  117. <i
  118. class="material-icons"
  119. @click="increaseVolume()"
  120. content="Increase Volume"
  121. v-tippy
  122. >volume_up</i
  123. >
  124. </p>
  125. </div>
  126. </div>
  127. </div>
  128. <img
  129. class="thumbnail-preview"
  130. :src="song.thumbnail"
  131. onerror="this.src='/assets/notes-transparent.png'"
  132. ref="thumbnailElement"
  133. v-if="songDataLoaded"
  134. />
  135. </div>
  136. <div class="edit-section" v-if="songDataLoaded">
  137. <div class="control is-grouped">
  138. <div class="title-container">
  139. <label class="label">Title</label>
  140. <p class="control has-addons">
  141. <input
  142. class="input"
  143. type="text"
  144. ref="title-input"
  145. v-model="song.title"
  146. placeholder="Enter song title..."
  147. @keyup.shift.enter="
  148. getAlbumData('title')
  149. "
  150. />
  151. <button
  152. class="button album-get-button"
  153. @click="getAlbumData('title')"
  154. >
  155. <i
  156. class="material-icons"
  157. v-tippy
  158. content="Fill from Discogs"
  159. >album</i
  160. >
  161. </button>
  162. </p>
  163. </div>
  164. <div class="duration-container">
  165. <label class="label">Duration</label>
  166. <p class="control has-addons">
  167. <input
  168. class="input"
  169. type="text"
  170. placeholder="Enter song duration..."
  171. v-model.number="song.duration"
  172. @keyup.shift.enter="fillDuration()"
  173. />
  174. <button
  175. class="button duration-fill-button"
  176. @click="fillDuration()"
  177. >
  178. <i
  179. class="material-icons"
  180. v-tippy
  181. content="Sync duration with YouTube"
  182. >sync</i
  183. >
  184. </button>
  185. </p>
  186. </div>
  187. <div class="skip-duration-container">
  188. <label class="label">Skip duration</label>
  189. <p class="control">
  190. <input
  191. class="input"
  192. type="text"
  193. placeholder="Enter skip duration..."
  194. v-model.number="song.skipDuration"
  195. />
  196. </p>
  197. </div>
  198. </div>
  199. <div class="control is-grouped">
  200. <div class="album-art-container">
  201. <label class="label">Album art</label>
  202. <p class="control has-addons">
  203. <input
  204. class="input"
  205. type="text"
  206. v-model="song.thumbnail"
  207. placeholder="Enter link to album art..."
  208. @keyup.shift.enter="
  209. getAlbumData('albumArt')
  210. "
  211. />
  212. <button
  213. class="button album-get-button"
  214. @click="getAlbumData('albumArt')"
  215. >
  216. <i
  217. class="material-icons"
  218. v-tippy
  219. content="Fill from Discogs"
  220. >album</i
  221. >
  222. </button>
  223. </p>
  224. </div>
  225. <div class="youtube-id-container">
  226. <label class="label">YouTube ID</label>
  227. <p class="control">
  228. <input
  229. class="input"
  230. type="text"
  231. placeholder="Enter YouTube ID..."
  232. v-model="song.youtubeId"
  233. />
  234. </p>
  235. </div>
  236. </div>
  237. <div class="control is-grouped">
  238. <div class="artists-container">
  239. <label class="label">Artists</label>
  240. <p class="control has-addons">
  241. <auto-suggest
  242. v-model="artistInputValue"
  243. ref="new-artist"
  244. placeholder="Add artist..."
  245. :all-items="
  246. autosuggest.allItems.artists
  247. "
  248. @submitted="addTag('artists')"
  249. @keyup.shift.enter="
  250. getAlbumData('artists')
  251. "
  252. />
  253. <button
  254. class="button album-get-button"
  255. @click="getAlbumData('artists')"
  256. >
  257. <i
  258. class="material-icons"
  259. v-tippy
  260. content="Fill from Discogs"
  261. >album</i
  262. >
  263. </button>
  264. <button
  265. class="button is-info add-button"
  266. @click="addTag('artists')"
  267. >
  268. <i class="material-icons">add</i>
  269. </button>
  270. </p>
  271. <div class="list-container">
  272. <div
  273. class="list-item"
  274. v-for="artist in song.artists"
  275. :key="artist"
  276. >
  277. <div
  278. class="list-item-circle"
  279. @click="
  280. removeTag('artists', artist)
  281. "
  282. >
  283. <i class="material-icons">close</i>
  284. </div>
  285. <p>{{ artist }}</p>
  286. </div>
  287. </div>
  288. </div>
  289. <div class="genres-container">
  290. <label class="label">
  291. <span>Genres</span>
  292. <i
  293. class="material-icons"
  294. @click="toggleGenreHelper"
  295. @dblclick="resetGenreHelper"
  296. v-tippy
  297. content="View list of genres"
  298. >info</i
  299. >
  300. </label>
  301. <p class="control has-addons">
  302. <auto-suggest
  303. v-model="genreInputValue"
  304. ref="new-genre"
  305. placeholder="Add genre..."
  306. :all-items="autosuggest.allItems.genres"
  307. @submitted="addTag('genres')"
  308. @keyup.shift.enter="
  309. getAlbumData('genres')
  310. "
  311. />
  312. <button
  313. class="button album-get-button"
  314. @click="getAlbumData('genres')"
  315. >
  316. <i
  317. class="material-icons"
  318. v-tippy
  319. content="Fill from Discogs"
  320. >album</i
  321. >
  322. </button>
  323. <button
  324. class="button is-info add-button"
  325. @click="addTag('genres')"
  326. >
  327. <i class="material-icons">add</i>
  328. </button>
  329. </p>
  330. <div class="list-container">
  331. <div
  332. class="list-item"
  333. v-for="genre in song.genres"
  334. :key="genre"
  335. >
  336. <div
  337. class="list-item-circle"
  338. @click="removeTag('genres', genre)"
  339. >
  340. <i class="material-icons">close</i>
  341. </div>
  342. <p>{{ genre }}</p>
  343. </div>
  344. </div>
  345. </div>
  346. <div class="tags-container">
  347. <label class="label">Tags</label>
  348. <p class="control has-addons">
  349. <auto-suggest
  350. v-model="tagInputValue"
  351. ref="new-tag"
  352. placeholder="Add tag..."
  353. :all-items="autosuggest.allItems.tags"
  354. @submitted="addTag('tags')"
  355. />
  356. <button
  357. class="button is-info add-button"
  358. @click="addTag('tags')"
  359. >
  360. <i class="material-icons">add</i>
  361. </button>
  362. </p>
  363. <div class="list-container">
  364. <div
  365. class="list-item"
  366. v-for="tag in song.tags"
  367. :key="tag"
  368. >
  369. <div
  370. class="list-item-circle"
  371. @click="removeTag('tags', tag)"
  372. >
  373. <i class="material-icons">close</i>
  374. </div>
  375. <p>{{ tag }}</p>
  376. </div>
  377. </div>
  378. </div>
  379. </div>
  380. </div>
  381. </div>
  382. <div class="right-section" v-if="songDataLoaded">
  383. <div id="tabs-container">
  384. <div id="tab-selection">
  385. <button
  386. class="button is-default"
  387. :class="{ selected: tab === 'discogs' }"
  388. ref="discogs-tab"
  389. @click="showTab('discogs')"
  390. >
  391. Discogs
  392. </button>
  393. <button
  394. class="button is-default"
  395. :class="{ selected: tab === 'reports' }"
  396. ref="reports-tab"
  397. @click="showTab('reports')"
  398. >
  399. Reports ({{ reports.length }})
  400. </button>
  401. <button
  402. class="button is-default"
  403. :class="{ selected: tab === 'youtube' }"
  404. ref="youtube-tab"
  405. @click="showTab('youtube')"
  406. >
  407. YouTube
  408. </button>
  409. <button
  410. class="button is-default"
  411. :class="{ selected: tab === 'musare-songs' }"
  412. ref="musare-songs-tab"
  413. @click="showTab('musare-songs')"
  414. >
  415. Songs
  416. </button>
  417. </div>
  418. <discogs class="tab" v-show="tab === 'discogs'" />
  419. <reports class="tab" v-show="tab === 'reports'" />
  420. <youtube class="tab" v-show="tab === 'youtube'" />
  421. <musare-songs
  422. class="tab"
  423. v-show="tab === 'musare-songs'"
  424. />
  425. </div>
  426. </div>
  427. </template>
  428. <template #footer>
  429. <slot name="footer-actions" :song="song" />
  430. <div>
  431. <save-button
  432. ref="saveButton"
  433. @clicked="save(song, false, false)"
  434. />
  435. <save-button
  436. ref="saveAndCloseButton"
  437. default-message="Save and close"
  438. @clicked="save(song, false, true)"
  439. />
  440. <save-button
  441. ref="saveVerifyAndCloseButton"
  442. default-message="Save, verify and close"
  443. @click="save(song, true, true)"
  444. />
  445. <div class="right">
  446. <button
  447. v-if="!song.verified"
  448. class="button is-success"
  449. @click="verify(song._id)"
  450. content="Verify Song"
  451. v-tippy
  452. >
  453. <i class="material-icons">check_circle</i>
  454. </button>
  455. <quick-confirm
  456. v-if="song.verified"
  457. placement="left"
  458. @confirm="unverify(song._id)"
  459. >
  460. <button
  461. class="button is-danger"
  462. content="Unverify Song"
  463. v-tippy
  464. >
  465. <i class="material-icons">cancel</i>
  466. </button>
  467. </quick-confirm>
  468. <button
  469. class="
  470. button
  471. is-danger
  472. icon-with-button
  473. material-icons
  474. "
  475. @click.prevent="
  476. confirmAction({
  477. message:
  478. 'Removing this song will remove it from all playlists and cause a ratings recalculation.',
  479. action: 'remove',
  480. params: song._id
  481. })
  482. "
  483. content="Delete Song"
  484. v-tippy
  485. >
  486. delete_forever
  487. </button>
  488. </div>
  489. </div>
  490. </template>
  491. </modal>
  492. <floating-box id="genreHelper" ref="genreHelper" :column="false">
  493. <template #body>
  494. <span
  495. v-for="item in autosuggest.allItems.genres"
  496. :key="`genre-helper-${item}`"
  497. >
  498. {{ item }}
  499. </span>
  500. </template>
  501. </floating-box>
  502. <confirm v-if="modals.editSongConfirm" @confirmed="handleConfirmed()" />
  503. </div>
  504. </template>
  505. <script>
  506. import { mapState, mapGetters, mapActions } from "vuex";
  507. import { defineAsyncComponent } from "vue";
  508. import Toast from "toasters";
  509. import aw from "@/aw";
  510. import ws from "@/ws";
  511. import validation from "@/validation";
  512. import keyboardShortcuts from "@/keyboardShortcuts";
  513. import QuickConfirm from "@/components/QuickConfirm.vue";
  514. import Modal from "../../Modal.vue";
  515. import FloatingBox from "../../FloatingBox.vue";
  516. import SaveButton from "../../SaveButton.vue";
  517. import AutoSuggest from "@/components/AutoSuggest.vue";
  518. import Discogs from "./Tabs/Discogs.vue";
  519. import Reports from "./Tabs/Reports.vue";
  520. import Youtube from "./Tabs/Youtube.vue";
  521. import MusareSongs from "./Tabs/Songs.vue";
  522. export default {
  523. components: {
  524. Modal,
  525. FloatingBox,
  526. SaveButton,
  527. QuickConfirm,
  528. AutoSuggest,
  529. Discogs,
  530. Reports,
  531. Youtube,
  532. MusareSongs,
  533. Confirm: defineAsyncComponent(() =>
  534. import("@/components/modals/Confirm.vue")
  535. )
  536. },
  537. props: {
  538. // songId: { type: String, default: null },
  539. discogsAlbum: { type: Object, default: null },
  540. sector: { type: String, default: "admin" },
  541. bulk: { type: Boolean, default: false }
  542. },
  543. emits: ["error", "savedSuccess", "savedError"],
  544. data() {
  545. return {
  546. songDataLoaded: false,
  547. youtubeError: false,
  548. youtubeErrorMessage: "",
  549. focusedElementBefore: null,
  550. youtubeVideoDuration: "0.000",
  551. youtubeVideoCurrentTime: 0,
  552. youtubeVideoNote: "",
  553. useHTTPS: false,
  554. muted: false,
  555. volumeSliderValue: 0,
  556. skipToLast10SecsPressed: false,
  557. artistInputValue: "",
  558. genreInputValue: "",
  559. tagInputValue: "",
  560. activityWatchVideoDataInterval: null,
  561. activityWatchVideoLastStatus: "",
  562. activityWatchVideoLastStartDuration: "",
  563. confirm: {
  564. message: "",
  565. action: "",
  566. params: null
  567. },
  568. autosuggest: {
  569. allItems: {
  570. artists: [],
  571. genres: [],
  572. tags: []
  573. }
  574. }
  575. };
  576. },
  577. computed: {
  578. ...mapState("modals/editSong", {
  579. tab: state => state.tab,
  580. video: state => state.video,
  581. song: state => state.song,
  582. songId: state => state.songId,
  583. prefillData: state => state.prefillData,
  584. originalSong: state => state.originalSong,
  585. reports: state => state.reports
  586. }),
  587. ...mapState("modalVisibility", {
  588. modals: state => state.modals
  589. }),
  590. ...mapGetters({
  591. socket: "websockets/getSocket"
  592. })
  593. },
  594. watch: {
  595. /* eslint-disable */
  596. "song.duration": function () {
  597. this.drawCanvas();
  598. },
  599. "song.skipDuration": function () {
  600. this.drawCanvas();
  601. },
  602. /* eslint-enable */
  603. songId(songId, oldSongId) {
  604. console.log("NEW SONG ID", songId);
  605. this.unloadSong(oldSongId);
  606. this.loadSong(songId);
  607. }
  608. },
  609. async mounted() {
  610. this.activityWatchVideoDataInterval = setInterval(() => {
  611. this.sendActivityWatchVideoData();
  612. }, 1000);
  613. this.useHTTPS = await lofig.get("cookie.secure");
  614. ws.onConnect(this.init);
  615. let volume = parseFloat(localStorage.getItem("volume"));
  616. volume =
  617. typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
  618. localStorage.setItem("volume", volume);
  619. this.volumeSliderValue = volume * 100;
  620. this.socket.on(
  621. "event:admin.song.updated",
  622. res => {
  623. if (res.data.song._id === this.song._id)
  624. this.song.verified = res.data.song.verified;
  625. },
  626. { modal: "editSong" }
  627. );
  628. this.socket.on(
  629. "event:admin.song.removed",
  630. res => {
  631. if (res.data.songId === this.song._id) {
  632. this.closeModal("editSong");
  633. setTimeout(() => {
  634. window.focusedElementBefore.focus();
  635. }, 500);
  636. }
  637. },
  638. { modal: "editSong" }
  639. );
  640. keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
  641. keyCode: 101,
  642. preventDefault: true,
  643. handler: () => {
  644. if (this.video.paused) this.play();
  645. else this.settings("pause");
  646. }
  647. });
  648. keyboardShortcuts.registerShortcut("editSong.stopVideo", {
  649. keyCode: 101,
  650. ctrl: true,
  651. preventDefault: true,
  652. handler: () => {
  653. this.settings("stop");
  654. }
  655. });
  656. keyboardShortcuts.registerShortcut("editSong.skipToLast10Secs", {
  657. keyCode: 102,
  658. preventDefault: true,
  659. handler: () => {
  660. this.settings("skipToLast10Secs");
  661. }
  662. });
  663. keyboardShortcuts.registerShortcut("editSong.lowerVolumeLarge", {
  664. keyCode: 98,
  665. preventDefault: true,
  666. handler: () => {
  667. this.volumeSliderValue = Math.max(
  668. 0,
  669. this.volumeSliderValue - 1000
  670. );
  671. this.changeVolume();
  672. }
  673. });
  674. keyboardShortcuts.registerShortcut("editSong.lowerVolumeSmall", {
  675. keyCode: 98,
  676. ctrl: true,
  677. preventDefault: true,
  678. handler: () => {
  679. this.volumeSliderValue = Math.max(
  680. 0,
  681. this.volumeSliderValue - 100
  682. );
  683. this.changeVolume();
  684. }
  685. });
  686. keyboardShortcuts.registerShortcut("editSong.increaseVolumeLarge", {
  687. keyCode: 104,
  688. preventDefault: true,
  689. handler: () => {
  690. this.volumeSliderValue = Math.min(
  691. 10000,
  692. this.volumeSliderValue + 1000
  693. );
  694. this.changeVolume();
  695. }
  696. });
  697. keyboardShortcuts.registerShortcut("editSong.increaseVolumeSmall", {
  698. keyCode: 104,
  699. ctrl: true,
  700. preventDefault: true,
  701. handler: () => {
  702. this.volumeSliderValue = Math.min(
  703. 10000,
  704. this.volumeSliderValue + 100
  705. );
  706. this.changeVolume();
  707. }
  708. });
  709. keyboardShortcuts.registerShortcut("editSong.save", {
  710. keyCode: 83,
  711. ctrl: true,
  712. preventDefault: true,
  713. handler: () => {
  714. this.save(this.song, false, false);
  715. }
  716. });
  717. keyboardShortcuts.registerShortcut("editSong.saveClose", {
  718. keyCode: 83,
  719. ctrl: true,
  720. alt: true,
  721. preventDefault: true,
  722. handler: () => {
  723. this.save(this.song, true);
  724. }
  725. });
  726. // TODO
  727. keyboardShortcuts.registerShortcut("editSong.saveVerifyClose", {
  728. keyCode: 86,
  729. ctrl: true,
  730. alt: true,
  731. preventDefault: true,
  732. handler: () => {
  733. // alert("not implemented yet");
  734. }
  735. });
  736. keyboardShortcuts.registerShortcut("editSong.close", {
  737. keyCode: 115,
  738. preventDefault: true,
  739. handler: () => {
  740. this.closeModal("editSong");
  741. setTimeout(() => {
  742. window.focusedElementBefore.focus();
  743. }, 500);
  744. }
  745. });
  746. keyboardShortcuts.registerShortcut("editSong.focusTitle", {
  747. keyCode: 36,
  748. preventDefault: true,
  749. handler: () => {
  750. this.$refs["title-input"].focus();
  751. }
  752. });
  753. keyboardShortcuts.registerShortcut("editSong.useAllDiscogs", {
  754. keyCode: 68,
  755. alt: true,
  756. ctrl: true,
  757. preventDefault: true,
  758. handler: () => {
  759. this.getAlbumData("title");
  760. this.getAlbumData("albumArt");
  761. this.getAlbumData("artists");
  762. this.getAlbumData("genres");
  763. }
  764. });
  765. /*
  766. editSong.pauseResume - Num 5 - Pause/resume song
  767. editSong.stopVideo - Ctrl - Num 5 - Stop
  768. editSong.skipToLast10Secs - Num 6 - Skip to last 10 seconds
  769. editSong.lowerVolumeLarge - Num 2 - Volume down by 10
  770. editSong.lowerVolumeSmall - Ctrl - Num 2 - Volume down by 1
  771. editSong.increaseVolumeLarge - Num 8 - Volume up by 10
  772. editSong.increaseVolumeSmall - Ctrl - Num 8 - Volume up by 1
  773. editSong.focusTitle - Home - Focus the title input
  774. editSong.focusDicogs - End - Focus the discogs input
  775. editSong.save - Ctrl - S - Saves song
  776. editSong.save - Ctrl - Alt - S - Saves song and closes the modal
  777. editSong.save - Ctrl - Alt - V - Saves song, verifies songs and then closes the modal
  778. editSong.close - F4 - Closes modal without saving
  779. editSong.useAllDiscogs - Ctrl - Alt - D - Sets all fields to the Discogs data
  780. Inside Discogs inputs: Ctrl - D - Sets this field to the Discogs data
  781. */
  782. },
  783. beforeUnmount() {
  784. this.unloadSong(this.songId);
  785. // this.video.player.stopVideo();
  786. this.playerReady = false;
  787. clearInterval(this.interval);
  788. clearInterval(this.activityWatchVideoDataInterval);
  789. // this.socket.dispatch("apis.leaveRoom", `edit-song.${this.song._id}`);
  790. const shortcutNames = [
  791. "editSong.pauseResume",
  792. "editSong.stopVideo",
  793. "editSong.skipToLast10Secs",
  794. "editSong.lowerVolumeLarge",
  795. "editSong.lowerVolumeSmall",
  796. "editSong.increaseVolumeLarge",
  797. "editSong.increaseVolumeSmall",
  798. "editSong.focusTitle",
  799. "editSong.focusDicogs",
  800. "editSong.save",
  801. "editSong.saveClose",
  802. "editSong.saveVerifyClose",
  803. "editSong.close",
  804. "editSong.useAllDiscogs"
  805. ];
  806. shortcutNames.forEach(shortcutName => {
  807. keyboardShortcuts.unregisterShortcut(shortcutName);
  808. });
  809. },
  810. methods: {
  811. init(why) {
  812. if (this.songId) this.loadSong(this.songId);
  813. else if (!this.bulk) {
  814. new Toast("You can't open EditSong without editing a song");
  815. return this.closeModal("editSong");
  816. }
  817. this.interval = setInterval(() => {
  818. if (
  819. this.song.duration !== -1 &&
  820. this.video.paused === false &&
  821. this.playerReady &&
  822. (this.video.player.getCurrentTime() -
  823. this.song.skipDuration >
  824. this.song.duration ||
  825. (this.video.player.getCurrentTime() > 0 &&
  826. this.video.player.getCurrentTime() >=
  827. this.video.player.getDuration()))
  828. ) {
  829. this.video.paused = true;
  830. this.video.player.stopVideo();
  831. this.drawCanvas();
  832. }
  833. if (
  834. this.playerReady &&
  835. this.video.player.getVideoData &&
  836. this.video.player.getVideoData().video_id ===
  837. this.song.youtubeId
  838. ) {
  839. const currentTime = this.video.player.getCurrentTime();
  840. if (currentTime !== undefined)
  841. this.youtubeVideoCurrentTime = currentTime.toFixed(3);
  842. if (this.youtubeVideoDuration === "0.000") {
  843. const duration = this.video.player.getDuration();
  844. if (duration !== undefined) {
  845. this.youtubeVideoDuration = duration.toFixed(3);
  846. this.youtubeVideoNote = "(~)";
  847. this.drawCanvas();
  848. }
  849. }
  850. }
  851. if (this.video.paused === false) this.drawCanvas();
  852. }, 200);
  853. if (window.YT && window.YT.Player) {
  854. this.video.player = new window.YT.Player("editSongPlayer", {
  855. height: 298,
  856. width: 530,
  857. videoId: null,
  858. host: "https://www.youtube-nocookie.com",
  859. playerVars: {
  860. controls: 0,
  861. iv_load_policy: 3,
  862. rel: 0,
  863. showinfo: 0,
  864. autoplay: 0
  865. },
  866. startSeconds: this.song.skipDuration,
  867. events: {
  868. onReady: () => {
  869. let volume = parseInt(
  870. localStorage.getItem("volume")
  871. );
  872. volume = typeof volume === "number" ? volume : 20;
  873. this.video.player.setVolume(volume);
  874. if (volume > 0) this.video.player.unMute();
  875. // const duration = this.video.player.getDuration();
  876. // this.youtubeVideoDuration = duration.toFixed(3);
  877. // this.youtubeVideoNote = "(~)";
  878. this.playerReady = true;
  879. if (this.song && this.song._id)
  880. this.video.player.cueVideoById(
  881. this.song.youtubeId,
  882. this.song.skipDuration
  883. );
  884. this.drawCanvas();
  885. },
  886. onStateChange: event => {
  887. this.drawCanvas();
  888. let skipToLast10SecsPressed = false;
  889. if (
  890. event.data === 1 &&
  891. this.skipToLast10SecsPressed
  892. ) {
  893. this.skipToLast10SecsPressed = false;
  894. skipToLast10SecsPressed = true;
  895. }
  896. if (event.data === 1 && !skipToLast10SecsPressed) {
  897. this.video.paused = false;
  898. let youtubeDuration =
  899. this.video.player.getDuration();
  900. const newYoutubeVideoDuration =
  901. youtubeDuration.toFixed(3);
  902. const songDurationNumber = Number(
  903. this.song.duration
  904. );
  905. const songDurationNumber2 =
  906. Number(this.song.duration) + 1;
  907. const songDurationNumber3 =
  908. Number(this.song.duration) - 1;
  909. const fixedSongDuration =
  910. songDurationNumber.toFixed(3);
  911. const fixedSongDuration2 =
  912. songDurationNumber2.toFixed(3);
  913. const fixedSongDuration3 =
  914. songDurationNumber3.toFixed(3);
  915. if (
  916. this.youtubeVideoDuration !==
  917. newYoutubeVideoDuration &&
  918. (fixedSongDuration ===
  919. this.youtubeVideoDuration ||
  920. fixedSongDuration2 ===
  921. this.youtubeVideoDuration ||
  922. fixedSongDuration3 ===
  923. this.youtubeVideoDuration)
  924. )
  925. this.song.duration =
  926. newYoutubeVideoDuration;
  927. this.youtubeVideoDuration =
  928. newYoutubeVideoDuration;
  929. this.youtubeVideoNote = "";
  930. if (this.song.duration === -1)
  931. this.song.duration = youtubeDuration;
  932. youtubeDuration -= this.song.skipDuration;
  933. if (this.song.duration > youtubeDuration + 1) {
  934. this.video.player.stopVideo();
  935. this.video.paused = true;
  936. return new Toast(
  937. "Video can't play. Specified duration is bigger than the YouTube song duration."
  938. );
  939. }
  940. if (this.song.duration <= 0) {
  941. this.video.player.stopVideo();
  942. this.video.paused = true;
  943. return new Toast(
  944. "Video can't play. Specified duration has to be more than 0 seconds."
  945. );
  946. }
  947. if (
  948. this.video.player.getCurrentTime() <
  949. this.song.skipDuration
  950. ) {
  951. return this.video.player.seekTo(
  952. this.song.skipDuration
  953. );
  954. }
  955. } else if (event.data === 2) {
  956. this.video.paused = true;
  957. }
  958. return false;
  959. }
  960. }
  961. });
  962. } else {
  963. this.youtubeError = true;
  964. this.youtubeErrorMessage = "Player could not be loaded.";
  965. }
  966. ["artists", "genres", "tags"].forEach(type => {
  967. this.socket.dispatch(
  968. `songs.get${type.charAt(0).toUpperCase()}${type.slice(1)}`,
  969. res => {
  970. if (res.status === "success") {
  971. const { items } = res.data;
  972. this.autosuggest.allItems[type] = items;
  973. } else {
  974. new Toast(res.message);
  975. }
  976. }
  977. );
  978. });
  979. },
  980. unloadSong(songId) {
  981. this.songDataLoaded = false;
  982. if (this.video.player && this.video.player.stopVideo)
  983. this.video.player.stopVideo();
  984. // this.resetSong(songId);
  985. this.youtubeVideoCurrentTime = "0.000";
  986. this.youtubeVideoDuration = "0.000";
  987. this.socket.dispatch("apis.leaveRoom", `edit-song.${songId}`);
  988. if (this.$refs.saveButton) this.$refs.saveButton.status = "default";
  989. },
  990. loadSong(songId) {
  991. console.log(`LOAD SONG ${songId}`);
  992. this.socket.dispatch(
  993. `songs.getSongFromSongId`,
  994. songId, // Was this.song._id
  995. res => {
  996. if (res.status === "success") {
  997. let { song } = res.data;
  998. song = Object.assign(song, this.prefillData);
  999. this.setSong(song);
  1000. this.songDataLoaded = true;
  1001. this.socket.dispatch(
  1002. "apis.joinRoom",
  1003. `edit-song.${this.song._id}`
  1004. );
  1005. // console.log(this.video.player);
  1006. // console.log(this.video.player.loadVideoById);
  1007. if (
  1008. this.video.player &&
  1009. this.video.player.cueVideoById
  1010. ) {
  1011. this.video.player.cueVideoById(
  1012. this.song.youtubeId,
  1013. this.song.skipDuration
  1014. );
  1015. // const duration = this.video.player.getDuration();
  1016. // if (duration !== undefined) {
  1017. // this.youtubeVideoDuration = duration.toFixed(3);
  1018. // this.youtubeVideoNote = "(~)";
  1019. // this.drawCanvas();
  1020. // }
  1021. }
  1022. } else {
  1023. new Toast("Song with that ID not found");
  1024. if (!this.bulk) this.closeModal("editSong");
  1025. }
  1026. }
  1027. );
  1028. this.socket.dispatch(
  1029. "reports.getReportsForSong",
  1030. this.song._id,
  1031. res => {
  1032. this.updateReports(res.data.reports);
  1033. }
  1034. );
  1035. },
  1036. importAlbum(result) {
  1037. this.selectDiscogsAlbum(result);
  1038. this.openModal("importAlbum");
  1039. this.closeModal("editSong");
  1040. },
  1041. save(songToCopy, verify, close) {
  1042. const song = JSON.parse(JSON.stringify(songToCopy));
  1043. this.$emit("saving", song._id);
  1044. let saveButtonRef = this.$refs.saveButton;
  1045. if (close && !verify) saveButtonRef = this.$refs.saveAndCloseButton;
  1046. else if (close && verify)
  1047. saveButtonRef = this.$refs.saveVerifyAndCloseButton;
  1048. if (!this.youtubeError && this.youtubeVideoDuration === "0.000") {
  1049. saveButtonRef.handleFailedSave();
  1050. this.$emit("savedError", song._id);
  1051. return new Toast("The video appears to not be working.");
  1052. }
  1053. if (!song.title) {
  1054. saveButtonRef.handleFailedSave();
  1055. this.$emit("savedError", song._id);
  1056. return new Toast("Please fill in all fields");
  1057. }
  1058. if (!song.thumbnail) {
  1059. saveButtonRef.handleFailedSave();
  1060. this.$emit("savedError", song._id);
  1061. return new Toast("Please fill in all fields");
  1062. }
  1063. // const thumbnailHeight = this.$refs.thumbnailElement.naturalHeight;
  1064. // const thumbnailWidth = this.$refs.thumbnailElement.naturalWidth;
  1065. // if (thumbnailHeight < 80 || thumbnailWidth < 80) {
  1066. // saveButtonRef.handleFailedSave();
  1067. // return new Toast(
  1068. // "Thumbnail width and height must be at least 80px."
  1069. // );
  1070. // }
  1071. // if (thumbnailHeight > 4000 || thumbnailWidth > 4000) {
  1072. // saveButtonRef.handleFailedSave();
  1073. // return new Toast(
  1074. // "Thumbnail width and height must be less than 4000px."
  1075. // );
  1076. // }
  1077. // if (thumbnailHeight - thumbnailWidth > 5) {
  1078. // saveButtonRef.handleFailedSave();
  1079. // return new Toast("Thumbnail cannot be taller than it is wide.");
  1080. // }
  1081. // Youtube Id
  1082. if (
  1083. this.youtubeError &&
  1084. this.originalSong.youtubeId !== song.youtubeId
  1085. ) {
  1086. saveButtonRef.handleFailedSave();
  1087. this.$emit("savedError", song._id);
  1088. return new Toast(
  1089. "You're not allowed to change the YouTube id while the player is not working"
  1090. );
  1091. }
  1092. // Duration
  1093. if (
  1094. Number(song.skipDuration) + Number(song.duration) >
  1095. this.youtubeVideoDuration &&
  1096. (!this.youtubeError ||
  1097. this.originalSong.duration !== song.duration)
  1098. ) {
  1099. saveButtonRef.handleFailedSave();
  1100. this.$emit("savedError", song._id);
  1101. return new Toast(
  1102. "Duration can't be higher than the length of the video"
  1103. );
  1104. }
  1105. // Title
  1106. if (!validation.isLength(song.title, 1, 100)) {
  1107. saveButtonRef.handleFailedSave();
  1108. this.$emit("savedError", song._id);
  1109. return new Toast(
  1110. "Title must have between 1 and 100 characters."
  1111. );
  1112. }
  1113. // Artists
  1114. if (song.artists.length < 1 || song.artists.length > 10) {
  1115. saveButtonRef.handleFailedSave();
  1116. this.$emit("savedError", song._id);
  1117. return new Toast(
  1118. "Invalid artists. You must have at least 1 artist and a maximum of 10 artists."
  1119. );
  1120. }
  1121. let error;
  1122. song.artists.forEach(artist => {
  1123. if (!validation.isLength(artist, 1, 64)) {
  1124. error = "Artist must have between 1 and 64 characters.";
  1125. return error;
  1126. }
  1127. if (artist === "NONE") {
  1128. error =
  1129. 'Invalid artist format. Artists are not allowed to be named "NONE".';
  1130. return error;
  1131. }
  1132. return false;
  1133. });
  1134. if (error) {
  1135. saveButtonRef.handleFailedSave();
  1136. this.$emit("savedError", song._id);
  1137. return new Toast(error);
  1138. }
  1139. // Genres
  1140. error = undefined;
  1141. song.genres.forEach(genre => {
  1142. if (!validation.isLength(genre, 1, 32)) {
  1143. error = "Genre must have between 1 and 32 characters.";
  1144. return error;
  1145. }
  1146. if (!validation.regex.ascii.test(genre)) {
  1147. error =
  1148. "Invalid genre format. Only ascii characters are allowed.";
  1149. return error;
  1150. }
  1151. return false;
  1152. });
  1153. if (song.genres.length < 1 || song.genres.length > 16)
  1154. error = "You must have between 1 and 16 genres.";
  1155. if (error) {
  1156. saveButtonRef.handleFailedSave();
  1157. this.$emit("savedError", song._id);
  1158. return new Toast(error);
  1159. }
  1160. error = undefined;
  1161. song.tags.forEach(tag => {
  1162. if (
  1163. !new RegExp(
  1164. /^[a-zA-Z0-9_]{1,64}$|^[a-zA-Z0-9_]{1,64}\[[a-zA-Z0-9_]{1,64}\]$/
  1165. ).test(tag)
  1166. ) {
  1167. error = "Invalid tag format.";
  1168. return error;
  1169. }
  1170. return false;
  1171. });
  1172. if (error) {
  1173. saveButtonRef.handleFailedSave();
  1174. this.$emit("savedError", song._id);
  1175. return new Toast(error);
  1176. }
  1177. // Thumbnail
  1178. if (!validation.isLength(song.thumbnail, 1, 256)) {
  1179. saveButtonRef.handleFailedSave();
  1180. this.$emit("savedError", song._id);
  1181. return new Toast(
  1182. "Thumbnail must have between 8 and 256 characters."
  1183. );
  1184. }
  1185. if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
  1186. saveButtonRef.handleFailedSave();
  1187. this.$emit("savedError", song._id);
  1188. return new Toast('Thumbnail must start with "https://".');
  1189. }
  1190. if (
  1191. !this.useHTTPS &&
  1192. song.thumbnail.indexOf("http://") !== 0 &&
  1193. song.thumbnail.indexOf("https://") !== 0
  1194. ) {
  1195. saveButtonRef.handleFailedSave();
  1196. this.$emit("savedError", song._id);
  1197. return new Toast('Thumbnail must start with "http://".');
  1198. }
  1199. saveButtonRef.status = "disabled";
  1200. return this.socket.dispatch(`songs.update`, song._id, song, res => {
  1201. new Toast(res.message);
  1202. if (res.status === "success")
  1203. saveButtonRef.handleSuccessfulSave();
  1204. else saveButtonRef.handleFailedSave();
  1205. if (res.status === "success")
  1206. this.$emit("savedSuccess", song._id);
  1207. else if (res.status === "error")
  1208. this.$emit("savedError", song._id);
  1209. if (verify) this.verify(this.song._id);
  1210. if (close) this.closeModal("editSong");
  1211. });
  1212. },
  1213. getAlbumData(type) {
  1214. if (!this.song.discogs) return;
  1215. if (type === "title")
  1216. this.updateSongField({
  1217. field: "title",
  1218. value: this.song.discogs.track.title
  1219. });
  1220. if (type === "albumArt")
  1221. this.updateSongField({
  1222. field: "thumbnail",
  1223. value: this.song.discogs.album.albumArt
  1224. });
  1225. if (type === "genres")
  1226. this.updateSongField({
  1227. field: "genres",
  1228. value: JSON.parse(
  1229. JSON.stringify(this.song.discogs.album.genres)
  1230. )
  1231. });
  1232. if (type === "artists")
  1233. this.updateSongField({
  1234. field: "artists",
  1235. value: JSON.parse(
  1236. JSON.stringify(this.song.discogs.album.artists)
  1237. )
  1238. });
  1239. },
  1240. fillDuration() {
  1241. this.song.duration =
  1242. this.youtubeVideoDuration - this.song.skipDuration;
  1243. },
  1244. settings(type) {
  1245. switch (type) {
  1246. default:
  1247. break;
  1248. case "stop":
  1249. this.stopVideo();
  1250. this.pauseVideo(true);
  1251. break;
  1252. case "pause":
  1253. this.pauseVideo(true);
  1254. break;
  1255. case "play":
  1256. this.pauseVideo(false);
  1257. break;
  1258. case "skipToLast10Secs":
  1259. this.skipToLast10SecsPressed = true;
  1260. if (this.video.paused) this.pauseVideo(false);
  1261. this.video.player.seekTo(
  1262. this.song.duration - 10 + this.song.skipDuration
  1263. );
  1264. break;
  1265. }
  1266. },
  1267. play() {
  1268. if (
  1269. this.video.player.getVideoData().video_id !==
  1270. this.song.youtubeId
  1271. ) {
  1272. this.song.duration = -1;
  1273. this.loadVideoById(this.song.youtubeId, this.song.skipDuration);
  1274. }
  1275. this.settings("play");
  1276. },
  1277. changeVolume() {
  1278. const volume = this.volumeSliderValue;
  1279. localStorage.setItem("volume", volume / 100);
  1280. this.video.player.setVolume(volume / 100);
  1281. if (volume > 0) {
  1282. this.video.player.unMute();
  1283. this.muted = false;
  1284. }
  1285. },
  1286. toggleMute() {
  1287. const previousVolume = parseFloat(localStorage.getItem("volume"));
  1288. const volume =
  1289. this.video.player.getVolume() * 100 <= 0 ? previousVolume : 0;
  1290. this.muted = !this.muted;
  1291. this.volumeSliderValue = volume * 100;
  1292. this.video.player.setVolume(volume);
  1293. if (!this.muted) localStorage.setItem("volume", volume);
  1294. },
  1295. increaseVolume() {
  1296. const previousVolume = parseInt(localStorage.getItem("volume"));
  1297. let volume = previousVolume + 5;
  1298. this.muted = false;
  1299. if (volume > 100) volume = 100;
  1300. this.volumeSliderValue = volume * 100;
  1301. this.video.player.setVolume(volume);
  1302. localStorage.setItem("volume", volume);
  1303. },
  1304. addTag(type, value) {
  1305. if (type === "genres") {
  1306. const genre = value || this.genreInputValue.trim();
  1307. if (
  1308. this.song.genres
  1309. .map(genre => genre.toLowerCase())
  1310. .indexOf(genre.toLowerCase()) !== -1
  1311. )
  1312. return new Toast("Genre already exists");
  1313. if (genre) {
  1314. this.song.genres.push(genre);
  1315. this.genreInputValue = "";
  1316. return false;
  1317. }
  1318. return new Toast("Genre cannot be empty");
  1319. }
  1320. if (type === "artists") {
  1321. const artist = value || this.artistInputValue;
  1322. if (this.song.artists.indexOf(artist) !== -1)
  1323. return new Toast("Artist already exists");
  1324. if (artist !== "") {
  1325. this.song.artists.push(artist);
  1326. this.artistInputValue = "";
  1327. return false;
  1328. }
  1329. return new Toast("Artist cannot be empty");
  1330. }
  1331. if (type === "tags") {
  1332. const tag = value || this.tagInputValue;
  1333. if (this.song.tags.indexOf(tag) !== -1)
  1334. return new Toast("Tag already exists");
  1335. if (tag !== "") {
  1336. this.song.tags.push(tag);
  1337. this.tagInputValue = "";
  1338. return false;
  1339. }
  1340. return new Toast("Tag cannot be empty");
  1341. }
  1342. return false;
  1343. },
  1344. removeTag(type, value) {
  1345. if (type === "genres")
  1346. this.song.genres.splice(this.song.genres.indexOf(value), 1);
  1347. else if (type === "artists")
  1348. this.song.artists.splice(this.song.artists.indexOf(value), 1);
  1349. else if (type === "tags")
  1350. this.song.tags.splice(this.song.tags.indexOf(value), 1);
  1351. },
  1352. drawCanvas() {
  1353. if (!this.songDataLoaded) return;
  1354. const canvasElement = this.$refs.durationCanvas;
  1355. const ctx = canvasElement.getContext("2d");
  1356. const videoDuration = Number(this.youtubeVideoDuration);
  1357. const skipDuration = Number(this.song.skipDuration);
  1358. const duration = Number(this.song.duration);
  1359. const afterDuration = videoDuration - (skipDuration + duration);
  1360. const width = 530;
  1361. const currentTime =
  1362. this.video.player && this.video.player.getCurrentTime
  1363. ? this.video.player.getCurrentTime()
  1364. : 0;
  1365. const widthSkipDuration = (skipDuration / videoDuration) * width;
  1366. const widthDuration = (duration / videoDuration) * width;
  1367. const widthAfterDuration = (afterDuration / videoDuration) * width;
  1368. const widthCurrentTime = (currentTime / videoDuration) * width;
  1369. const skipDurationColor = "#F42003";
  1370. const durationColor = "#03A9F4";
  1371. const afterDurationColor = "#41E841";
  1372. const currentDurationColor = "#3b25e8";
  1373. ctx.fillStyle = skipDurationColor;
  1374. ctx.fillRect(0, 0, widthSkipDuration, 20);
  1375. ctx.fillStyle = durationColor;
  1376. ctx.fillRect(widthSkipDuration, 0, widthDuration, 20);
  1377. ctx.fillStyle = afterDurationColor;
  1378. ctx.fillRect(
  1379. widthSkipDuration + widthDuration,
  1380. 0,
  1381. widthAfterDuration,
  1382. 20
  1383. );
  1384. ctx.fillStyle = currentDurationColor;
  1385. ctx.fillRect(widthCurrentTime, 0, 1, 20);
  1386. },
  1387. toggleGenreHelper() {
  1388. this.$refs.genreHelper.toggleBox();
  1389. },
  1390. resetGenreHelper() {
  1391. this.$refs.genreHelper.resetBox();
  1392. },
  1393. sendActivityWatchVideoData() {
  1394. if (!this.video.paused) {
  1395. if (this.activityWatchVideoLastStatus !== "playing") {
  1396. this.activityWatchVideoLastStatus = "playing";
  1397. if (
  1398. this.song.skipDuration > 0 &&
  1399. parseFloat(this.youtubeVideoCurrentTime) === 0
  1400. ) {
  1401. this.activityWatchVideoLastStartDuration = Math.floor(
  1402. this.song.skipDuration +
  1403. parseFloat(this.youtubeVideoCurrentTime)
  1404. );
  1405. } else {
  1406. this.activityWatchVideoLastStartDuration = Math.floor(
  1407. parseFloat(this.youtubeVideoCurrentTime)
  1408. );
  1409. }
  1410. }
  1411. const videoData = {
  1412. title: this.song.title,
  1413. artists: this.song.artists
  1414. ? this.song.artists.join(", ")
  1415. : null,
  1416. youtubeId: this.song.youtubeId,
  1417. muted: this.muted,
  1418. volume: this.volumeSliderValue / 100,
  1419. startedDuration:
  1420. this.activityWatchVideoLastStartDuration <= 0
  1421. ? 0
  1422. : this.activityWatchVideoLastStartDuration,
  1423. source: `editSong#${this.song.youtubeId}`,
  1424. hostname: window.location.hostname
  1425. };
  1426. aw.sendVideoData(videoData);
  1427. } else {
  1428. this.activityWatchVideoLastStatus = "not_playing";
  1429. }
  1430. },
  1431. verify(id) {
  1432. this.socket.dispatch("songs.verify", id, res => {
  1433. new Toast(res.message);
  1434. });
  1435. },
  1436. unverify(id) {
  1437. this.socket.dispatch("songs.unverify", id, res => {
  1438. new Toast(res.message);
  1439. });
  1440. },
  1441. hide(id) {
  1442. this.socket.dispatch("songs.hide", id, res => {
  1443. new Toast(res.message);
  1444. });
  1445. },
  1446. unhide(id) {
  1447. this.socket.dispatch("songs.unhide", id, res => {
  1448. new Toast(res.message);
  1449. });
  1450. },
  1451. remove(id) {
  1452. this.socket.dispatch("songs.remove", id, res => {
  1453. new Toast(res.message);
  1454. });
  1455. },
  1456. confirmAction(confirm) {
  1457. this.confirm = confirm;
  1458. this.updateConfirmMessage(confirm.message);
  1459. this.openModal("editSongConfirm");
  1460. },
  1461. handleConfirmed() {
  1462. const { action, params } = this.confirm;
  1463. if (typeof this[action] === "function") {
  1464. if (params) this[action](params);
  1465. else this[action]();
  1466. }
  1467. this.confirm = {
  1468. message: "",
  1469. action: "",
  1470. params: null
  1471. };
  1472. },
  1473. ...mapActions("modals/importAlbum", ["selectDiscogsAlbum"]),
  1474. ...mapActions({
  1475. showTab(dispatch, payload) {
  1476. this.$refs[`${payload}-tab`].scrollIntoView({
  1477. block: "nearest"
  1478. });
  1479. return dispatch("modals/editSong/showTab", payload);
  1480. }
  1481. }),
  1482. ...mapActions("modals/editSong", [
  1483. "stopVideo",
  1484. "loadVideoById",
  1485. "pauseVideo",
  1486. "getCurrentTime",
  1487. "setSong",
  1488. "resetSong",
  1489. "updateSongField",
  1490. "updateReports"
  1491. ]),
  1492. ...mapActions("modals/confirm", ["updateConfirmMessage"]),
  1493. ...mapActions("modalVisibility", ["closeModal", "openModal"])
  1494. }
  1495. };
  1496. </script>
  1497. <style lang="scss" scoped>
  1498. .night-mode {
  1499. .edit-section,
  1500. .player-footer,
  1501. #tabs-container {
  1502. background-color: var(--dark-grey-3) !important;
  1503. border: 0 !important;
  1504. .tab {
  1505. border: 0 !important;
  1506. }
  1507. }
  1508. #tabs-container #tab-selection .button {
  1509. background: var(--dark-grey) !important;
  1510. color: var(--white) !important;
  1511. }
  1512. .left-section {
  1513. .edit-section {
  1514. .album-get-button,
  1515. .duration-fill-button,
  1516. .add-button {
  1517. &:focus,
  1518. &:hover {
  1519. border: none !important;
  1520. }
  1521. }
  1522. }
  1523. }
  1524. }
  1525. .modal-card-body {
  1526. display: flex;
  1527. }
  1528. .notice-container {
  1529. display: flex;
  1530. flex: 1;
  1531. justify-content: center;
  1532. h4 {
  1533. margin: auto;
  1534. }
  1535. }
  1536. .left-section {
  1537. flex-basis: unset !important;
  1538. height: 100%;
  1539. display: flex;
  1540. flex-direction: column;
  1541. margin-right: 16px;
  1542. .top-section {
  1543. display: flex;
  1544. .player-section {
  1545. width: 530px;
  1546. display: flex;
  1547. flex-direction: column;
  1548. .player-error {
  1549. height: 318px;
  1550. width: 530px;
  1551. display: block;
  1552. border: 1px rgba(163, 224, 255, 0.75) solid;
  1553. border-radius: 5px 5px 0px 0px;
  1554. display: flex;
  1555. align-items: center;
  1556. * {
  1557. margin: 0;
  1558. flex: 1;
  1559. font-size: 30px;
  1560. text-align: center;
  1561. }
  1562. }
  1563. .player-footer {
  1564. border: 1px solid var(--light-grey-3);
  1565. border-radius: 0px 0px 3px 3px;
  1566. display: flex;
  1567. justify-content: space-between;
  1568. height: 54px;
  1569. padding-left: 10px;
  1570. padding-right: 10px;
  1571. > * {
  1572. width: 33.3%;
  1573. display: flex;
  1574. align-items: center;
  1575. }
  1576. .player-footer-left {
  1577. flex: 1;
  1578. .button {
  1579. width: 75px;
  1580. &:not(:first-of-type) {
  1581. margin-left: 5px;
  1582. }
  1583. }
  1584. }
  1585. .player-footer-center {
  1586. justify-content: center;
  1587. align-items: center;
  1588. flex: 2;
  1589. font-size: 18px;
  1590. font-weight: 400;
  1591. width: 200px;
  1592. margin: 0 5px;
  1593. img {
  1594. height: 21px;
  1595. margin-right: 12px;
  1596. filter: invert(26%) sepia(54%) saturate(6317%)
  1597. hue-rotate(2deg) brightness(92%) contrast(115%);
  1598. }
  1599. }
  1600. .player-footer-right {
  1601. justify-content: right;
  1602. flex: 1;
  1603. #volume-control {
  1604. margin: 3px;
  1605. margin-top: 0;
  1606. display: flex;
  1607. align-items: center;
  1608. cursor: pointer;
  1609. .volume-slider {
  1610. width: 100%;
  1611. padding: 0 15px;
  1612. background: transparent;
  1613. min-width: 100px;
  1614. }
  1615. input[type="range"] {
  1616. -webkit-appearance: none;
  1617. margin: 7.3px 0;
  1618. }
  1619. input[type="range"]:focus {
  1620. outline: none;
  1621. }
  1622. input[type="range"]::-webkit-slider-runnable-track {
  1623. width: 100%;
  1624. height: 5.2px;
  1625. cursor: pointer;
  1626. box-shadow: 0;
  1627. background: var(--light-grey-3);
  1628. border-radius: 0;
  1629. border: 0;
  1630. }
  1631. input[type="range"]::-webkit-slider-thumb {
  1632. box-shadow: 0;
  1633. border: 0;
  1634. height: 19px;
  1635. width: 19px;
  1636. border-radius: 15px;
  1637. background: var(--primary-color);
  1638. cursor: pointer;
  1639. -webkit-appearance: none;
  1640. margin-top: -6.5px;
  1641. }
  1642. input[type="range"]::-moz-range-track {
  1643. width: 100%;
  1644. height: 5.2px;
  1645. cursor: pointer;
  1646. box-shadow: 0;
  1647. background: var(--light-grey-3);
  1648. border-radius: 0;
  1649. border: 0;
  1650. }
  1651. input[type="range"]::-moz-range-thumb {
  1652. box-shadow: 0;
  1653. border: 0;
  1654. height: 19px;
  1655. width: 19px;
  1656. border-radius: 15px;
  1657. background: var(--primary-color);
  1658. cursor: pointer;
  1659. -webkit-appearance: none;
  1660. margin-top: -6.5px;
  1661. }
  1662. input[type="range"]::-ms-track {
  1663. width: 100%;
  1664. height: 5.2px;
  1665. cursor: pointer;
  1666. box-shadow: 0;
  1667. background: var(--light-grey-3);
  1668. border-radius: 1.3px;
  1669. }
  1670. input[type="range"]::-ms-fill-lower {
  1671. background: var(--light-grey-3);
  1672. border: 0;
  1673. border-radius: 0;
  1674. box-shadow: 0;
  1675. }
  1676. input[type="range"]::-ms-fill-upper {
  1677. background: var(--light-grey-3);
  1678. border: 0;
  1679. border-radius: 0;
  1680. box-shadow: 0;
  1681. }
  1682. input[type="range"]::-ms-thumb {
  1683. box-shadow: 0;
  1684. border: 0;
  1685. height: 15px;
  1686. width: 15px;
  1687. border-radius: 15px;
  1688. background: var(--primary-color);
  1689. cursor: pointer;
  1690. -webkit-appearance: none;
  1691. margin-top: 1.5px;
  1692. }
  1693. }
  1694. }
  1695. }
  1696. }
  1697. .thumbnail-preview {
  1698. width: 189px;
  1699. height: 189px;
  1700. margin-left: 16px;
  1701. }
  1702. }
  1703. .edit-section {
  1704. width: 735px;
  1705. border: 1px solid var(--light-grey-3);
  1706. flex: 1;
  1707. margin-top: 16px;
  1708. border-radius: 3px;
  1709. .album-get-button {
  1710. background-color: var(--purple);
  1711. color: var(--white);
  1712. width: 32px;
  1713. text-align: center;
  1714. border-width: 0;
  1715. }
  1716. .duration-fill-button {
  1717. background-color: var(--dark-red);
  1718. color: var(--white);
  1719. width: 32px;
  1720. text-align: center;
  1721. border-width: 0;
  1722. }
  1723. .add-button {
  1724. background-color: var(--primary-color) !important;
  1725. width: 32px;
  1726. i {
  1727. font-size: 32px;
  1728. }
  1729. }
  1730. .album-get-button,
  1731. .duration-fill-button,
  1732. .add-button {
  1733. &:focus,
  1734. &:hover {
  1735. filter: contrast(0.75);
  1736. border: 1px solid var(--black) !important;
  1737. }
  1738. }
  1739. > div {
  1740. margin: 16px !important;
  1741. }
  1742. input {
  1743. width: 100%;
  1744. }
  1745. .title-container {
  1746. width: calc((100% - 32px) / 2);
  1747. }
  1748. .duration-container {
  1749. margin-right: 16px;
  1750. margin-left: 16px;
  1751. width: calc((100% - 32px) / 4);
  1752. }
  1753. .skip-duration-container {
  1754. width: calc((100% - 32px) / 4);
  1755. }
  1756. .album-art-container {
  1757. margin-right: 16px;
  1758. width: calc((100% - 16px) / 3 * 2);
  1759. }
  1760. .youtube-id-container {
  1761. width: calc((100% - 16px) / 3);
  1762. }
  1763. .artists-container {
  1764. width: calc((100% - 32px) / 3);
  1765. position: relative;
  1766. }
  1767. .genres-container {
  1768. width: calc((100% - 32px) / 3);
  1769. margin-left: 16px;
  1770. margin-right: 16px;
  1771. position: relative;
  1772. label {
  1773. display: flex;
  1774. i {
  1775. font-size: 15px;
  1776. align-self: center;
  1777. margin-left: 5px;
  1778. color: var(--primary-color);
  1779. cursor: pointer;
  1780. -webkit-user-select: none;
  1781. -moz-user-select: none;
  1782. -ms-user-select: none;
  1783. user-select: none;
  1784. }
  1785. }
  1786. }
  1787. .tags-container {
  1788. width: calc((100% - 32px) / 3);
  1789. position: relative;
  1790. }
  1791. .list-item-circle {
  1792. background-color: var(--primary-color);
  1793. width: 16px;
  1794. height: 16px;
  1795. border-radius: 8px;
  1796. cursor: pointer;
  1797. margin-right: 8px;
  1798. float: left;
  1799. -webkit-touch-callout: none;
  1800. -webkit-user-select: none;
  1801. -khtml-user-select: none;
  1802. -moz-user-select: none;
  1803. -ms-user-select: none;
  1804. user-select: none;
  1805. i {
  1806. color: var(--primary-color);
  1807. font-size: 14px;
  1808. margin-left: 1px;
  1809. position: relative;
  1810. top: -1px;
  1811. }
  1812. }
  1813. .list-item-circle:hover,
  1814. .list-item-circle:focus {
  1815. i {
  1816. color: var(--white);
  1817. }
  1818. }
  1819. .list-item > p {
  1820. line-height: 16px;
  1821. word-wrap: break-word;
  1822. width: calc(100% - 24px);
  1823. left: 24px;
  1824. float: left;
  1825. margin-bottom: 8px;
  1826. }
  1827. .list-item:last-child > p {
  1828. margin-bottom: 0;
  1829. }
  1830. }
  1831. }
  1832. .right-section {
  1833. flex-basis: unset !important;
  1834. flex-grow: 0 !important;
  1835. display: flex;
  1836. height: 100%;
  1837. #tabs-container {
  1838. width: 376px;
  1839. #tab-selection {
  1840. display: flex;
  1841. overflow-x: auto;
  1842. .button {
  1843. border-radius: 5px 5px 0 0;
  1844. border: 0;
  1845. text-transform: uppercase;
  1846. font-size: 14px;
  1847. color: var(--dark-grey-3);
  1848. background-color: var(--light-grey-2);
  1849. flex-grow: 1;
  1850. height: 32px;
  1851. &:not(:first-of-type) {
  1852. margin-left: 5px;
  1853. }
  1854. }
  1855. .selected {
  1856. background-color: var(--primary-color) !important;
  1857. color: var(--white) !important;
  1858. font-weight: 600;
  1859. }
  1860. }
  1861. .tab {
  1862. border: 1px solid var(--light-grey-3);
  1863. border-radius: 0 0 5px 5px;
  1864. padding: 15px;
  1865. height: calc(100% - 32px);
  1866. overflow: auto;
  1867. }
  1868. }
  1869. }
  1870. .modal-card-foot .is-primary {
  1871. width: 200px;
  1872. }
  1873. /deep/ .autosuggest-container {
  1874. top: unset;
  1875. }
  1876. </style>