index.vue 71 KB

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