index.vue 49 KB

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