Skip to content

Commit e7f9f5b

Browse files
committed
导出
1 parent 9c0b6e0 commit e7f9f5b

File tree

8 files changed

+987
-5
lines changed

8 files changed

+987
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ riderModule.iml
66
.vs
77
*.user
88
/MaiChartManager/Resources/AquaMai.dll
9+
node_modules

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dotrush.roslyn.projectOrSolutionFiles": [
3+
"d:\\Projects\\mai\\Sitreamai\\Sitreamai.sln"
4+
]
5+
}

MaiChartManager/Front/src/components/MusicList/BatchActionButton/ChooseAction.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { defineComponent, PropType, ref } from "vue";
22
import { MusicXmlWithABJacket } from "@/client/apiGen";
3-
import { NButton, NFlex, NPopover, NRadio, NRadioGroup, useMessage, useNotification } from "naive-ui";
3+
import { NButton, NFlex, NPopover, NRadio, NRadioGroup, NSelect, useMessage, useNotification } from "naive-ui";
44
import { STEP } from "@/components/MusicList/BatchActionButton/index";
55
import api from "@/client/api";
66
import { showNeedPurchaseDialog, updateMusicList, version } from "@/store/refs";
77
import remoteExport from "@/components/MusicList/BatchActionButton/remoteExport";
8+
import TransitionVertical from "@/components/TransitionVertical.vue";
9+
import { useStorage } from "@vueuse/core";
810

