index.vue 72 KB

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