index.vue 72 KB

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