911
export enum OPTIONS {
1012
None,
@@ -17,13 +19,21 @@ export enum OPTIONS {
1719
CreateNewOptMa2_103,
1820
}
1921

22+
export enum MAIDATA_SUBDIR {
23+
None,
24+
Genre,
25+
Version,
26+
}
27+
2028
export default defineComponent({
2129
props: {
2230
selectedMusic: Array as PropType<MusicXmlWithABJacket[]>,
2331
continue: {type: Function, required: true},
2432
},
2533
setup(props) {
2634
const selectedOption = ref(OPTIONS.None);
35+
// 导出为 Maidata 的子目录选项
36+
const selectedMaidataSubdir = useStorage('selectedMaidataSubdir', MAIDATA_SUBDIR.None);
2737
const load = ref(false);
2838
const notify = useNotification();
2939

@@ -57,7 +67,7 @@ export default defineComponent({
5767
showNeedPurchaseDialog.value = true
5868
break;
5969
}
60-
remoteExport(props.continue as any, props.selectedMusic!, selectedOption.value, notify);
70+
remoteExport(props.continue as any, props.selectedMusic!, selectedOption.value, notify, selectedMaidataSubdir.value);
6171
break;
6272
}
6373
}
@@ -107,6 +117,11 @@ export default defineComponent({
107117
<NRadio value={OPTIONS.ConvertToMaidataIgnoreVideo}>
108118
转换为 Maidata(无 BGA)
109119
</NRadio>
120+
121+
<TransitionVertical>
122+
{(selectedOption.value === OPTIONS.ConvertToMaidata || selectedOption.value === OPTIONS.ConvertToMaidataIgnoreVideo) &&
123+
<NSelect v-model:value={selectedMaidataSubdir.value} options={[{label: '平铺文件夹', value: MAIDATA_SUBDIR.None}, {label: '按流派分组', value: MAIDATA_SUBDIR.Genre}, {label: '按版本分组', value: MAIDATA_SUBDIR.Version}]}/>}
124+
</TransitionVertical>
110125
</NFlex>
111126
</NRadioGroup>
112127
<NFlex justify="end">

MaiChartManager/Front/src/components/MusicList/BatchActionButton/remoteExport.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { currentProcessItem, progressAll, progressCurrent } from "@/components/M
33
import { MusicXmlWithABJacket } from "@/client/apiGen";
44
import { ZipReader } from "@zip.js/zip.js";
55
import getSubDirFile from "@/utils/getSubDirFile";
6-
import { OPTIONS } from "@/components/MusicList/BatchActionButton/ChooseAction";
6+
import { MAIDATA_SUBDIR, OPTIONS } from "@/components/MusicList/BatchActionButton/ChooseAction";
77
import { useNotification } from "naive-ui";
88
import { getUrl } from "@/client/api";
9+
import { addVersionList, genreList } from "@/store/refs";
910

10-
export default async (setStep: (step: STEP) => void, musicList: MusicXmlWithABJacket[], action: OPTIONS, notify: ReturnType<typeof useNotification>) => {
11+
export default async (setStep: (step: STEP) => void, musicList: MusicXmlWithABJacket[], action: OPTIONS, notify: ReturnType<typeof useNotification>, dirOption: MAIDATA_SUBDIR) => {
1112
let folderHandle: FileSystemDirectoryHandle;
1213
try {
1314
folderHandle = await window.showDirectoryPicker({
@@ -61,7 +62,19 @@ export default async (setStep: (step: STEP) => void, musicList: MusicXmlWithABJa
6162
}
6263
let filename = entry.filename;
6364
if (action === OPTIONS.ConvertToMaidata || action === OPTIONS.ConvertToMaidataIgnoreVideo) {
64-
filename = `${sanitizeFilename(music.name!)}${music.id! > 1e4 && music.id! < 2e4 ? ' [DX]' : ''}/${filename}`;
65+
let dir = '';
66+
switch (dirOption) {
67+
case MAIDATA_SUBDIR.Genre:
68+
dir = genreList.value.find(genre => genre.id === music.genreId)?.genreName || '未知';
69+
break;
70+
case MAIDATA_SUBDIR.Version:
71+
dir = addVersionList.value.find(version => version.id === music.addVersionId)?.genreName || '未知';
72+
break;
73+
}
74+
if (dir) {
75+
dir = sanitizeFilename(dir) + '/';
76+
}
77+
filename = `${dir}${sanitizeFilename(music.name!)}${music.id! > 1e4 && music.id! < 2e4 ? ' [DX]' : ''}/${filename}`;
6578
}
6679
const fileHandle = await getSubDirFile(folderHandle, filename);
6780
const writable = await fileHandle.createWritable();
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<script setup lang="ts">
2+
// From: https://stackoverflow.com/a/71426342/22392721
3+
interface Props {
4+
duration?: number
5+
easingEnter?: string
6+
easingLeave?: string
7+
opacityClosed?: number
8+
opacityOpened?: number
9+
}
10+
11+
const props = withDefaults(defineProps<Props>(), {
12+
duration: 250,
13+
easingEnter: 'ease-in-out',
14+
easingLeave: 'ease-in-out',
15+
opacityClosed: 0,
16+
opacityOpened: 1,
17+
})
18+
19+
const closed = '0px'
20+
21+
interface initialStyle {
22+
height: string
23+
width: string
24+
position: string
25+
visibility: string
26+
overflow: string
27+
paddingTop: string
28+
paddingBottom: string
29+
borderTopWidth: string
30+
borderBottomWidth: string
31+
marginTop: string
32+
marginBottom: string
33+
}
34+
35+
function getElementStyle(element: HTMLElement) {
36+
return {
37+
height: element.style.height,
38+
width: element.style.width,
39+
position: element.style.position,
40+
visibility: element.style.visibility,
41+
overflow: element.style.overflow,
42+
paddingTop: element.style.paddingTop,
43+
paddingBottom: element.style.paddingBottom,
44+
borderTopWidth: element.style.borderTopWidth,
45+
borderBottomWidth: element.style.borderBottomWidth,
46+
marginTop: element.style.marginTop,
47+
marginBottom: element.style.marginBottom,
48+
}
49+
}
50+
51+
let animation: Animation | null = null
52+
let lastElement: HTMLElement | null = null
53+
54+
function prepareElement(element: HTMLElement, initialStyle: initialStyle) {
55+
const { width } = getComputedStyle(element)
56+
element.style.width = width
57+
element.style.position = 'absolute'
58+
element.style.visibility = 'hidden'
59+
element.style.height = ''
60+
const { height } = getComputedStyle(element)
61+
element.style.width = initialStyle.width
62+
element.style.position = initialStyle.position
63+
element.style.visibility = initialStyle.visibility
64+
element.style.height = closed
65+
element.style.overflow = 'hidden'
66+
return initialStyle.height && initialStyle.height !== closed
67+
? initialStyle.height
68+
: height
69+
}
70+
71+
function animateTransition(
72+
element: HTMLElement,
73+
initialStyle: initialStyle,
74+
done: () => void,
75+
keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
76+
options?: number | KeyframeAnimationOptions,
77+
) {
78+
lastElement = element
79+
animation = element.animate(keyframes, options)
80+
// Set height to 'auto' to restore it after animation
81+
element.style.height = initialStyle.height
82+
animation.onfinish = () => {
83+
element.style.overflow = initialStyle.overflow
84+
done()
85+
}
86+
}
87+
88+
function getEnterKeyframes(height: string, initialStyle: initialStyle) {
89+
return [
90+
{
91+
height: closed,
92+
opacity: props.opacityClosed,
93+
paddingTop: closed,
94+
paddingBottom: closed,
95+
borderTopWidth: closed,
96+
borderBottomWidth: closed,
97+
marginTop: closed,
98+
marginBottom: closed,
99+
},
100+
{
101+
height,
102+
opacity: props.opacityOpened,
103+
paddingTop: initialStyle.paddingTop,
104+
paddingBottom: initialStyle.paddingBottom,
105+
borderTopWidth: initialStyle.borderTopWidth,
106+
borderBottomWidth: initialStyle.borderBottomWidth,
107+
marginTop: initialStyle.marginTop,
108+
marginBottom: initialStyle.marginBottom,
109+
},
110+
]
111+
}
112+
113+
function cancelAnimation(HTMLElement: HTMLElement, overflow: string, done: () => void) {
114+
if (HTMLElement !== lastElement) return false;
115+
if (!animation) return false;
116+
if (animation.playState !== 'running') return false;
117+
animation.onfinish = () => {
118+
HTMLElement.style.overflow = overflow;
119+
done();
120+
};
121+
animation.reverse();
122+
return true;
123+
}
124+
125+
function enterTransition(element: Element, done: () => void) {
126+
const HTMLElement = element as HTMLElement
127+
const initialStyle = getElementStyle(HTMLElement)
128+
if (cancelAnimation(HTMLElement, initialStyle.overflow, done)) return;
129+
const height = prepareElement(HTMLElement, initialStyle)
130+
const keyframes = getEnterKeyframes(height, initialStyle)
131+
const options = { duration: props.duration, easing: props.easingEnter }
132+
animateTransition(HTMLElement, initialStyle, done, keyframes, options)
133+
}
134+
135+
function leaveTransition(element: Element, done: () => void) {
136+
const HTMLElement = element as HTMLElement
137+
const initialStyle = getElementStyle(HTMLElement)
138+
if (cancelAnimation(HTMLElement, initialStyle.overflow, done)) return;
139+
const { height } = getComputedStyle(HTMLElement)
140+
HTMLElement.style.height = height
141+
HTMLElement.style.overflow = 'hidden'
142+
const keyframes = getEnterKeyframes(height, initialStyle).reverse()
143+
const options = { duration: props.duration, easing: props.easingLeave }
144+
animateTransition(HTMLElement, initialStyle, done, keyframes, options)
145+
}
146+
</script>
147+
148+
<template>
149+
<Transition :css="false" @enter="enterTransition" @leave="leaveTransition">
150+
<slot />
151+
</Transition>
152+
</template>

MaiChartManager/Front/vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { sentryVitePlugin } from "@sentry/vite-plugin";
22
import { defineConfig } from 'vite';
33
import vueJsx from '@vitejs/plugin-vue-jsx';
4+
import vue from '@vitejs/plugin-vue';
45
import UnoCSS from 'unocss/vite';
56
import ViteYaml from '@modyfi/vite-plugin-yaml';
67
import svgLoader from 'vite-svg-loader'
78

89
// https://vitejs.dev/config/
910
export default defineConfig(({command}) => ({
1011
plugins: [
12+
vue(),
1113
vueJsx(),
1214
UnoCSS(),
1315
ViteYaml(),

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"devDependencies": {
3+
"@vitejs/plugin-vue": "^6.0.1"
4+
}
5+
}

0 commit comments

Comments
 (0)