22import { ref , onMounted , computed , watch , nextTick } from ' vue'
33import { Button , Select , TextInput , CheckBox , DropMenu , NumberInput , Modal , theme } from ' @munet/ui'
44import type { SelectOption } from ' @munet/ui'
5- import { useStorage } from ' @vueuse/core'
5+ import { useStorage , useDropZone } from ' @vueuse/core'
66import { VList } from ' virtua/vue'
77import { getMusicList , getSources , getGenreMap , getJacketUrl , saveMusic , getExportMp3Url , ensureBackendUrl , importJacket , importChart , getExportChartUrl , getExportOptUrl , getExportCustomUrl , openExplorer , openXml , changeId , deleteMusic , setJacket , setAudio , replaceChart , isWebView , getBaseUrl } from ' @/api'
88import type { MusicListItem } from ' @/api'
@@ -15,6 +15,7 @@ import ImportMusicModal from '@/views/ImportMusicModal.vue'
1515import PlayerBar from ' @/components/PlayerBar.vue'
1616import BottomOverlay from ' @/components/BottomOverlay.vue'
1717import FileTypeIcon from ' @/components/FileTypeIcon.vue'
18+ import MusicIdConflictNotifier from ' @/components/MusicIdConflictNotifier'
1819import { BlobWriter , ZipReader } from ' @zip.js/zip.js'
1920import getSubDirFile from ' @/utils/getSubDirFile'
2021import { useI18n } from ' vue-i18n'
@@ -272,6 +273,13 @@ async function exportToFolder(url: string) {
272273 }
273274}
274275
276+ async function applyJacket(file : File ) {
277+ if (! selectedMusic .value ) return
278+ await setJacket (selectedMusic .value .id , selectedMusic .value .assetDir , file )
279+ selectedMusic .value .hasJacket = true
280+ setStatus (t (' music.jacketImported' ))
281+ }
282+
275283async function handleSetJacket() {
276284 if (! selectedMusic .value ) return
277285 let fileHandle: FileSystemFileHandle
@@ -281,14 +289,22 @@ async function handleSetJacket() {
281289 types: [{ description: ' Image' , accept: { ' image/*' : [' .png' , ' .jpg' , ' .jpeg' , ' .bmp' ] } }],
282290 })
283291 } catch { return }
284- const file = await fileHandle .getFile ()
285- await setJacket (selectedMusic .value .id , selectedMusic .value .assetDir , file )
286- selectedMusic .value .hasJacket = true
287- setStatus (t (' music.jacketImported' ))
292+ await applyJacket (await fileHandle .getFile ())
288293}
289294
290295const showAudioOverlay = ref (false )
291296
297+ async function applyAudio(file : File ) {
298+ if (! selectedMusic .value ) return
299+ setStatus (t (' music.audioImporting' ))
300+ try {
301+ await setAudio (selectedMusic .value .id , selectedMusic .value .assetDir , file )
302+ setStatus (t (' music.audioReplaced' ))
303+ } catch (e : any ) {
304+ setStatus (t (' music.audioReplaceFailed' , { error: e ?.response ?.data || e ?.message }))
305+ }
306+ }
307+
292308async function handleReplaceAudio() {
293309 if (! selectedMusic .value ) return
294310 showAudioOverlay .value = true
@@ -303,18 +319,26 @@ async function handleReplaceAudio() {
303319 return
304320 }
305321 showAudioOverlay .value = false
306- const file = await fileHandle .getFile ()
307- setStatus (t (' music.audioImporting' ))
322+ await applyAudio (await fileHandle .getFile ())
323+ }
324+
325+ const showChartOverlay = ref (false )
326+
327+ async function applyChart(diffIndex : number , file : File ) {
328+ if (! selectedMusic .value ) return
308329 try {
309- await setAudio (selectedMusic .value .id , selectedMusic .value .assetDir , file )
310- setStatus (t (' music.audioReplaced' ))
330+ const res = await replaceChart (selectedMusic .value .id , selectedMusic .value .assetDir , diffIndex , file )
331+ if (res .imported ) {
332+ const suffix = res .convertedFrom ? ` (${res .convertedFrom .toUpperCase ()} → C2S) ` : ' '
333+ setStatus (t (' music.chartImported' , { diff: diffNames [diffIndex ], suffix }))
334+ await loadMusic ()
335+ selectMusic (selectedMusic .value )
336+ }
311337 } catch (e : any ) {
312- setStatus (t (' music.audioReplaceFailed ' , { error: e ?.response ?.data || e ?.message }))
338+ setStatus (t (' music.chartReplaceFailed ' , { error: e ?.response ?.data ?. error || e ?.message }))
313339 }
314340}
315341
316- const showChartOverlay = ref (false )
317-
318342async function handleReplaceChart(diffIndex : number ) {
319343 if (! selectedMusic .value ) return
320344 showChartOverlay .value = true
@@ -329,18 +353,7 @@ async function handleReplaceChart(diffIndex: number) {
329353 return
330354 }
331355 showChartOverlay .value = false
332- const file = await fileHandle .getFile ()
333- try {
334- const res = await replaceChart (selectedMusic .value .id , selectedMusic .value .assetDir , diffIndex , file )
335- if (res .imported ) {
336- const suffix = res .convertedFrom ? ` (${res .convertedFrom .toUpperCase ()} → C2S) ` : ' '
337- setStatus (t (' music.chartImported' , { diff: diffNames [diffIndex ], suffix }))
338- await loadMusic ()
339- selectMusic (selectedMusic .value )
340- }
341- } catch (e : any ) {
342- setStatus (t (' music.chartReplaceFailed' , { error: e ?.response ?.data ?.error || e ?.message }))
343- }
356+ await applyChart (diffIndex , await fileHandle .getFile ())
344357}
345358
346359function handleExportChart(diffIndex : number , format : ' c2s' | ' ugc' | ' sus' ) {
@@ -429,6 +442,35 @@ async function handleChangeId() {
429442 }
430443}
431444
445+ const rootRef = ref <HTMLDivElement >()
446+ const importModalRef = ref <InstanceType <typeof ImportMusicModal > | null >(null )
447+
448+ async function onDrop(_files : File [] | null , e : DragEvent ) {
449+ const items = e .dataTransfer ?.items
450+ if (! items ?.length ) return
451+ const handles = (await Promise .all (Array .from (items ).map (item => item .getAsFileSystemHandle ())))
452+ .filter (h => h != null )
453+ if (! handles .length ) return
454+
455+ if (handles [0 ].kind === ' directory' ) {
456+ importModalRef .value ?.startImportWithHandle (handles [0 ] as FileSystemDirectoryHandle )
457+ return
458+ }
459+
460+ if (handles .length !== 1 || isA000 .value || ! selectedMusic .value ) return
461+ const file = await (handles [0 ] as FileSystemFileHandle ).getFile ()
462+ const name = file .name .toLowerCase ()
463+ if (/ \. (png| jpe? g| bmp)$ / .test (name )) await applyJacket (file )
464+ else if (/ \. (wav| mp3| ogg| awb)$ / .test (name )) await applyAudio (file )
465+ else if (/ \. (c2s| ugc| sus)$ / .test (name )) await applyChart (selectedDiff .value , file )
466+ }
467+
468+ const { isOverDropZone } = useDropZone (rootRef , {
469+ onDrop ,
470+ multiple: true ,
471+ preventDefaultForUnhandled: true ,
472+ })
473+
432474const copyExportOptions = computed (() => {
433475 if (! selectedMusic .value ) return []
434476 const m = selectedMusic .value
@@ -482,7 +524,13 @@ const copyExportOptions = computed(() => {
482524 </script >
483525
484526<template >
485- <div class =" h-full flex" >
527+ <div ref =" rootRef" class =" h-full flex relative" >
528+ <div
529+ v-if =" isOverDropZone"
530+ class =" absolute inset-0 z-100 bg-white/50 backdrop-blur-sm flex items-center justify-center text-xl pointer-events-none"
531+ >
532+ {{ t('music.dropHint') }}
533+ </div >
486534 <div class =" w-40em max-w-[40vw] border-r border-white/10 flex flex-col relative" >
487535 <Transition
488536 enter-active-class="panel-transition"
@@ -551,15 +599,18 @@ const copyExportOptions = computed(() => {
551599 <template v-else-if =" isA000 " >
552600 <span class =" text-sm op-50" >{{ t('music.a000Hint') }}</span >
553601 </template >
554- <ImportMusicModal v-if =" ! isA000 " @imported =" refresh " />
602+ <ImportMusicModal ref="importModalRef" : show-button =" ! isA000 " @imported =" refresh " />
555603 </div >
556604
557605 <div v-if =" selectedMusic" class =" of-y-auto cst flex-1 min-h-0 p-6" >
558606 <div class =" flex gap-6 mb-6" >
559607 <img v-if =" selectedMusic.hasJacket" :src =" getJacketUrl(selectedMusic.id, selectedMusic.assetDir)" class =" w-48 h-48 rounded-lg object-cover shrink-0 cursor-pointer hover:op-80 transition-opacity" :title =" t('music.clickToReplaceJacket')" @click =" !isA000 && handleSetJacket()" />
560608 <div v-else class =" w-48 h-48 rounded-lg bg-white/10 flex items-center justify-center op-30 text-2xl shrink-0 cursor-pointer hover:op-50 transition-opacity" @click =" !isA000 && handleSetJacket()" >?</div >
561609 <div class =" flex-1 min-w-0" >
562- <h2 class =" text-xl font-bold mb-1" >{{ selectedMusic.name }}</h2 >
610+ <h2 class =" text-xl font-bold mb-1 flex items-center gap-2" >
611+ {{ selectedMusic.name }}
612+ <MusicIdConflictNotifier :id =" selectedMusic .id " :assetDir =" selectedMusic .assetDir " />
613+ </h2 >
563614 <p class =" op-60 mb-2" >{{ selectedMusic.artist }}</p >
564615 <p class =" text-sm op-40 mb-1" >{{ genreMap[selectedMusic.genreId] || '' }}</p >
565616 <p class =" text-sm op-40 mb-3" >{{ releaseTagMap[selectedMusic.releaseTagId] || selectedMusic.releaseTagStr || '' }}</p >
0 commit comments