index.vue 51 KB

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