index.vue 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073
  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 validation from "@/validation";
  14. import keyboardShortcuts from "@/keyboardShortcuts";
  15. import { useForm } from "@/composables/useForm";
  16. import { Song } from "@/types/song.js";
  17. import { useWebsocketsStore } from "@/stores/websockets";
  18. import { useModalsStore } from "@/stores/modals";
  19. import { useEditSongStore } from "@/stores/editSong";
  20. import { useStationStore } from "@/stores/station";
  21. import { useUserAuthStore } from "@/stores/userAuth";
  22. import Modal from "@/components/Modal.vue";
  23. const FloatingBox = defineAsyncComponent(
  24. () => import("@/components/FloatingBox.vue")
  25. );
  26. const SaveButton = defineAsyncComponent(
  27. () => import("@/components/SaveButton.vue")
  28. );
  29. const AutoSuggest = defineAsyncComponent(
  30. () => import("@/components/AutoSuggest.vue")
  31. );
  32. const SongItem = defineAsyncComponent(
  33. () => import("@/components/SongItem.vue")
  34. );
  35. const Discogs = defineAsyncComponent(() => import("./Tabs/Discogs.vue"));
  36. const ReportsTab = defineAsyncComponent(() => import("./Tabs/Reports.vue"));
  37. const Youtube = defineAsyncComponent(() => import("./Tabs/Youtube.vue"));
  38. const MusareSongs = defineAsyncComponent(() => import("./Tabs/Songs.vue"));
  39. const SongThumbnail = defineAsyncComponent(
  40. () => import("@/components/SongThumbnail.vue")
  41. );
  42. const props = defineProps({
  43. modalUuid: { type: String, required: true },
  44. modalModulePath: {
  45. type: String,
  46. default: "modals/editSong/MODAL_UUID"
  47. },
  48. discogsAlbum: { type: Object, default: null },
  49. song: { type: Object, default: null },
  50. songs: { type: Array, default: null }
  51. });
  52. const editSongStore = useEditSongStore({ modalUuid: props.modalUuid });
  53. const stationStore = useStationStore();
  54. const { socket } = useWebsocketsStore();
  55. const userAuthStore = useUserAuthStore();
  56. const { openModal, closeCurrentModal, preventCloseCbs } = useModalsStore();
  57. const { hasPermission } = userAuthStore;
  58. const {
  59. tab,
  60. video,
  61. song,
  62. youtubeId,
  63. prefillData,
  64. reports,
  65. newSong,
  66. bulk,
  67. youtubeIds,
  68. songPrefillData
  69. } = storeToRefs(editSongStore);
  70. const songDataLoaded = ref(false);
  71. const songDeleted = ref(false);
  72. const youtubeError = ref(false);
  73. const youtubeErrorMessage = ref("");
  74. const youtubeVideoDuration = ref("0.000");
  75. const youtubeVideoCurrentTime = ref<number | string>(0);
  76. const youtubeVideoNote = ref("");
  77. const useHTTPS = ref(false);
  78. const muted = ref(false);
  79. const volumeSliderValue = ref(0);
  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 playerReady = ref(true);
  131. const interval = ref();
  132. const saveButtonRefs = ref([]);
  133. const canvasElement = ref();
  134. const genreHelper = ref();
  135. const saveButtonRefName = ref();
  136. // EditSongs
  137. const items = ref([]);
  138. const currentSong = ref<Song>({});
  139. const flagFilter = ref(false);
  140. const sidebarMobileActive = ref(false);
  141. const songItems = ref([]);
  142. const editingItemIndex = computed(() =>
  143. items.value.findIndex(
  144. item => item.song.youtubeId === currentSong.value.youtubeId
  145. )
  146. );
  147. const filteredItems = computed({
  148. get: () =>
  149. items.value.filter(item => (flagFilter.value ? item.flagged : true)),
  150. set: (newItem: any) => {
  151. const index = items.value.findIndex(
  152. item => item.song.youtubeId === newItem.youtubeId
  153. );
  154. items.value[index] = newItem;
  155. }
  156. });
  157. const filteredEditingItemIndex = computed(() =>
  158. filteredItems.value.findIndex(
  159. item => item.song.youtubeId === currentSong.value.youtubeId
  160. )
  161. );
  162. const currentSongFlagged = computed(
  163. () =>
  164. items.value.find(
  165. item => item.song.youtubeId === currentSong.value.youtubeId
  166. )?.flagged
  167. );
  168. // EditSongs end
  169. const {
  170. editSong,
  171. stopVideo,
  172. hardStopVideo,
  173. loadVideoById,
  174. pauseVideo,
  175. setSong,
  176. resetSong,
  177. updateReports,
  178. setPlaybackRate
  179. } = editSongStore;
  180. const { updateMediaModalPlayingAudio } = stationStore;
  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: string, reset?: boolean) => {
  198. songNotFound.value = false;
  199. socket.dispatch(`songs.getSongsFromYoutubeIds`, [_youtubeId], res => {
  200. const { songs } = res.data;
  201. if (res.status === "success" && songs.length > 0) {
  202. let _song = songs[0];
  203. _song = Object.assign(_song, prefillData.value);
  204. setSong(_song, reset);
  205. songDataLoaded.value = true;
  206. if (_song._id) {
  207. socket.dispatch("apis.joinRoom", `edit-song.${_song._id}`);
  208. socket.dispatch("reports.getReportsForSong", _song._id, res => {
  209. updateReports(res.data.reports);
  210. });
  211. newSong.value = false;
  212. }
  213. } else {
  214. new Toast("Song with that ID not found");
  215. if (bulk.value) songNotFound.value = true;
  216. if (!bulk.value) closeCurrentModal();
  217. }
  218. });
  219. };
  220. const onSavedSuccess = youtubeId => {
  221. const itemIndex = items.value.findIndex(
  222. item => item.song.youtubeId === youtubeId
  223. );
  224. if (itemIndex > -1) {
  225. items.value[itemIndex].status = "done";
  226. items.value[itemIndex].flagged = false;
  227. }
  228. };
  229. const onSavedError = youtubeId => {
  230. const itemIndex = items.value.findIndex(
  231. item => item.song.youtubeId === youtubeId
  232. );
  233. if (itemIndex > -1) items.value[itemIndex].status = "error";
  234. };
  235. const onSaving = youtubeId => {
  236. const itemIndex = items.value.findIndex(
  237. item => item.song.youtubeId === youtubeId
  238. );
  239. if (itemIndex > -1) items.value[itemIndex].status = "saving";
  240. };
  241. const { inputs, unsavedChanges, save, setValue, setOriginalValue } = useForm(
  242. {
  243. title: {
  244. value: "",
  245. validate: value => {
  246. if (!validation.isLength(value, 1, 100))
  247. return "Title must have between 1 and 100 characters.";
  248. return true;
  249. }
  250. },
  251. duration: {
  252. value: 0,
  253. validate: value => {
  254. if (
  255. Number(inputs.value.skipDuration.value) + Number(value) >
  256. Number.parseInt(youtubeVideoDuration.value) &&
  257. (((!newSong.value || bulk.value) && !youtubeError.value) ||
  258. inputs.value.duration.originalValue !== value)
  259. )
  260. return "Duration can't be higher than the length of the video";
  261. return true;
  262. }
  263. },
  264. skipDuration: 0,
  265. thumbnail: {
  266. value: "",
  267. validate: value => {
  268. if (!validation.isLength(value, 8, 256))
  269. return "Thumbnail must have between 8 and 256 characters.";
  270. if (useHTTPS.value && value.indexOf("https://") !== 0)
  271. return 'Thumbnail must start with "https://".';
  272. if (
  273. !useHTTPS.value &&
  274. value.indexOf("https://") !== 0 &&
  275. value.indexOf("http://") !== 0
  276. )
  277. return 'Thumbnail must start with "http(s)://".';
  278. return true;
  279. }
  280. },
  281. youtubeId: {
  282. value: "",
  283. validate: value => {
  284. if (
  285. !newSong.value &&
  286. youtubeError.value &&
  287. inputs.value.youtubeId.originalValue !== value
  288. )
  289. return "You're not allowed to change the YouTube id while the player is not working";
  290. return true;
  291. }
  292. },
  293. verified: false,
  294. addArtist: {
  295. value: "",
  296. ignoreUnsaved: true,
  297. required: false
  298. },
  299. artists: {
  300. value: [],
  301. validate: value => {
  302. if (
  303. (inputs.value.verified.value && value.length < 1) ||
  304. value.length > 10
  305. )
  306. return "Invalid artists. You must have at least 1 artist and a maximum of 10 artists.";
  307. let error;
  308. value.forEach(artist => {
  309. if (!validation.isLength(artist, 1, 64))
  310. error = "Artist must have between 1 and 64 characters.";
  311. if (artist === "NONE")
  312. error =
  313. 'Invalid artist format. Artists are not allowed to be named "NONE".';
  314. });
  315. return error || true;
  316. }
  317. },
  318. addGenre: {
  319. value: "",
  320. ignoreUnsaved: true,
  321. required: false
  322. },
  323. genres: {
  324. value: [],
  325. validate: value => {
  326. if (
  327. (inputs.value.verified.value && value.length < 1) ||
  328. value.length > 16
  329. )
  330. return "Invalid genres. You must have between 1 and 16 genres.";
  331. let error;
  332. value.forEach(genre => {
  333. if (!validation.isLength(genre, 1, 32))
  334. error = "Genre must have between 1 and 32 characters.";
  335. if (!validation.regex.ascii.test(genre))
  336. error =
  337. "Invalid genre format. Only ascii characters are allowed.";
  338. });
  339. return error || true;
  340. }
  341. },
  342. addTag: {
  343. value: "",
  344. ignoreUnsaved: true,
  345. required: false
  346. },
  347. tags: {
  348. value: [],
  349. validate: value => {
  350. let error;
  351. value.forEach(tag => {
  352. if (
  353. !/^[a-zA-Z0-9_]{1,64}$|^[a-zA-Z0-9_]{1,64}\[[a-zA-Z0-9_]{1,64}\]$/.test(
  354. tag
  355. )
  356. )
  357. error = "Invalid tag format.";
  358. });
  359. return error || true;
  360. }
  361. },
  362. discogs: {
  363. value: {},
  364. required: false
  365. }
  366. },
  367. ({ status, messages, values }, resolve, reject) => {
  368. const saveButtonRef = saveButtonRefs.value[saveButtonRefName.value];
  369. if (status === "success" || (status === "unchanged" && newSong.value)) {
  370. const mergedValues = Object.assign(song.value, values);
  371. const cb = res => {
  372. if (res.status === "error") {
  373. reject(new Error(res.message));
  374. return;
  375. }
  376. new Toast(res.message);
  377. saveButtonRef.handleSuccessfulSave();
  378. onSavedSuccess(values.youtubeId);
  379. if (newSong.value) loadSong(values.youtubeId, true);
  380. else setSong(mergedValues);
  381. resolve();
  382. };
  383. if (newSong.value)
  384. socket.dispatch("songs.create", mergedValues, cb);
  385. else
  386. socket.dispatch(
  387. "songs.update",
  388. song.value._id,
  389. mergedValues,
  390. cb
  391. );
  392. } else {
  393. if (status === "unchanged") {
  394. new Toast(messages.unchanged);
  395. saveButtonRef.handleSuccessfulSave();
  396. onSavedSuccess(values.youtubeId);
  397. } else {
  398. Object.values(messages).forEach(message => {
  399. new Toast({ content: message, timeout: 8000 });
  400. });
  401. saveButtonRef.handleFailedSave();
  402. onSavedError(values.youtubeId);
  403. }
  404. resolve();
  405. }
  406. },
  407. { modalUuid: props.modalUuid, preventCloseUnsaved: false }
  408. );
  409. const showTab = payload => {
  410. if (tabs.value[`${payload}-tab`])
  411. tabs.value[`${payload}-tab`].scrollIntoView({ block: "nearest" });
  412. editSongStore.showTab(payload);
  413. };
  414. const toggleDone = (index, overwrite = null) => {
  415. const { status } = filteredItems.value[index];
  416. if (status === "done" && overwrite !== "done")
  417. filteredItems.value[index].status = "todo";
  418. else {
  419. filteredItems.value[index].status = "done";
  420. filteredItems.value[index].flagged = false;
  421. }
  422. };
  423. const toggleFlagFilter = () => {
  424. flagFilter.value = !flagFilter.value;
  425. };
  426. const toggleMobileSidebar = () => {
  427. sidebarMobileActive.value = !sidebarMobileActive.value;
  428. };
  429. const onCloseOrNext = (next?: boolean): Promise<void> =>
  430. new Promise(resolve => {
  431. const confirmReasons = [];
  432. if (unsavedChanges.value.length > 0) {
  433. confirmReasons.push(
  434. "You have unsaved changes. Are you sure you want to discard unsaved changes?"
  435. );
  436. }
  437. if (!next && bulk.value) {
  438. const doneItems = items.value.filter(
  439. item => item.status === "done"
  440. ).length;
  441. const flaggedItems = items.value.filter(
  442. item => item.flagged
  443. ).length;
  444. const notDoneItems = items.value.length - doneItems;
  445. if (doneItems > 0 && notDoneItems > 0)
  446. confirmReasons.push(
  447. "You have songs which are not done yet. Are you sure you want to stop editing songs?"
  448. );
  449. else if (flaggedItems > 0)
  450. confirmReasons.push(
  451. "You have songs which are flagged. Are you sure you want to stop editing songs?"
  452. );
  453. }
  454. if (confirmReasons.length > 0)
  455. openModal({
  456. modal: "confirm",
  457. props: {
  458. message: confirmReasons,
  459. onCompleted: resolve
  460. }
  461. });
  462. else resolve();
  463. });
  464. const pickSong = song => {
  465. onCloseOrNext(true).then(() => {
  466. editSong({
  467. youtubeId: song.youtubeId,
  468. prefill: songPrefillData.value[song.youtubeId]
  469. });
  470. currentSong.value = song;
  471. if (songItems.value[`edit-songs-item-${song.youtubeId}`])
  472. songItems.value[
  473. `edit-songs-item-${song.youtubeId}`
  474. ].scrollIntoView();
  475. });
  476. };
  477. const editNextSong = () => {
  478. const currentlyEditingSongIndex = filteredEditingItemIndex.value;
  479. let newEditingSongIndex = -1;
  480. const index =
  481. currentlyEditingSongIndex + 1 === filteredItems.value.length
  482. ? 0
  483. : currentlyEditingSongIndex + 1;
  484. for (let i = index; i < filteredItems.value.length; i += 1) {
  485. if (!flagFilter.value || filteredItems.value[i].flagged) {
  486. newEditingSongIndex = i;
  487. break;
  488. }
  489. }
  490. if (newEditingSongIndex > -1) {
  491. const nextSong = filteredItems.value[newEditingSongIndex].song;
  492. if (nextSong.removed) editNextSong();
  493. else pickSong(nextSong);
  494. }
  495. };
  496. const saveSong = (refName: string, closeOrNext?: boolean) => {
  497. saveButtonRefName.value = refName;
  498. onSaving(inputs.value.youtubeId.value);
  499. save(() => {
  500. if (closeOrNext && bulk.value) editNextSong();
  501. else if (closeOrNext) closeCurrentModal();
  502. });
  503. };
  504. const toggleFlag = (songIndex = null) => {
  505. if (songIndex && songIndex > -1) {
  506. filteredItems.value[songIndex].flagged =
  507. !filteredItems.value[songIndex].flagged;
  508. new Toast(
  509. `Successfully ${
  510. filteredItems.value[songIndex].flagged ? "flagged" : "unflagged"
  511. } song.`
  512. );
  513. } else if (!songIndex && editingItemIndex.value > -1) {
  514. items.value[editingItemIndex.value].flagged =
  515. !items.value[editingItemIndex.value].flagged;
  516. new Toast(
  517. `Successfully ${
  518. items.value[editingItemIndex.value].flagged
  519. ? "flagged"
  520. : "unflagged"
  521. } song.`
  522. );
  523. }
  524. };
  525. const onThumbnailLoad = () => {
  526. if (thumbnailElement.value) {
  527. const height = thumbnailElement.value.naturalHeight;
  528. const width = thumbnailElement.value.naturalWidth;
  529. thumbnailNotSquare.value = height !== width;
  530. thumbnailHeight.value = height;
  531. thumbnailWidth.value = width;
  532. } else {
  533. thumbnailNotSquare.value = false;
  534. thumbnailHeight.value = null;
  535. thumbnailWidth.value = null;
  536. }
  537. };
  538. const onThumbnailLoadError = error => {
  539. thumbnailLoadError.value = error !== 0;
  540. };
  541. const isYoutubeThumbnail = computed(
  542. () =>
  543. songDataLoaded.value &&
  544. inputs.value.youtubeId.value &&
  545. inputs.value.thumbnail.value &&
  546. (inputs.value.thumbnail.value.lastIndexOf("i.ytimg.com") !== -1 ||
  547. inputs.value.thumbnail.value.lastIndexOf("img.youtube.com") !== -1)
  548. );
  549. const drawCanvas = () => {
  550. if (!songDataLoaded.value || !canvasElement.value) return;
  551. const ctx = canvasElement.value.getContext("2d");
  552. const videoDuration = Number(youtubeVideoDuration.value);
  553. const skipDuration = Number(inputs.value.skipDuration.value);
  554. const duration = Number(inputs.value.duration.value);
  555. const afterDuration = videoDuration - (skipDuration + duration);
  556. const width = 530;
  557. const currentTime =
  558. video.value.player && video.value.player.getCurrentTime
  559. ? video.value.player.getCurrentTime()
  560. : 0;
  561. const widthSkipDuration = (skipDuration / videoDuration) * width;
  562. const widthDuration = (duration / videoDuration) * width;
  563. const widthAfterDuration = (afterDuration / videoDuration) * width;
  564. const widthCurrentTime = (currentTime / videoDuration) * width;
  565. const skipDurationColor = "#F42003";
  566. const durationColor = "#03A9F4";
  567. const afterDurationColor = "#41E841";
  568. const currentDurationColor = "#3b25e8";
  569. ctx.fillStyle = skipDurationColor;
  570. ctx.fillRect(0, 0, widthSkipDuration, 20);
  571. ctx.fillStyle = durationColor;
  572. ctx.fillRect(widthSkipDuration, 0, widthDuration, 20);
  573. ctx.fillStyle = afterDurationColor;
  574. ctx.fillRect(widthSkipDuration + widthDuration, 0, widthAfterDuration, 20);
  575. ctx.fillStyle = currentDurationColor;
  576. ctx.fillRect(widthCurrentTime, 0, 1, 20);
  577. };
  578. const seekTo = position => {
  579. pauseVideo(false);
  580. video.value.player.seekTo(position);
  581. };
  582. const getAlbumData = type => {
  583. if (!inputs.value.discogs.value) return;
  584. if (type === "title")
  585. setValue({ title: inputs.value.discogs.value.track.title });
  586. if (type === "albumArt")
  587. setValue({ thumbnail: inputs.value.discogs.value.album.albumArt });
  588. if (type === "genres")
  589. setValue({
  590. genres: JSON.parse(
  591. JSON.stringify(inputs.value.discogs.value.album.genres)
  592. )
  593. });
  594. if (type === "artists")
  595. setValue({
  596. artists: JSON.parse(
  597. JSON.stringify(inputs.value.discogs.value.album.artists)
  598. )
  599. });
  600. };
  601. const getYouTubeData = type => {
  602. if (type === "title") {
  603. try {
  604. const { title } = video.value.player.getVideoData();
  605. if (title) setValue({ title });
  606. else throw new Error("No title found");
  607. } catch (e) {
  608. new Toast(
  609. "Unable to fetch YouTube video title. Try starting the video."
  610. );
  611. }
  612. }
  613. if (type === "thumbnail")
  614. setValue({
  615. thumbnail: `https://img.youtube.com/vi/${inputs.value.youtubeId.value}/mqdefault.jpg`
  616. });
  617. if (type === "author") {
  618. try {
  619. const { author } = video.value.player.getVideoData();
  620. if (author) setValue({ addArtist: author });
  621. else throw new Error("No video author found");
  622. } catch (e) {
  623. new Toast(
  624. "Unable to fetch YouTube video author. Try starting the video."
  625. );
  626. }
  627. }
  628. };
  629. const fillDuration = () => {
  630. setValue({
  631. duration:
  632. Number.parseInt(youtubeVideoDuration.value) -
  633. inputs.value.skipDuration.value
  634. });
  635. };
  636. const settings = type => {
  637. switch (type) {
  638. case "stop":
  639. stopVideo();
  640. pauseVideo(true);
  641. break;
  642. case "hardStop":
  643. hardStopVideo();
  644. pauseVideo(true);
  645. break;
  646. case "pause":
  647. pauseVideo(true);
  648. break;
  649. case "play":
  650. pauseVideo(false);
  651. break;
  652. case "skipToLast10Secs":
  653. seekTo(
  654. inputs.value.duration.value -
  655. 10 +
  656. inputs.value.skipDuration.value
  657. );
  658. break;
  659. default:
  660. break;
  661. }
  662. };
  663. const play = () => {
  664. if (
  665. video.value.player.getVideoData().video_id !==
  666. inputs.value.youtubeId.value
  667. ) {
  668. setValue({ duration: -1 });
  669. loadVideoById(
  670. inputs.value.youtubeId.value,
  671. inputs.value.skipDuration.value
  672. );
  673. }
  674. settings("play");
  675. };
  676. const changeVolume = () => {
  677. const volume = volumeSliderValue.value;
  678. localStorage.setItem("volume", `${volume}`);
  679. video.value.player.setVolume(volume);
  680. if (volume > 0) {
  681. video.value.player.unMute();
  682. muted.value = false;
  683. }
  684. };
  685. const toggleMute = () => {
  686. const previousVolume = parseFloat(localStorage.getItem("volume"));
  687. const volume = video.value.player.getVolume() <= 0 ? previousVolume : 0;
  688. muted.value = !muted.value;
  689. volumeSliderValue.value = volume;
  690. video.value.player.setVolume(volume);
  691. if (!muted.value) localStorage.setItem("volume", `${volume}`);
  692. };
  693. const addTag = (type, value?) => {
  694. if (type === "genres") {
  695. const genre = value || inputs.value.addGenre.value.trim();
  696. if (
  697. inputs.value.genres.value
  698. .map(genre => genre.toLowerCase())
  699. .indexOf(genre.toLowerCase()) !== -1
  700. )
  701. return new Toast("Genre already exists");
  702. if (genre) {
  703. inputs.value.genres.value.push(genre);
  704. setValue({ addGenre: "" });
  705. return false;
  706. }
  707. return new Toast("Genre cannot be empty");
  708. }
  709. if (type === "artists") {
  710. const artist = value || inputs.value.addArtist.value.trim();
  711. if (inputs.value.artists.value.indexOf(artist) !== -1)
  712. return new Toast("Artist already exists");
  713. if (artist !== "") {
  714. inputs.value.artists.value.push(artist);
  715. setValue({ addArtist: "" });
  716. return false;
  717. }
  718. return new Toast("Artist cannot be empty");
  719. }
  720. if (type === "tags") {
  721. const tag = value || inputs.value.addTag.value.trim();
  722. if (inputs.value.tags.value.indexOf(tag) !== -1)
  723. return new Toast("Tag already exists");
  724. if (tag !== "") {
  725. inputs.value.tags.value.push(tag);
  726. setValue({ addTag: "" });
  727. return false;
  728. }
  729. return new Toast("Tag cannot be empty");
  730. }
  731. return false;
  732. };
  733. const removeTag = (type, value) => {
  734. if (type === "genres")
  735. inputs.value.genres.value.splice(
  736. inputs.value.genres.value.indexOf(value),
  737. 1
  738. );
  739. else if (type === "artists")
  740. inputs.value.artists.value.splice(
  741. inputs.value.artists.value.indexOf(value),
  742. 1
  743. );
  744. else if (type === "tags")
  745. inputs.value.tags.value.splice(
  746. inputs.value.tags.value.indexOf(value),
  747. 1
  748. );
  749. };
  750. const setTrackPosition = event => {
  751. seekTo(
  752. Number(
  753. Number(video.value.player.getDuration()) *
  754. ((event.pageX - event.target.getBoundingClientRect().left) /
  755. 530)
  756. )
  757. );
  758. };
  759. const toggleGenreHelper = () => {
  760. genreHelper.value.toggleBox();
  761. };
  762. const resetGenreHelper = () => {
  763. genreHelper.value.resetBox();
  764. };
  765. const sendActivityWatchVideoData = () => {
  766. if (!video.value.paused) {
  767. if (activityWatchVideoLastStatus.value !== "playing") {
  768. activityWatchVideoLastStatus.value = "playing";
  769. if (
  770. inputs.value.skipDuration.value > 0 &&
  771. Number(youtubeVideoCurrentTime.value) === 0
  772. ) {
  773. activityWatchVideoLastStartDuration.value = Math.floor(
  774. inputs.value.skipDuration.value +
  775. Number(youtubeVideoCurrentTime.value)
  776. );
  777. } else {
  778. activityWatchVideoLastStartDuration.value = Math.floor(
  779. Number(youtubeVideoCurrentTime.value)
  780. );
  781. }
  782. }
  783. const videoData = {
  784. title: inputs.value.title.value,
  785. artists: inputs.value.artists.value
  786. ? inputs.value.artists.value.join(", ")
  787. : null,
  788. youtubeId: inputs.value.youtubeId.value,
  789. muted: muted.value,
  790. volume: volumeSliderValue.value,
  791. startedDuration:
  792. activityWatchVideoLastStartDuration.value <= 0
  793. ? 0
  794. : activityWatchVideoLastStartDuration.value,
  795. source: `editSong#${inputs.value.youtubeId.value}`,
  796. hostname: window.location.hostname
  797. };
  798. aw.sendVideoData(videoData);
  799. } else {
  800. activityWatchVideoLastStatus.value = "not_playing";
  801. }
  802. };
  803. const remove = id => {
  804. socket.dispatch("songs.remove", id, res => {
  805. new Toast(res.message);
  806. });
  807. };
  808. watch(
  809. [() => inputs.value.duration.value, () => inputs.value.skipDuration.value],
  810. () => drawCanvas()
  811. );
  812. watch(youtubeId, (_youtubeId, _oldYoutubeId) => {
  813. if (_oldYoutubeId) unloadSong(_oldYoutubeId);
  814. if (_youtubeId) loadSong(_youtubeId, true);
  815. });
  816. watch(
  817. () => inputs.value.youtubeId.value,
  818. value => {
  819. if (video.value.player && video.value.player.cueVideoById)
  820. video.value.player.cueVideoById(
  821. value,
  822. inputs.value.skipDuration.value
  823. );
  824. }
  825. );
  826. watch(
  827. () => hasPermission("songs.update"),
  828. value => {
  829. if (!value) closeCurrentModal(true);
  830. }
  831. );
  832. onMounted(async () => {
  833. editSongStore.init({ song: props.song, songs: props.songs });
  834. editSongStore.form = {
  835. inputs,
  836. unsavedChanges,
  837. save,
  838. setValue,
  839. setOriginalValue
  840. };
  841. preventCloseCbs[props.modalUuid] = onCloseOrNext;
  842. activityWatchVideoDataInterval.value = setInterval(() => {
  843. sendActivityWatchVideoData();
  844. }, 1000);
  845. useHTTPS.value = await lofig.get("cookie.secure");
  846. socket.onConnect(() => {
  847. if (newSong.value && !youtubeId.value && !bulk.value) {
  848. setSong({
  849. youtubeId: "",
  850. title: "",
  851. artists: [],
  852. genres: [],
  853. tags: [],
  854. duration: 0,
  855. skipDuration: 0,
  856. thumbnail: "",
  857. verified: false
  858. });
  859. songDataLoaded.value = true;
  860. showTab("youtube");
  861. } else if (youtubeId.value) loadSong(youtubeId.value);
  862. else if (!bulk.value) {
  863. new Toast("You can't open EditSong without editing a song");
  864. return closeCurrentModal();
  865. }
  866. interval.value = setInterval(() => {
  867. if (
  868. inputs.value.duration.value !== -1 &&
  869. video.value.paused === false &&
  870. playerReady.value &&
  871. (video.value.player.getCurrentTime() -
  872. inputs.value.skipDuration.value >
  873. inputs.value.duration.value ||
  874. (video.value.player.getCurrentTime() > 0 &&
  875. video.value.player.getCurrentTime() >=
  876. video.value.player.getDuration()))
  877. ) {
  878. stopVideo();
  879. pauseVideo(true);
  880. drawCanvas();
  881. }
  882. if (
  883. playerReady.value &&
  884. video.value.player.getVideoData &&
  885. video.value.player.getVideoData() &&
  886. video.value.player.getVideoData().video_id ===
  887. inputs.value.youtubeId.value
  888. ) {
  889. const currentTime = video.value.player.getCurrentTime();
  890. if (currentTime !== undefined)
  891. youtubeVideoCurrentTime.value = currentTime.toFixed(3);
  892. if (youtubeVideoDuration.value.indexOf(".000") !== -1) {
  893. const duration = video.value.player.getDuration();
  894. if (duration !== undefined) {
  895. if (
  896. `${youtubeVideoDuration.value}` ===
  897. `${Number(inputs.value.duration.value).toFixed(3)}`
  898. )
  899. setValue({ duration: duration.toFixed(3) });
  900. youtubeVideoDuration.value = duration.toFixed(3);
  901. if (youtubeVideoDuration.value.indexOf(".000") !== -1)
  902. youtubeVideoNote.value = "(~)";
  903. else youtubeVideoNote.value = "";
  904. drawCanvas();
  905. }
  906. }
  907. }
  908. if (video.value.paused === false) drawCanvas();
  909. }, 200);
  910. if (window.YT && window.YT.Player) {
  911. video.value.player = new window.YT.Player(
  912. `editSongPlayer-${props.modalUuid}`,
  913. {
  914. height: 298,
  915. width: 530,
  916. videoId: null,
  917. host: "https://www.youtube-nocookie.com",
  918. playerVars: {
  919. controls: 0,
  920. iv_load_policy: 3,
  921. rel: 0,
  922. showinfo: 0,
  923. autoplay: 0
  924. },
  925. startSeconds: inputs.value.skipDuration.value,
  926. events: {
  927. onReady: () => {
  928. let volume = parseFloat(
  929. localStorage.getItem("volume")
  930. );
  931. volume = typeof volume === "number" ? volume : 20;
  932. video.value.player.setVolume(volume);
  933. if (volume > 0) video.value.player.unMute();
  934. playerReady.value = true;
  935. if (inputs.value.youtubeId.value)
  936. video.value.player.cueVideoById(
  937. inputs.value.youtubeId.value,
  938. inputs.value.skipDuration.value
  939. );
  940. setPlaybackRate(null);
  941. drawCanvas();
  942. },
  943. onStateChange: event => {
  944. drawCanvas();
  945. if (event.data === 1) {
  946. video.value.paused = false;
  947. updateMediaModalPlayingAudio(true);
  948. let youtubeDuration =
  949. video.value.player.getDuration();
  950. const newYoutubeVideoDuration =
  951. youtubeDuration.toFixed(3);
  952. if (
  953. youtubeVideoDuration.value.indexOf(
  954. ".000"
  955. ) !== -1 &&
  956. `${youtubeVideoDuration.value}` !==
  957. `${newYoutubeVideoDuration}`
  958. ) {
  959. const songDurationNumber = Number(
  960. inputs.value.duration.value
  961. );
  962. const songDurationNumber2 =
  963. Number(inputs.value.duration.value) + 1;
  964. const songDurationNumber3 =
  965. Number(inputs.value.duration.value) - 1;
  966. const fixedSongDuration =
  967. songDurationNumber.toFixed(3);
  968. const fixedSongDuration2 =
  969. songDurationNumber2.toFixed(3);
  970. const fixedSongDuration3 =
  971. songDurationNumber3.toFixed(3);
  972. if (
  973. `${youtubeVideoDuration.value}` ===
  974. `${Number(
  975. inputs.value.duration.value
  976. ).toFixed(3)}` &&
  977. (fixedSongDuration ===
  978. youtubeVideoDuration.value ||
  979. fixedSongDuration2 ===
  980. youtubeVideoDuration.value ||
  981. fixedSongDuration3 ===
  982. youtubeVideoDuration.value)
  983. )
  984. setValue({
  985. duration: newYoutubeVideoDuration
  986. });
  987. youtubeVideoDuration.value =
  988. newYoutubeVideoDuration;
  989. if (
  990. youtubeVideoDuration.value.indexOf(
  991. ".000"
  992. ) !== -1
  993. )
  994. youtubeVideoNote.value = "(~)";
  995. else youtubeVideoNote.value = "";
  996. }
  997. if (inputs.value.duration.value === -1)
  998. setValue({
  999. duration: Number.parseInt(
  1000. youtubeVideoDuration.value
  1001. )
  1002. });
  1003. youtubeDuration -=
  1004. inputs.value.skipDuration.value;
  1005. if (
  1006. inputs.value.duration.value >
  1007. youtubeDuration + 1
  1008. ) {
  1009. stopVideo();
  1010. pauseVideo(true);
  1011. return new Toast(
  1012. "Video can't play. Specified duration is bigger than the YouTube song duration."
  1013. );
  1014. }
  1015. if (inputs.value.duration.value <= 0) {
  1016. stopVideo();
  1017. pauseVideo(true);
  1018. return new Toast(
  1019. "Video can't play. Specified duration has to be more than 0 seconds."
  1020. );
  1021. }
  1022. if (
  1023. video.value.player.getCurrentTime() <
  1024. inputs.value.skipDuration.value
  1025. ) {
  1026. return seekTo(
  1027. inputs.value.skipDuration.value
  1028. );
  1029. }
  1030. setPlaybackRate(null);
  1031. } else if (event.data === 2) {
  1032. video.value.paused = true;
  1033. updateMediaModalPlayingAudio(false);
  1034. }
  1035. return false;
  1036. }
  1037. }
  1038. }
  1039. );
  1040. } else {
  1041. youtubeError.value = true;
  1042. youtubeErrorMessage.value = "Player could not be loaded.";
  1043. }
  1044. ["artists", "genres", "tags"].forEach(type => {
  1045. socket.dispatch(
  1046. `songs.get${type.charAt(0).toUpperCase()}${type.slice(1)}`,
  1047. res => {
  1048. if (res.status === "success") {
  1049. const { items } = res.data;
  1050. if (type === "genres")
  1051. autosuggest.value.allItems[type] = Array.from(
  1052. new Set([...recommendedGenres.value, ...items])
  1053. );
  1054. else autosuggest.value.allItems[type] = items;
  1055. } else {
  1056. new Toast(res.message);
  1057. }
  1058. }
  1059. );
  1060. });
  1061. socket.on(
  1062. `event:admin.song.updated`,
  1063. res => {
  1064. if (song.value._id === res.data.song._id)
  1065. setOriginalValue({
  1066. title: res.data.song.title,
  1067. duration: res.data.song.duration,
  1068. skipDuration: res.data.song.skipDuration,
  1069. thumbnail: res.data.song.thumbnail,
  1070. youtubeId: res.data.song.youtubeId,
  1071. verified: res.data.song.verified,
  1072. artists: res.data.song.artists,
  1073. genres: res.data.song.genres,
  1074. tags: res.data.song.tags,
  1075. discogs: res.data.song.discogs
  1076. });
  1077. if (bulk.value) {
  1078. const index = items.value
  1079. .map(item => item.song.youtubeId)
  1080. .indexOf(res.data.song.youtubeId);
  1081. if (index >= 0)
  1082. items.value[index].song = {
  1083. ...items.value[index].song,
  1084. ...res.data.song,
  1085. updated: true
  1086. };
  1087. }
  1088. },
  1089. { modalUuid: props.modalUuid }
  1090. );
  1091. socket.on(
  1092. "event:admin.song.removed",
  1093. res => {
  1094. if (res.data.songId === song.value._id) {
  1095. songDeleted.value = true;
  1096. if (!bulk.value) closeCurrentModal(true);
  1097. }
  1098. },
  1099. { modalUuid: props.modalUuid }
  1100. );
  1101. if (bulk.value) {
  1102. socket.dispatch("apis.joinRoom", "edit-songs");
  1103. socket.dispatch(
  1104. "songs.getSongsFromYoutubeIds",
  1105. youtubeIds.value,
  1106. res => {
  1107. if (res.data.songs.length === 0) {
  1108. closeCurrentModal();
  1109. new Toast("You can't edit 0 songs.");
  1110. } else {
  1111. items.value = res.data.songs.map(song => ({
  1112. status: "todo",
  1113. flagged: false,
  1114. song
  1115. }));
  1116. editNextSong();
  1117. }
  1118. }
  1119. );
  1120. socket.on(
  1121. `event:admin.song.created`,
  1122. res => {
  1123. const index = items.value
  1124. .map(item => item.song.youtubeId)
  1125. .indexOf(res.data.song.youtubeId);
  1126. if (index >= 0)
  1127. items.value[index].song = {
  1128. ...items.value[index].song,
  1129. ...res.data.song,
  1130. created: true
  1131. };
  1132. },
  1133. { modalUuid: props.modalUuid }
  1134. );
  1135. socket.on(
  1136. `event:admin.song.removed`,
  1137. res => {
  1138. const index = items.value
  1139. .map(item => item.song._id)
  1140. .indexOf(res.data.songId);
  1141. if (index >= 0) items.value[index].song.removed = true;
  1142. },
  1143. { modalUuid: props.modalUuid }
  1144. );
  1145. socket.on(
  1146. `event:admin.youtubeVideo.removed`,
  1147. res => {
  1148. const index = items.value
  1149. .map(item => item.song.youtubeVideoId)
  1150. .indexOf(res.videoId);
  1151. if (index >= 0) items.value[index].song.removed = true;
  1152. },
  1153. { modalUuid: props.modalUuid }
  1154. );
  1155. }
  1156. return null;
  1157. });
  1158. let volume = parseFloat(localStorage.getItem("volume"));
  1159. volume = typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
  1160. localStorage.setItem("volume", `${volume}`);
  1161. volumeSliderValue.value = volume;
  1162. keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
  1163. keyCode: 101,
  1164. preventDefault: true,
  1165. handler: () => {
  1166. if (video.value.paused) play();
  1167. else settings("pause");
  1168. }
  1169. });
  1170. keyboardShortcuts.registerShortcut("editSong.stopVideo", {
  1171. keyCode: 101,
  1172. ctrl: true,
  1173. preventDefault: true,
  1174. handler: () => {
  1175. settings("stop");
  1176. }
  1177. });
  1178. keyboardShortcuts.registerShortcut("editSong.hardStopVideo", {
  1179. keyCode: 101,
  1180. ctrl: true,
  1181. shift: true,
  1182. preventDefault: true,
  1183. handler: () => {
  1184. settings("hardStop");
  1185. }
  1186. });
  1187. keyboardShortcuts.registerShortcut("editSong.skipToLast10Secs", {
  1188. keyCode: 102,
  1189. preventDefault: true,
  1190. handler: () => {
  1191. settings("skipToLast10Secs");
  1192. }
  1193. });
  1194. keyboardShortcuts.registerShortcut("editSong.lowerVolumeLarge", {
  1195. keyCode: 98,
  1196. preventDefault: true,
  1197. handler: () => {
  1198. volumeSliderValue.value = Math.max(0, volumeSliderValue.value - 10);
  1199. changeVolume();
  1200. }
  1201. });
  1202. keyboardShortcuts.registerShortcut("editSong.lowerVolumeSmall", {
  1203. keyCode: 98,
  1204. ctrl: true,
  1205. preventDefault: true,
  1206. handler: () => {
  1207. volumeSliderValue.value = Math.max(0, volumeSliderValue.value - 1);
  1208. changeVolume();
  1209. }
  1210. });
  1211. keyboardShortcuts.registerShortcut("editSong.increaseVolumeLarge", {
  1212. keyCode: 104,
  1213. preventDefault: true,
  1214. handler: () => {
  1215. volumeSliderValue.value = Math.min(
  1216. 100,
  1217. volumeSliderValue.value + 10
  1218. );
  1219. changeVolume();
  1220. }
  1221. });
  1222. keyboardShortcuts.registerShortcut("editSong.increaseVolumeSmall", {
  1223. keyCode: 104,
  1224. ctrl: true,
  1225. preventDefault: true,
  1226. handler: () => {
  1227. volumeSliderValue.value = Math.min(
  1228. 100,
  1229. volumeSliderValue.value + 1
  1230. );
  1231. changeVolume();
  1232. }
  1233. });
  1234. keyboardShortcuts.registerShortcut("editSong.save", {
  1235. keyCode: 83,
  1236. ctrl: true,
  1237. preventDefault: true,
  1238. handler: () => {
  1239. saveSong("saveButton");
  1240. }
  1241. });
  1242. keyboardShortcuts.registerShortcut("editSong.saveClose", {
  1243. keyCode: 83,
  1244. ctrl: true,
  1245. alt: true,
  1246. preventDefault: true,
  1247. handler: () => {
  1248. saveSong("saveAndCloseButton", true);
  1249. }
  1250. });
  1251. keyboardShortcuts.registerShortcut("editSong.focusTitle", {
  1252. keyCode: 36,
  1253. preventDefault: true,
  1254. handler: () => {
  1255. inputs.value["title-input"].focus();
  1256. }
  1257. });
  1258. keyboardShortcuts.registerShortcut("editSong.useAllDiscogs", {
  1259. keyCode: 68,
  1260. alt: true,
  1261. ctrl: true,
  1262. preventDefault: true,
  1263. handler: () => {
  1264. getAlbumData("title");
  1265. getAlbumData("albumArt");
  1266. getAlbumData("artists");
  1267. getAlbumData("genres");
  1268. }
  1269. });
  1270. /*
  1271. editSong.pauseResume - Num 5 - Pause/resume song
  1272. editSong.stopVideo - Ctrl - Num 5 - Stop
  1273. editSong.hardStopVideo - Shift - Ctrl - Num 5 - Stop
  1274. editSong.skipToLast10Secs - Num 6 - Skip to last 10 seconds
  1275. editSong.lowerVolumeLarge - Num 2 - Volume down by 10
  1276. editSong.lowerVolumeSmall - Ctrl - Num 2 - Volume down by 1
  1277. editSong.increaseVolumeLarge - Num 8 - Volume up by 10
  1278. editSong.increaseVolumeSmall - Ctrl - Num 8 - Volume up by 1
  1279. editSong.focusTitle - Home - Focus the title input
  1280. editSong.focusDicogs - End - Focus the discogs input
  1281. editSong.save - Ctrl - S - Saves song
  1282. editSong.save - Ctrl - Alt - S - Saves song and closes the modal
  1283. editSong.save - Ctrl - Alt - V - Saves song, verifies songs and then closes the modal
  1284. editSong.close - F4 - Closes modal without saving
  1285. editSong.useAllDiscogs - Ctrl - Alt - D - Sets all fields to the Discogs data
  1286. Inside Discogs inputs: Ctrl - D - Sets this field to the Discogs data
  1287. */
  1288. });
  1289. onBeforeUnmount(() => {
  1290. if (bulk.value) {
  1291. socket.dispatch("apis.leaveRoom", "edit-songs");
  1292. }
  1293. unloadSong(youtubeId.value, song.value._id);
  1294. updateMediaModalPlayingAudio(false);
  1295. playerReady.value = false;
  1296. clearInterval(interval.value);
  1297. clearInterval(activityWatchVideoDataInterval.value);
  1298. const shortcutNames = [
  1299. "editSong.pauseResume",
  1300. "editSong.stopVideo",
  1301. "editSong.hardStopVideo",
  1302. "editSong.skipToLast10Secs",
  1303. "editSong.lowerVolumeLarge",
  1304. "editSong.lowerVolumeSmall",
  1305. "editSong.increaseVolumeLarge",
  1306. "editSong.increaseVolumeSmall",
  1307. "editSong.focusTitle",
  1308. "editSong.focusDicogs",
  1309. "editSong.save",
  1310. "editSong.saveClose",
  1311. "editSong.useAllDiscogs",
  1312. "editSong.closeModal"
  1313. ];
  1314. shortcutNames.forEach(shortcutName => {
  1315. keyboardShortcuts.unregisterShortcut(shortcutName);
  1316. });
  1317. delete preventCloseCbs[props.modalUuid];
  1318. // Delete the Pinia store that was created for this modal, after all other cleanup tasks are performed
  1319. editSongStore.$dispose();
  1320. });
  1321. </script>
  1322. <template>
  1323. <div>
  1324. <modal
  1325. :title="`${newSong ? 'Create' : 'Edit'} Song`"
  1326. class="song-modal"
  1327. :size="'wide'"
  1328. :split="true"
  1329. >
  1330. <template #toggleMobileSidebar v-if="bulk">
  1331. <i
  1332. class="material-icons toggle-sidebar-icon"
  1333. :content="`${
  1334. sidebarMobileActive ? 'Close' : 'Open'
  1335. } Edit Queue`"
  1336. v-tippy
  1337. @click="toggleMobileSidebar()"
  1338. >expand_circle_down</i
  1339. >
  1340. </template>
  1341. <template #sidebar v-if="bulk">
  1342. <div class="sidebar" :class="{ active: sidebarMobileActive }">
  1343. <header class="sidebar-head">
  1344. <h2 class="sidebar-title is-marginless">Edit Queue</h2>
  1345. <i
  1346. class="material-icons toggle-sidebar-icon"
  1347. :content="`${
  1348. sidebarMobileActive ? 'Close' : 'Open'
  1349. } Edit Queue`"
  1350. v-tippy
  1351. @click="toggleMobileSidebar()"
  1352. >expand_circle_down</i
  1353. >
  1354. </header>
  1355. <section class="sidebar-body">
  1356. <div
  1357. v-show="filteredItems.length > 0"
  1358. class="edit-songs-items"
  1359. >
  1360. <div
  1361. class="item"
  1362. v-for="(data, index) in filteredItems"
  1363. :key="`edit-songs-item-${index}`"
  1364. :ref="
  1365. el =>
  1366. (songItems[
  1367. `edit-songs-item-${data.song.youtubeId}`
  1368. ] = el)
  1369. "
  1370. >
  1371. <song-item
  1372. :song="data.song"
  1373. :thumbnail="false"
  1374. :duration="false"
  1375. :disabled-actions="
  1376. data.song.removed
  1377. ? ['all']
  1378. : ['report', 'edit']
  1379. "
  1380. :class="{
  1381. updated: data.song.updated,
  1382. removed: data.song.removed
  1383. }"
  1384. >
  1385. <template #leftIcon>
  1386. <i
  1387. v-if="
  1388. currentSong.youtubeId ===
  1389. data.song.youtubeId &&
  1390. !data.song.removed
  1391. "
  1392. class="material-icons item-icon editing-icon"
  1393. content="Currently editing song"
  1394. v-tippy="{ theme: 'info' }"
  1395. @click="toggleDone(index)"
  1396. >edit</i
  1397. >
  1398. <i
  1399. v-else-if="data.song.removed"
  1400. class="material-icons item-icon removed-icon"
  1401. content="Song removed"
  1402. v-tippy="{ theme: 'info' }"
  1403. >delete_forever</i
  1404. >
  1405. <i
  1406. v-else-if="data.status === 'error'"
  1407. class="material-icons item-icon error-icon"
  1408. content="Error saving song"
  1409. v-tippy="{ theme: 'info' }"
  1410. @click="toggleDone(index)"
  1411. >error</i
  1412. >
  1413. <i
  1414. v-else-if="data.status === 'saving'"
  1415. class="material-icons item-icon saving-icon"
  1416. content="Currently saving song"
  1417. v-tippy="{ theme: 'info' }"
  1418. >pending</i
  1419. >
  1420. <i
  1421. v-else-if="data.flagged"
  1422. class="material-icons item-icon flag-icon"
  1423. content="Song flagged"
  1424. v-tippy="{ theme: 'info' }"
  1425. @click="toggleDone(index)"
  1426. >flag_circle</i
  1427. >
  1428. <i
  1429. v-else-if="data.status === 'done'"
  1430. class="material-icons item-icon done-icon"
  1431. content="Song marked complete"
  1432. v-tippy="{ theme: 'info' }"
  1433. @click="toggleDone(index)"
  1434. >check_circle</i
  1435. >
  1436. <i
  1437. v-else-if="data.status === 'todo'"
  1438. class="material-icons item-icon todo-icon"
  1439. content="Song marked todo"
  1440. v-tippy="{ theme: 'info' }"
  1441. @click="toggleDone(index)"
  1442. >cancel</i
  1443. >
  1444. </template>
  1445. <template
  1446. v-if="!data.song.removed"
  1447. #actions
  1448. >
  1449. <i
  1450. class="material-icons edit-icon"
  1451. content="Edit Song"
  1452. v-tippy
  1453. @click="pickSong(data.song)"
  1454. >
  1455. edit
  1456. </i>
  1457. </template>
  1458. <template #tippyActions>
  1459. <i
  1460. class="material-icons flag-icon"
  1461. :class="{
  1462. flagged: data.flagged
  1463. }"
  1464. content="Toggle Flag"
  1465. v-tippy
  1466. @click="toggleFlag(index)"
  1467. >
  1468. flag_circle
  1469. </i>
  1470. </template>
  1471. </song-item>
  1472. </div>
  1473. </div>
  1474. <p v-if="filteredItems.length === 0" class="no-items">
  1475. {{
  1476. flagFilter
  1477. ? "No flagged songs queued"
  1478. : "No songs queued"
  1479. }}
  1480. </p>
  1481. </section>
  1482. <footer class="sidebar-foot">
  1483. <button
  1484. @click="toggleFlagFilter()"
  1485. class="button is-primary"
  1486. >
  1487. {{
  1488. flagFilter
  1489. ? "Show All Songs"
  1490. : "Show Only Flagged Songs"
  1491. }}
  1492. </button>
  1493. </footer>
  1494. </div>
  1495. <div
  1496. v-if="sidebarMobileActive"
  1497. class="sidebar-overlay"
  1498. @click="toggleMobileSidebar()"
  1499. ></div>
  1500. </template>
  1501. <template #body>
  1502. <div v-if="!youtubeId && !newSong" class="notice-container">
  1503. <h4>No song has been selected</h4>
  1504. </div>
  1505. <div v-if="songDeleted" class="notice-container">
  1506. <h4>The song you were editing has been deleted</h4>
  1507. </div>
  1508. <div
  1509. v-if="
  1510. youtubeId &&
  1511. !songDataLoaded &&
  1512. !songNotFound &&
  1513. !newSong
  1514. "
  1515. class="notice-container"
  1516. >
  1517. <h4>Song hasn't loaded yet</h4>
  1518. </div>
  1519. <div
  1520. v-if="youtubeId && songNotFound && !newSong"
  1521. class="notice-container"
  1522. >
  1523. <h4>Song was not found</h4>
  1524. </div>
  1525. <div
  1526. class="left-section"
  1527. v-show="songDataLoaded && !songDeleted"
  1528. >
  1529. <div class="top-section">
  1530. <div class="player-section">
  1531. <div :id="`editSongPlayer-${modalUuid}`" />
  1532. <div v-show="youtubeError" class="player-error">
  1533. <h2>{{ youtubeErrorMessage }}</h2>
  1534. </div>
  1535. <canvas
  1536. ref="canvasElement"
  1537. class="duration-canvas"
  1538. v-show="!youtubeError"
  1539. height="20"
  1540. width="530"
  1541. @click="setTrackPosition($event)"
  1542. />
  1543. <div class="player-footer">
  1544. <div class="player-footer-left">
  1545. <button
  1546. class="button is-primary"
  1547. @click="play()"
  1548. @keyup.enter="play()"
  1549. v-if="video.paused"
  1550. content="Resume Playback"
  1551. v-tippy
  1552. >
  1553. <i class="material-icons">play_arrow</i>
  1554. </button>
  1555. <button
  1556. class="button is-primary"
  1557. @click="settings('pause')"
  1558. @keyup.enter="settings('pause')"
  1559. v-else
  1560. content="Pause Playback"
  1561. v-tippy
  1562. >
  1563. <i class="material-icons">pause</i>
  1564. </button>
  1565. <button
  1566. class="button is-danger"
  1567. @click.exact="settings('stop')"
  1568. @click.shift="settings('hardStop')"
  1569. @keyup.enter.exact="settings('stop')"
  1570. @keyup.shift.enter="
  1571. settings('hardStop')
  1572. "
  1573. content="Stop Playback"
  1574. v-tippy
  1575. >
  1576. <i class="material-icons">stop</i>
  1577. </button>
  1578. <tippy
  1579. class="playerRateDropdown"
  1580. :touch="true"
  1581. :interactive="true"
  1582. placement="bottom"
  1583. theme="dropdown"
  1584. ref="dropdown"
  1585. trigger="click"
  1586. append-to="parent"
  1587. @show="
  1588. () => {
  1589. showRateDropdown = true;
  1590. }
  1591. "
  1592. @hide="
  1593. () => {
  1594. showRateDropdown = false;
  1595. }
  1596. "
  1597. >
  1598. <div
  1599. ref="trigger"
  1600. class="control has-addons"
  1601. content="Set Playback Rate"
  1602. v-tippy
  1603. >
  1604. <button class="button is-primary">
  1605. <i class="material-icons"
  1606. >fast_forward</i
  1607. >
  1608. </button>
  1609. <button
  1610. class="button dropdown-toggle"
  1611. >
  1612. <i class="material-icons">
  1613. {{
  1614. showRateDropdown
  1615. ? "expand_more"
  1616. : "expand_less"
  1617. }}
  1618. </i>
  1619. </button>
  1620. </div>
  1621. <template #content>
  1622. <div class="nav-dropdown-items">
  1623. <button
  1624. class="nav-item button"
  1625. :class="{
  1626. active:
  1627. video.playbackRate ===
  1628. 0.5
  1629. }"
  1630. title="0.5x"
  1631. @click="
  1632. setPlaybackRate(0.5)
  1633. "
  1634. >
  1635. <p>0.5x</p>
  1636. </button>
  1637. <button
  1638. class="nav-item button"
  1639. :class="{
  1640. active:
  1641. video.playbackRate ===
  1642. 1
  1643. }"
  1644. title="1x"
  1645. @click="setPlaybackRate(1)"
  1646. >
  1647. <p>1x</p>
  1648. </button>
  1649. <button
  1650. class="nav-item button"
  1651. :class="{
  1652. active:
  1653. video.playbackRate ===
  1654. 2
  1655. }"
  1656. title="2x"
  1657. @click="setPlaybackRate(2)"
  1658. >
  1659. <p>2x</p>
  1660. </button>
  1661. </div>
  1662. </template>
  1663. </tippy>
  1664. </div>
  1665. <div class="player-footer-center">
  1666. <span>
  1667. <span>
  1668. {{ youtubeVideoCurrentTime }}
  1669. </span>
  1670. /
  1671. <span>
  1672. {{ youtubeVideoDuration }}
  1673. {{ youtubeVideoNote }}
  1674. </span>
  1675. </span>
  1676. </div>
  1677. <div class="player-footer-right">
  1678. <p id="volume-control">
  1679. <i
  1680. class="material-icons"
  1681. @click="toggleMute()"
  1682. :content="`${
  1683. muted ? 'Unmute' : 'Mute'
  1684. }`"
  1685. v-tippy
  1686. >{{
  1687. muted
  1688. ? "volume_mute"
  1689. : volumeSliderValue >= 50
  1690. ? "volume_up"
  1691. : "volume_down"
  1692. }}</i
  1693. >
  1694. <input
  1695. v-model="volumeSliderValue"
  1696. type="range"
  1697. min="0"
  1698. max="100"
  1699. class="volume-slider active"
  1700. @change="changeVolume()"
  1701. @input="changeVolume()"
  1702. />
  1703. </p>
  1704. </div>
  1705. </div>
  1706. </div>
  1707. <song-thumbnail
  1708. v-if="songDataLoaded && !songDeleted"
  1709. :song="{
  1710. youtubeId: inputs['youtubeId'].value,
  1711. thumbnail: inputs['thumbnail'].value
  1712. }"
  1713. :fallback="false"
  1714. class="thumbnail-preview"
  1715. @load-error="onThumbnailLoadError"
  1716. />
  1717. <img
  1718. v-if="
  1719. !isYoutubeThumbnail &&
  1720. songDataLoaded &&
  1721. !songDeleted
  1722. "
  1723. class="thumbnail-dummy"
  1724. :src="inputs['thumbnail'].value"
  1725. ref="thumbnailElement"
  1726. @load="onThumbnailLoad"
  1727. />
  1728. </div>
  1729. <div
  1730. class="edit-section"
  1731. v-if="songDataLoaded && !songDeleted"
  1732. >
  1733. <div class="control is-grouped">
  1734. <div class="title-container">
  1735. <label class="label">Title</label>
  1736. <p class="control has-addons">
  1737. <input
  1738. class="input"
  1739. type="text"
  1740. :ref="el => (inputs['title'].ref = el)"
  1741. v-model="inputs['title'].value"
  1742. placeholder="Enter song title..."
  1743. @keyup.shift.enter="
  1744. getAlbumData('title')
  1745. "
  1746. />
  1747. <button
  1748. class="button youtube-get-button"
  1749. @click="getYouTubeData('title')"
  1750. >
  1751. <div
  1752. class="youtube-icon"
  1753. v-tippy
  1754. content="Fill from YouTube"
  1755. ></div>
  1756. </button>
  1757. <button
  1758. class="button album-get-button"
  1759. @click="getAlbumData('title')"
  1760. >
  1761. <i
  1762. class="material-icons"
  1763. v-tippy
  1764. content="Fill from Discogs"
  1765. >album</i
  1766. >
  1767. </button>
  1768. </p>
  1769. </div>
  1770. <div class="duration-container">
  1771. <label class="label">Duration</label>
  1772. <p class="control has-addons">
  1773. <input
  1774. class="input"
  1775. type="text"
  1776. placeholder="Enter song duration..."
  1777. v-model.number="
  1778. inputs['duration'].value
  1779. "
  1780. @keyup.shift.enter="fillDuration()"
  1781. />
  1782. <button
  1783. class="button duration-fill-button"
  1784. @click="fillDuration()"
  1785. >
  1786. <i
  1787. class="material-icons"
  1788. v-tippy
  1789. content="Sync duration with YouTube"
  1790. >sync</i
  1791. >
  1792. </button>
  1793. </p>
  1794. </div>
  1795. <div class="skip-duration-container">
  1796. <label class="label">Skip duration</label>
  1797. <p class="control">
  1798. <input
  1799. class="input"
  1800. type="text"
  1801. placeholder="Enter skip duration..."
  1802. v-model.number="
  1803. inputs['skipDuration'].value
  1804. "
  1805. />
  1806. </p>
  1807. </div>
  1808. </div>
  1809. <div class="control is-grouped">
  1810. <div class="album-art-container">
  1811. <label class="label">
  1812. Thumbnail
  1813. <i
  1814. v-if="
  1815. thumbnailNotSquare &&
  1816. !isYoutubeThumbnail
  1817. "
  1818. class="material-icons thumbnail-warning"
  1819. content="Thumbnail not square, it will be stretched"
  1820. v-tippy="{ theme: 'info' }"
  1821. >
  1822. warning
  1823. </i>
  1824. <i
  1825. v-if="
  1826. thumbnailLoadError &&
  1827. !isYoutubeThumbnail
  1828. "
  1829. class="material-icons thumbnail-warning"
  1830. content="Error loading thumbnail"
  1831. v-tippy="{ theme: 'info' }"
  1832. >
  1833. warning
  1834. </i>
  1835. </label>
  1836. <p class="control has-addons">
  1837. <input
  1838. class="input"
  1839. type="text"
  1840. v-model="inputs['thumbnail'].value"
  1841. placeholder="Enter link to thumbnail..."
  1842. @keyup.shift.enter="
  1843. getAlbumData('albumArt')
  1844. "
  1845. />
  1846. <button
  1847. class="button youtube-get-button"
  1848. @click="getYouTubeData('thumbnail')"
  1849. >
  1850. <div
  1851. class="youtube-icon"
  1852. v-tippy
  1853. content="Fill from YouTube"
  1854. ></div>
  1855. </button>
  1856. <button
  1857. class="button album-get-button"
  1858. @click="getAlbumData('albumArt')"
  1859. >
  1860. <i
  1861. class="material-icons"
  1862. v-tippy
  1863. content="Fill from Discogs"
  1864. >album</i
  1865. >
  1866. </button>
  1867. </p>
  1868. </div>
  1869. <div class="youtube-id-container">
  1870. <label class="label">YouTube ID</label>
  1871. <p class="control">
  1872. <input
  1873. class="input"
  1874. type="text"
  1875. placeholder="Enter YouTube ID..."
  1876. v-model="inputs['youtubeId'].value"
  1877. />
  1878. </p>
  1879. </div>
  1880. <div class="verified-container">
  1881. <label class="label">Verified</label>
  1882. <p class="is-expanded checkbox-control">
  1883. <label class="switch">
  1884. <input
  1885. type="checkbox"
  1886. id="verified"
  1887. v-model="inputs['verified'].value"
  1888. />
  1889. <span class="slider round"></span>
  1890. </label>
  1891. </p>
  1892. </div>
  1893. </div>
  1894. <div class="control is-grouped">
  1895. <div class="artists-container">
  1896. <label class="label">Artists</label>
  1897. <p class="control has-addons">
  1898. <auto-suggest
  1899. v-model="inputs['addArtist'].value"
  1900. ref="new-artist"
  1901. placeholder="Add artist..."
  1902. :all-items="
  1903. autosuggest.allItems.artists
  1904. "
  1905. @submitted="addTag('artists')"
  1906. @keyup.shift.enter="
  1907. getAlbumData('artists')
  1908. "
  1909. />
  1910. <button
  1911. class="button youtube-get-button"
  1912. @click="getYouTubeData('author')"
  1913. >
  1914. <div
  1915. class="youtube-icon"
  1916. v-tippy
  1917. content="Fill from YouTube"
  1918. ></div>
  1919. </button>
  1920. <button
  1921. class="button album-get-button"
  1922. @click="getAlbumData('artists')"
  1923. >
  1924. <i
  1925. class="material-icons"
  1926. v-tippy
  1927. content="Fill from Discogs"
  1928. >album</i
  1929. >
  1930. </button>
  1931. <button
  1932. class="button is-info add-button"
  1933. @click="addTag('artists')"
  1934. >
  1935. <i class="material-icons">add</i>
  1936. </button>
  1937. </p>
  1938. <div class="list-container">
  1939. <div
  1940. class="list-item"
  1941. v-for="artist in inputs['artists']
  1942. .value"
  1943. :key="artist"
  1944. >
  1945. <div
  1946. class="list-item-circle"
  1947. @click="
  1948. removeTag('artists', artist)
  1949. "
  1950. >
  1951. <i class="material-icons">close</i>
  1952. </div>
  1953. <p>{{ artist }}</p>
  1954. </div>
  1955. </div>
  1956. </div>
  1957. <div class="genres-container">
  1958. <label class="label">
  1959. <span>Genres</span>
  1960. <i
  1961. class="material-icons"
  1962. @click="toggleGenreHelper"
  1963. @dblclick="resetGenreHelper"
  1964. v-tippy
  1965. content="View list of genres"
  1966. >info</i
  1967. >
  1968. </label>
  1969. <p class="control has-addons">
  1970. <auto-suggest
  1971. v-model="inputs['addGenre'].value"
  1972. ref="new-genre"
  1973. placeholder="Add genre..."
  1974. :all-items="autosuggest.allItems.genres"
  1975. @submitted="addTag('genres')"
  1976. @keyup.shift.enter="
  1977. getAlbumData('genres')
  1978. "
  1979. />
  1980. <button
  1981. class="button album-get-button"
  1982. @click="getAlbumData('genres')"
  1983. >
  1984. <i
  1985. class="material-icons"
  1986. v-tippy
  1987. content="Fill from Discogs"
  1988. >album</i
  1989. >
  1990. </button>
  1991. <button
  1992. class="button is-info add-button"
  1993. @click="addTag('genres')"
  1994. >
  1995. <i class="material-icons">add</i>
  1996. </button>
  1997. </p>
  1998. <div class="list-container">
  1999. <div
  2000. class="list-item"
  2001. v-for="genre in inputs['genres'].value"
  2002. :key="genre"
  2003. >
  2004. <div
  2005. class="list-item-circle"
  2006. @click="removeTag('genres', genre)"
  2007. >
  2008. <i class="material-icons">close</i>
  2009. </div>
  2010. <p>{{ genre }}</p>
  2011. </div>
  2012. </div>
  2013. </div>
  2014. <div class="tags-container">
  2015. <label class="label">Tags</label>
  2016. <p class="control has-addons">
  2017. <auto-suggest
  2018. v-model="inputs['addTag'].value"
  2019. ref="new-tag"
  2020. placeholder="Add tag..."
  2021. :all-items="autosuggest.allItems.tags"
  2022. @submitted="addTag('tags')"
  2023. />
  2024. <button
  2025. class="button is-info add-button"
  2026. @click="addTag('tags')"
  2027. >
  2028. <i class="material-icons">add</i>
  2029. </button>
  2030. </p>
  2031. <div class="list-container">
  2032. <div
  2033. class="list-item"
  2034. v-for="tag in inputs['tags'].value"
  2035. :key="tag"
  2036. >
  2037. <div
  2038. class="list-item-circle"
  2039. @click="removeTag('tags', tag)"
  2040. >
  2041. <i class="material-icons">close</i>
  2042. </div>
  2043. <p>{{ tag }}</p>
  2044. </div>
  2045. </div>
  2046. </div>
  2047. </div>
  2048. </div>
  2049. </div>
  2050. <div
  2051. class="right-section"
  2052. v-if="songDataLoaded && !songDeleted"
  2053. >
  2054. <div id="tabs-container">
  2055. <div id="tab-selection">
  2056. <button
  2057. class="button is-default"
  2058. :class="{ selected: tab === 'discogs' }"
  2059. :ref="el => (tabs['discogs-tab'] = el)"
  2060. @click="showTab('discogs')"
  2061. >
  2062. Discogs
  2063. </button>
  2064. <button
  2065. v-if="!newSong"
  2066. class="button is-default"
  2067. :class="{ selected: tab === 'reports' }"
  2068. :ref="el => (tabs['reports-tab'] = el)"
  2069. @click="showTab('reports')"
  2070. >
  2071. Reports ({{ reports.length }})
  2072. </button>
  2073. <button
  2074. class="button is-default"
  2075. :class="{ selected: tab === 'youtube' }"
  2076. :ref="el => (tabs['youtube-tab'] = el)"
  2077. @click="showTab('youtube')"
  2078. >
  2079. YouTube
  2080. </button>
  2081. <button
  2082. class="button is-default"
  2083. :class="{ selected: tab === 'musare-songs' }"
  2084. :ref="el => (tabs['musare-songs-tab'] = el)"
  2085. @click="showTab('musare-songs')"
  2086. >
  2087. Songs
  2088. </button>
  2089. </div>
  2090. <discogs
  2091. class="tab"
  2092. v-show="tab === 'discogs'"
  2093. :bulk="bulk"
  2094. :modal-uuid="modalUuid"
  2095. :modal-module-path="modalModulePath"
  2096. />
  2097. <reports-tab
  2098. v-if="!newSong"
  2099. class="tab"
  2100. v-show="tab === 'reports'"
  2101. :modal-uuid="modalUuid"
  2102. :modal-module-path="modalModulePath"
  2103. />
  2104. <youtube
  2105. class="tab"
  2106. v-show="tab === 'youtube'"
  2107. :modal-uuid="modalUuid"
  2108. :modal-module-path="modalModulePath"
  2109. />
  2110. <musare-songs
  2111. class="tab"
  2112. v-show="tab === 'musare-songs'"
  2113. :modal-uuid="modalUuid"
  2114. :modal-module-path="modalModulePath"
  2115. />
  2116. </div>
  2117. </div>
  2118. </template>
  2119. <template #footer>
  2120. <div v-if="bulk">
  2121. <button class="button is-primary" @click="editNextSong()">
  2122. Next
  2123. </button>
  2124. <button
  2125. class="button is-primary"
  2126. @click="toggleFlag()"
  2127. v-if="youtubeId && !songDeleted"
  2128. >
  2129. {{ currentSongFlagged ? "Unflag" : "Flag" }}
  2130. </button>
  2131. </div>
  2132. <div v-if="!newSong && !songDeleted">
  2133. <save-button
  2134. :ref="el => (saveButtonRefs['saveButton'] = el)"
  2135. @clicked="saveSong('saveButton')"
  2136. />
  2137. <save-button
  2138. :ref="el => (saveButtonRefs['saveAndCloseButton'] = el)"
  2139. :default-message="
  2140. bulk ? `Save and next` : `Save and close`
  2141. "
  2142. @clicked="saveSong('saveAndCloseButton', true)"
  2143. />
  2144. <div class="right">
  2145. <button
  2146. v-if="hasPermission('songs.remove')"
  2147. class="button is-danger icon-with-button material-icons"
  2148. @click.prevent="
  2149. openModal({
  2150. modal: 'confirm',
  2151. props: {
  2152. message:
  2153. 'Removing this song will remove it from all playlists and cause a ratings recalculation.',
  2154. onCompleted: () => remove(song._id)
  2155. }
  2156. })
  2157. "
  2158. content="Delete Song"
  2159. v-tippy
  2160. >
  2161. delete_forever
  2162. </button>
  2163. </div>
  2164. </div>
  2165. <div v-else-if="newSong">
  2166. <save-button
  2167. :ref="el => (saveButtonRefs['createButton'] = el)"
  2168. default-message="Create Song"
  2169. @clicked="saveSong('createButton')"
  2170. />
  2171. <save-button
  2172. :ref="
  2173. el => (saveButtonRefs['createAndCloseButton'] = el)
  2174. "
  2175. :default-message="
  2176. bulk ? `Create and next` : `Create and close`
  2177. "
  2178. @clicked="saveSong('createAndCloseButton', true)"
  2179. />
  2180. </div>
  2181. </template>
  2182. </modal>
  2183. <floating-box
  2184. id="genreHelper"
  2185. ref="genreHelper"
  2186. :column="false"
  2187. title="Song Genres List"
  2188. >
  2189. <template #body>
  2190. <span
  2191. v-for="item in autosuggest.allItems.genres"
  2192. :key="`genre-helper-${item}`"
  2193. >
  2194. {{ item }}
  2195. </span>
  2196. </template>
  2197. </floating-box>
  2198. </div>
  2199. </template>
  2200. <style lang="less" scoped>
  2201. .night-mode {
  2202. .edit-section,
  2203. .player-section,
  2204. #tabs-container {
  2205. background-color: var(--dark-grey-3) !important;
  2206. border: 0 !important;
  2207. .tab {
  2208. border: 0 !important;
  2209. }
  2210. }
  2211. #tabs-container #tab-selection .button {
  2212. background: var(--dark-grey) !important;
  2213. color: var(--white) !important;
  2214. }
  2215. .left-section {
  2216. .edit-section {
  2217. .album-get-button,
  2218. .duration-fill-button,
  2219. .youtube-get-button,
  2220. .add-button {
  2221. &:focus,
  2222. &:hover {
  2223. border: none !important;
  2224. }
  2225. }
  2226. }
  2227. }
  2228. .duration-canvas {
  2229. background-color: var(--dark-grey-2) !important;
  2230. }
  2231. .sidebar {
  2232. .sidebar-head,
  2233. .sidebar-foot {
  2234. background-color: var(--dark-grey-3);
  2235. border: none;
  2236. }
  2237. .sidebar-body {
  2238. background-color: var(--dark-grey-4) !important;
  2239. }
  2240. .sidebar-head .toggle-sidebar-icon.material-icons,
  2241. .sidebar-title {
  2242. color: var(--white);
  2243. }
  2244. p,
  2245. label,
  2246. td,
  2247. th {
  2248. color: var(--light-grey-2) !important;
  2249. }
  2250. h1,
  2251. h2,
  2252. h3,
  2253. h4,
  2254. h5,
  2255. h6 {
  2256. color: var(--white) !important;
  2257. }
  2258. }
  2259. }
  2260. .modal-card-body {
  2261. display: flex;
  2262. }
  2263. .notice-container {
  2264. display: flex;
  2265. flex: 1;
  2266. justify-content: center;
  2267. h4 {
  2268. margin: auto;
  2269. }
  2270. }
  2271. .left-section {
  2272. height: 100%;
  2273. display: flex;
  2274. flex-direction: column;
  2275. margin-right: 16px;
  2276. .top-section {
  2277. display: flex;
  2278. .player-section {
  2279. width: 530px;
  2280. display: flex;
  2281. flex-direction: column;
  2282. border: 1px solid var(--light-grey-3);
  2283. border-radius: @border-radius;
  2284. overflow: hidden;
  2285. .duration-canvas {
  2286. background-color: var(--light-grey-2);
  2287. }
  2288. .player-error {
  2289. display: flex;
  2290. height: 318px;
  2291. width: 530px;
  2292. align-items: center;
  2293. * {
  2294. margin: 0;
  2295. flex: 1;
  2296. font-size: 30px;
  2297. text-align: center;
  2298. }
  2299. }
  2300. .player-footer {
  2301. display: flex;
  2302. justify-content: space-between;
  2303. height: 54px;
  2304. padding-left: 10px;
  2305. padding-right: 10px;
  2306. > * {
  2307. width: 33.3%;
  2308. display: flex;
  2309. align-items: center;
  2310. }
  2311. .player-footer-left {
  2312. flex: 1;
  2313. & > .button:not(:first-child) {
  2314. margin-left: 5px;
  2315. }
  2316. :deep(& > .playerRateDropdown) {
  2317. margin-left: 5px;
  2318. margin-bottom: unset !important;
  2319. .control.has-addons {
  2320. margin-bottom: unset !important;
  2321. & > .button {
  2322. font-size: 24px;
  2323. }
  2324. }
  2325. }
  2326. :deep(.tippy-box[data-theme~="dropdown"]) {
  2327. max-width: 100px !important;
  2328. .nav-dropdown-items .nav-item {
  2329. justify-content: center !important;
  2330. border-radius: @border-radius !important;
  2331. &.active {
  2332. background-color: var(--primary-color);
  2333. color: var(--white);
  2334. }
  2335. }
  2336. }
  2337. }
  2338. .player-footer-center {
  2339. justify-content: center;
  2340. align-items: center;
  2341. flex: 2;
  2342. font-size: 18px;
  2343. font-weight: 400;
  2344. width: 200px;
  2345. margin: 0 5px;
  2346. img {
  2347. height: 21px;
  2348. margin-right: 12px;
  2349. filter: invert(26%) sepia(54%) saturate(6317%)
  2350. hue-rotate(2deg) brightness(92%) contrast(115%);
  2351. }
  2352. }
  2353. .player-footer-right {
  2354. justify-content: right;
  2355. flex: 1;
  2356. #volume-control {
  2357. margin: 3px;
  2358. margin-top: 0;
  2359. display: flex;
  2360. align-items: center;
  2361. cursor: pointer;
  2362. .volume-slider {
  2363. width: 100%;
  2364. padding: 0 15px;
  2365. background: transparent;
  2366. min-width: 100px;
  2367. }
  2368. input[type="range"] {
  2369. -webkit-appearance: none;
  2370. margin: 7.3px 0;
  2371. }
  2372. input[type="range"]:focus {
  2373. outline: none;
  2374. }
  2375. input[type="range"]::-webkit-slider-runnable-track {
  2376. width: 100%;
  2377. height: 5.2px;
  2378. cursor: pointer;
  2379. box-shadow: 0;
  2380. background: var(--light-grey-3);
  2381. border-radius: @border-radius;
  2382. border: 0;
  2383. }
  2384. input[type="range"]::-webkit-slider-thumb {
  2385. box-shadow: 0;
  2386. border: 0;
  2387. height: 19px;
  2388. width: 19px;
  2389. border-radius: 100%;
  2390. background: var(--primary-color);
  2391. cursor: pointer;
  2392. -webkit-appearance: none;
  2393. margin-top: -6.5px;
  2394. }
  2395. input[type="range"]::-moz-range-track {
  2396. width: 100%;
  2397. height: 5.2px;
  2398. cursor: pointer;
  2399. box-shadow: 0;
  2400. background: var(--light-grey-3);
  2401. border-radius: @border-radius;
  2402. border: 0;
  2403. }
  2404. input[type="range"]::-moz-range-thumb {
  2405. box-shadow: 0;
  2406. border: 0;
  2407. height: 19px;
  2408. width: 19px;
  2409. border-radius: 100%;
  2410. background: var(--primary-color);
  2411. cursor: pointer;
  2412. -webkit-appearance: none;
  2413. margin-top: -6.5px;
  2414. }
  2415. input[type="range"]::-ms-track {
  2416. width: 100%;
  2417. height: 5.2px;
  2418. cursor: pointer;
  2419. box-shadow: 0;
  2420. background: var(--light-grey-3);
  2421. border-radius: @border-radius;
  2422. }
  2423. input[type="range"]::-ms-fill-lower {
  2424. background: var(--light-grey-3);
  2425. border: 0;
  2426. border-radius: 0;
  2427. box-shadow: 0;
  2428. }
  2429. input[type="range"]::-ms-fill-upper {
  2430. background: var(--light-grey-3);
  2431. border: 0;
  2432. border-radius: 0;
  2433. box-shadow: 0;
  2434. }
  2435. input[type="range"]::-ms-thumb {
  2436. box-shadow: 0;
  2437. border: 0;
  2438. height: 15px;
  2439. width: 15px;
  2440. border-radius: 100%;
  2441. background: var(--primary-color);
  2442. cursor: pointer;
  2443. -webkit-appearance: none;
  2444. margin-top: 1.5px;
  2445. }
  2446. }
  2447. }
  2448. }
  2449. }
  2450. :deep(.thumbnail-preview) {
  2451. width: 189px;
  2452. height: 189px;
  2453. margin-left: 16px;
  2454. }
  2455. .thumbnail-dummy {
  2456. opacity: 0;
  2457. height: 10px;
  2458. width: 10px;
  2459. }
  2460. }
  2461. .edit-section {
  2462. display: flex;
  2463. flex-wrap: wrap;
  2464. flex-grow: 1;
  2465. border: 1px solid var(--light-grey-3);
  2466. margin-top: 16px;
  2467. border-radius: @border-radius;
  2468. .album-get-button {
  2469. background-color: var(--purple);
  2470. color: var(--white);
  2471. width: 32px;
  2472. text-align: center;
  2473. border-width: 0;
  2474. }
  2475. .duration-fill-button,
  2476. .youtube-get-button {
  2477. background-color: var(--dark-red);
  2478. color: var(--white);
  2479. width: 32px;
  2480. text-align: center;
  2481. border-width: 0;
  2482. }
  2483. .add-button {
  2484. background-color: var(--primary-color) !important;
  2485. width: 32px;
  2486. i {
  2487. font-size: 32px;
  2488. }
  2489. }
  2490. .album-get-button,
  2491. .duration-fill-button,
  2492. .youtube-get-button,
  2493. .add-button {
  2494. &:focus,
  2495. &:hover {
  2496. filter: contrast(0.75);
  2497. border: 1px solid var(--black) !important;
  2498. }
  2499. }
  2500. .youtube-get-button {
  2501. padding-left: 4px;
  2502. padding-right: 4px;
  2503. .youtube-icon {
  2504. background: var(--white);
  2505. }
  2506. }
  2507. > div {
  2508. margin: 16px !important;
  2509. width: 100%;
  2510. }
  2511. input {
  2512. width: 100%;
  2513. }
  2514. .title-container {
  2515. width: calc((100% - 32px) / 2);
  2516. }
  2517. .duration-container {
  2518. margin-right: 16px;
  2519. margin-left: 16px;
  2520. width: calc((100% - 32px) / 4);
  2521. }
  2522. .skip-duration-container {
  2523. width: calc((100% - 32px) / 4);
  2524. }
  2525. .album-art-container {
  2526. margin-right: 16px;
  2527. width: 100%;
  2528. }
  2529. .youtube-id-container {
  2530. margin-right: 16px;
  2531. width: calc((100% - 16px) / 8 * 3);
  2532. }
  2533. .verified-container {
  2534. width: calc((100% - 16px) / 8);
  2535. .checkbox-control {
  2536. margin-top: 10px;
  2537. }
  2538. }
  2539. .artists-container {
  2540. width: calc((100% - 32px) / 3);
  2541. position: relative;
  2542. }
  2543. .genres-container {
  2544. width: calc((100% - 32px) / 3);
  2545. margin-left: 16px;
  2546. margin-right: 16px;
  2547. position: relative;
  2548. label {
  2549. display: flex;
  2550. i {
  2551. font-size: 15px;
  2552. align-self: center;
  2553. margin-left: 5px;
  2554. color: var(--primary-color);
  2555. cursor: pointer;
  2556. -webkit-user-select: none;
  2557. -moz-user-select: none;
  2558. -ms-user-select: none;
  2559. user-select: none;
  2560. }
  2561. }
  2562. }
  2563. .tags-container {
  2564. width: calc((100% - 32px) / 3);
  2565. position: relative;
  2566. }
  2567. .list-item-circle {
  2568. background-color: var(--primary-color);
  2569. width: 16px;
  2570. height: 16px;
  2571. border-radius: 8px;
  2572. cursor: pointer;
  2573. margin-right: 8px;
  2574. float: left;
  2575. -webkit-touch-callout: none;
  2576. -webkit-user-select: none;
  2577. -khtml-user-select: none;
  2578. -moz-user-select: none;
  2579. -ms-user-select: none;
  2580. user-select: none;
  2581. i {
  2582. color: var(--primary-color);
  2583. font-size: 14px;
  2584. margin-left: 1px;
  2585. position: relative;
  2586. top: -1px;
  2587. }
  2588. }
  2589. .list-item-circle:hover,
  2590. .list-item-circle:focus {
  2591. i {
  2592. color: var(--white);
  2593. }
  2594. }
  2595. .list-item > p {
  2596. line-height: 16px;
  2597. word-wrap: break-word;
  2598. width: calc(100% - 24px);
  2599. left: 24px;
  2600. float: left;
  2601. margin-bottom: 8px;
  2602. }
  2603. .list-item:last-child > p {
  2604. margin-bottom: 0;
  2605. }
  2606. .thumbnail-warning {
  2607. color: var(--red);
  2608. font-size: 18px;
  2609. margin: auto 0 auto 5px;
  2610. }
  2611. }
  2612. }
  2613. .right-section {
  2614. flex-basis: unset !important;
  2615. flex-grow: 0 !important;
  2616. display: flex;
  2617. height: 100%;
  2618. #tabs-container {
  2619. width: 376px;
  2620. #tab-selection {
  2621. display: flex;
  2622. overflow-x: auto;
  2623. .button {
  2624. border-radius: @border-radius @border-radius 0 0;
  2625. border: 0;
  2626. text-transform: uppercase;
  2627. font-size: 14px;
  2628. color: var(--dark-grey-3);
  2629. background-color: var(--light-grey-2);
  2630. flex-grow: 1;
  2631. height: 32px;
  2632. &:not(:first-of-type) {
  2633. margin-left: 5px;
  2634. }
  2635. }
  2636. .selected {
  2637. background-color: var(--primary-color) !important;
  2638. color: var(--white) !important;
  2639. font-weight: 600;
  2640. }
  2641. }
  2642. .tab {
  2643. border: 1px solid var(--light-grey-3);
  2644. border-radius: 0 0 @border-radius @border-radius;
  2645. padding: 15px;
  2646. height: calc(100% - 32px);
  2647. overflow: auto;
  2648. }
  2649. }
  2650. }
  2651. @media screen and (max-width: 1100px) {
  2652. .left-section,
  2653. .right-section {
  2654. height: unset;
  2655. max-height: unset;
  2656. }
  2657. .left-section {
  2658. margin-right: 0;
  2659. }
  2660. .right-section {
  2661. flex-basis: 100% !important;
  2662. #tabs-container {
  2663. width: 100%;
  2664. }
  2665. }
  2666. }
  2667. .modal-card-foot .is-primary {
  2668. width: 200px;
  2669. }
  2670. :deep(.autosuggest-container) {
  2671. top: unset;
  2672. }
  2673. .toggle-sidebar-icon {
  2674. display: none;
  2675. }
  2676. .sidebar {
  2677. width: 100%;
  2678. max-width: 350px;
  2679. z-index: 2000;
  2680. display: flex;
  2681. flex-direction: column;
  2682. position: relative;
  2683. height: 100%;
  2684. max-height: calc(100vh - 40px);
  2685. overflow: auto;
  2686. margin-right: 8px;
  2687. border-radius: @border-radius;
  2688. .sidebar-head,
  2689. .sidebar-foot {
  2690. display: flex;
  2691. flex-shrink: 0;
  2692. position: relative;
  2693. justify-content: flex-start;
  2694. align-items: center;
  2695. padding: 20px;
  2696. background-color: var(--light-grey);
  2697. }
  2698. .sidebar-head {
  2699. border-bottom: 1px solid var(--light-grey-2);
  2700. border-radius: @border-radius @border-radius 0 0;
  2701. .sidebar-title {
  2702. display: flex;
  2703. flex: 1;
  2704. margin: 0;
  2705. font-size: 26px;
  2706. font-weight: 600;
  2707. }
  2708. }
  2709. .sidebar-body {
  2710. background-color: var(--white);
  2711. display: flex;
  2712. flex-direction: column;
  2713. row-gap: 8px;
  2714. flex: 1;
  2715. overflow: auto;
  2716. padding: 10px;
  2717. .edit-songs-items {
  2718. display: flex;
  2719. flex-direction: column;
  2720. row-gap: 8px;
  2721. .item {
  2722. display: flex;
  2723. flex-direction: row;
  2724. align-items: center;
  2725. column-gap: 8px;
  2726. :deep(.song-item) {
  2727. .item-icon {
  2728. margin-right: 10px;
  2729. cursor: pointer;
  2730. }
  2731. .removed-icon,
  2732. .error-icon {
  2733. color: var(--red);
  2734. }
  2735. .saving-icon,
  2736. .todo-icon,
  2737. .editing-icon {
  2738. color: var(--primary-color);
  2739. }
  2740. .done-icon {
  2741. color: var(--green);
  2742. }
  2743. .flag-icon {
  2744. color: var(--orange);
  2745. &.flagged {
  2746. color: var(--grey);
  2747. }
  2748. }
  2749. &.removed {
  2750. filter: grayscale(100%);
  2751. cursor: not-allowed;
  2752. user-select: none;
  2753. }
  2754. }
  2755. }
  2756. }
  2757. .no-items {
  2758. text-align: center;
  2759. font-size: 18px;
  2760. }
  2761. }
  2762. .sidebar-foot {
  2763. border-top: 1px solid var(--light-grey-2);
  2764. border-radius: 0 0 @border-radius @border-radius;
  2765. .button {
  2766. flex: 1;
  2767. }
  2768. }
  2769. .sidebar-overlay {
  2770. display: none;
  2771. }
  2772. }
  2773. @media only screen and (max-width: 1580px) {
  2774. .toggle-sidebar-icon {
  2775. display: flex;
  2776. margin-right: 5px;
  2777. transform: rotate(90deg);
  2778. cursor: pointer;
  2779. }
  2780. .sidebar {
  2781. display: none;
  2782. &.active {
  2783. display: flex;
  2784. position: absolute;
  2785. z-index: 2010;
  2786. top: 20px;
  2787. left: 20px;
  2788. .sidebar-head .toggle-sidebar-icon {
  2789. display: flex;
  2790. margin-left: 5px;
  2791. transform: rotate(-90deg);
  2792. }
  2793. }
  2794. }
  2795. .sidebar-overlay {
  2796. display: flex;
  2797. position: absolute;
  2798. z-index: 2009;
  2799. top: 0;
  2800. left: 0;
  2801. right: 0;
  2802. bottom: 0;
  2803. background-color: rgba(10, 10, 10, 0.85);
  2804. }
  2805. }
  2806. </style>