11<script setup lang="ts">
22import { ref , computed , onMounted } from ' vue'
33import { useI18n } from ' vue-i18n'
4- import { Button , TextInput , NumberInput , Select , Modal , DropMenu , addToast } from ' @munet/ui'
4+ import { Button , TextInput , NumberInput , Select , Modal , addToast } from ' @munet/ui'
55import { getGenreMap , getSources , importMusicCheck , importMusicExecute } from ' @/api'
6- import { selectedSource } from ' @/store/refs'
76import type { ImportCheckResult } from ' @/api'
7+ import { selectedSource } from ' @/store/refs'
88import BottomOverlay from ' @/components/BottomOverlay.vue'
99import FileTypeIcon from ' @/components/FileTypeIcon.vue'
1010
@@ -14,45 +14,31 @@ const { t } = useI18n()
1414const loading = ref (false )
1515const step = ref <' idle' | ' picking' | ' checking' | ' form' | ' executing' >(' idle' )
1616
17- const chartFile = ref <File | null >( null )
17+ const chartFiles = ref <File []>([] )
1818const audioFile = ref <File | null >(null )
1919const coverFile = ref <File | null >(null )
2020const checkResult = ref <ImportCheckResult | null >(null )
2121
2222const title = ref (' ' )
2323const artist = ref (' ' )
2424const genreId = ref (0 )
25- const difficulty = ref (3 )
26- const level = ref (10 )
27- const levelDecimal = ref (0 )
28- const constant = computed ({
29- get : () => level .value + levelDecimal .value / 100 ,
30- set : (v : number ) => {
31- level .value = Math .floor (v )
32- levelDecimal .value = Math .round ((v - Math .floor (v )) * 100 )
33- },
34- })
3525const targetDir = ref (' ' )
3626const musicId = ref (8000 )
3727
28+ const diffNames = [' BASIC' , ' ADVANCED' , ' EXPERT' , ' MASTER' , ' ULTIMA' ]
29+
3830const genreMap = ref <Record <number , string >>({})
3931const sources = ref <string []>([])
4032
4133const showForm = computed (() => step .value === ' form' || step .value === ' executing' )
34+ const coverUrl = computed (() => coverFile .value ? URL .createObjectURL (coverFile .value ) : ' ' )
4235
4336const genreOptions = computed (() =>
4437 Object .entries (genreMap .value ).map (([id , name ]) => ({ label: name , value: Number (id ) }))
4538)
4639const sourceOptions = computed (() =>
4740 sources .value .filter (s => s !== ' A000' ).map (s => ({ label: s , value: s }))
4841)
49- const diffOptions = [
50- { label: ' BASIC' , value: 0 },
51- { label: ' ADVANCED' , value: 1 },
52- { label: ' EXPERT' , value: 2 },
53- { label: ' MASTER' , value: 3 },
54- { label: ' ULTIMA' , value: 4 },
55- ]
5642
5743onMounted (async () => {
5844 const [g, s] = await Promise .all ([getGenreMap (), getSources ()])
@@ -64,6 +50,14 @@ onMounted(async () => {
6450 }
6551})
6652
53+ function findAllByExt(files : File [], exts : string []): File [] {
54+ return files .filter (f => exts .some (e => f .name .toLowerCase ().endsWith (e )))
55+ }
56+
57+ function findByExt(files : File [], exts : string []): File | null {
58+ return files .find (f => exts .some (e => f .name .toLowerCase ().endsWith (e ))) ?? null
59+ }
60+
6761function parseFolderName(name : string ) {
6862 const cleaned = name .replace (/ \s * \( v\d + \) \s * $ / , ' ' ).trim ()
6963 const sep = cleaned .indexOf (' - ' )
@@ -76,15 +70,11 @@ function parseFolderName(name: string) {
7670 }
7771}
7872
79- function findByExt(files : File [], exts : string []): File | null {
80- return files .find (f => exts .some (e => f .name .toLowerCase ().endsWith (e ))) ?? null
81- }
82-
8373async function startImport() {
8474 step .value = ' picking'
8575 let dirHandle: FileSystemDirectoryHandle
8676 try {
87- dirHandle = await ( window as any ) .showDirectoryPicker ({ id: ' import-chart' , startIn: ' downloads' })
77+ dirHandle = await window .showDirectoryPicker ({ id: ' import-chart' , startIn: ' downloads' })
8878 } catch {
8979 step .value = ' idle'
9080 return
@@ -93,15 +83,15 @@ async function startImport() {
9383 step .value = ' checking'
9484
9585 const files: File [] = []
96- for await (const entry of ( dirHandle as any ) .values ()) {
97- if (entry .kind === ' file' ) files .push (await entry .getFile ())
86+ for await (const entry of dirHandle .values ()) {
87+ if (entry .kind === ' file' ) files .push (await ( entry as FileSystemFileHandle ) .getFile ())
9888 }
9989
100- chartFile .value = findByExt (files , [' .ugc' , ' .c2s' , ' .sus' ])
90+ chartFiles .value = findAllByExt (files , [' .ugc' , ' .c2s' , ' .sus' ])
10191 audioFile .value = findByExt (files , [' .wav' , ' .mp3' , ' .ogg' ])
10292 coverFile .value = findByExt (files , [' .png' , ' .jpg' , ' .jpeg' ])
10393
104- if (! chartFile .value ) {
94+ if (chartFiles .value . length === 0 ) {
10595 addToast ({ message: t (' music.importNoChart' ), type: ' error' })
10696 step .value = ' idle'
10797 return
@@ -113,11 +103,8 @@ async function startImport() {
113103 }
114104
115105 try {
116- checkResult .value = await importMusicCheck (chartFile .value )
106+ checkResult .value = await importMusicCheck (chartFiles .value )
117107 musicId .value = checkResult .value .suggestedId
118- difficulty .value = checkResult .value .difficulty
119- level .value = checkResult .value .level
120- levelDecimal .value = checkResult .value .levelDecimal
121108 if (checkResult .value .title ) title .value = checkResult .value .title
122109 else parseFolderName (dirHandle .name )
123110 if (checkResult .value .artist ) artist .value = checkResult .value .artist
@@ -129,24 +116,21 @@ async function startImport() {
129116}
130117
131118async function doImport() {
132- if (! chartFile .value || ! audioFile .value ) return
119+ if (chartFiles .value . length === 0 || ! audioFile .value ) return
133120 step .value = ' executing'
134121 loading .value = true
135122
136123 try {
137124 const genreName = genreMap .value [genreId .value ] || ' '
138125 const result = await importMusicExecute ({
139- chart: chartFile .value ,
126+ charts: chartFiles .value ,
140127 audio: audioFile .value ,
141128 cover: coverFile .value ?? undefined ,
142129 id: musicId .value ,
143130 title: title .value ,
144131 artist: artist .value ,
145132 genreId: genreId .value ,
146133 genreName ,
147- difficulty: difficulty .value ,
148- level: level .value ,
149- levelDecimal: levelDecimal .value ,
150134 targetDir: targetDir .value ,
151135 })
152136
@@ -203,15 +187,28 @@ defineExpose({ startImport })
203187 </div >
204188
205189 <template v-if =" showForm " >
190+ <div v-if =" coverFile" class =" flex justify-center" >
191+ <img :src =" coverUrl" class =" w-32 h-32 rounded-lg object-cover" />
192+ </div >
193+
206194 <div class =" flex flex-col gap-1 text-sm op-70 bg-white/5 rd p-3" >
207195 <div >{{ t('music.importFoundFiles') }}:</div >
208196 <div class =" flex gap-3 flex-wrap" >
209- <span v-if = " chartFile " >{{ chartFile .name }}</span >
197+ <span v-for = " f in chartFiles " :key = " f.name " >{{ f .name }}</span >
210198 <span v-if =" audioFile" >{{ audioFile.name }}</span >
211199 <span v-if =" coverFile" >{{ coverFile.name }}</span >
212200 </div >
213201 </div >
214202
203+ <div v-if =" checkResult?.difficulties?.length" class =" text-sm bg-white/5 rd p-3" >
204+ <div class =" op-70 mb-2" >{{ t('music.importDifficulties') }}:</div >
205+ <div class =" flex gap-2 flex-wrap" >
206+ <span v-for =" d in checkResult.difficulties" :key =" d.difficulty" class =" px-2 py-0.5 rd bg-white/10" >
207+ {{ diffNames[d.difficulty] || `DIFF ${d.difficulty}` }} {{ (d.level + d.levelDecimal / 100).toFixed(1) }}
208+ </span >
209+ </div >
210+ </div >
211+
215212 <div v-if =" checkResult?.alerts?.length" class =" text-sm c-orange bg-orange/10 rd p-2" >
216213 <div v-for =" (a, i) in checkResult.alerts" :key =" i" >{{ a }}</div >
217214 </div >
@@ -229,14 +226,6 @@ defineExpose({ startImport })
229226 <label class =" block text-sm op-60 mb-1" >{{ t('music.genre') }}</label >
230227 <Select :options =" genreOptions " v-model :value =" genreId " :disabled =" loading " />
231228 </div >
232- <div >
233- <label class =" block text-sm op-60 mb-1" >{{ t('music.importDifficulty') }}</label >
234- <Select :options =" diffOptions " v-model :value =" difficulty " :disabled =" loading " />
235- </div >
236- <div >
237- <label class =" block text-sm op-60 mb-1" >{{ t('music.chartConstant') }}</label >
238- <NumberInput v-model :value =" constant " :min =" 0 " :max =" 15.9 " :step =" 0.1 " :decimal =" 1 " :disabled =" loading " class="w-full" />
239- </div >
240229 <div >
241230 <label class =" block text-sm op-60 mb-1" >{{ t('music.importTargetDir') }}</label >
242231 <Select :options =" sourceOptions " v-model :value =" targetDir " :disabled =" loading " />
0 commit comments