index.vue 53 KB

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