index.vue 53 KB

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