index.vue 58 KB

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