Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button, Modal, Radio, showTransactionalDialog, addToast } from '@munet/
import { computed, defineComponent, ref, shallowRef } from 'vue';
import JacketBox from '../JacketBox';
import { DIFFICULTY } from '@/consts';
import { isReadOnlyDir } from "@/store/refs";
import api from '@/client/api';
import CheckingModal from "@/views/Charts/ImportCreateChartButton/ImportChartButton/CheckingModal";
import LevelTagsDisplay from "@/components/LevelTagsDisplay";
Expand All @@ -23,6 +24,10 @@ export let prepareReplaceChart = async (fileHandle?: FileSystemFileHandle, side?
export default defineComponent({
setup() {

// 替换一旦开始就把目标目录固定下来,确认时不能再读 selectedADir:
// 期间切到只读的 A000 会把写操作带偏
let replaceDir = selectedADir.value;

const checking = ref(false);
const file = shallowRef<File | null>(null);
const show = ref<"" | "ma2" | "maidata" | "failed">("");
Expand All @@ -40,6 +45,9 @@ export default defineComponent({

// 注:本功能的逻辑是,如果选择的是ma2文件,则只替换指定难度的谱面;如果选择的是maidata,则替换整首歌的所有难度。
prepareReplaceChart = async (fHandle?: FileSystemFileHandle, side?: ChartSide) => {
// A000 只读:替换谱面是写操作,入口处直接挡掉
if (isReadOnlyDir.value) return;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
replaceDir = selectedADir.value;
targetSide.value = side;
let selectedFile: File;
if (!fHandle) {
Expand Down Expand Up @@ -86,7 +94,7 @@ export default defineComponent({
const level = replacingMaidata ? -1 : selectedLevel.value;
const side = replacingMaidata ? undefined : targetSide.value;
show.value = "";
const result = (await api.ReplaceChart(selectMusicId.value, level, selectedADir.value, {
const result = (await api.ReplaceChart(selectMusicId.value, level, replaceDir, {
file: uploadFile,
shift: tempOption.value.shift,
side,
Expand Down
10 changes: 7 additions & 3 deletions MaiChartManager/Front/src/components/JacketBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { computed, defineComponent, PropType, ref } from "vue";
import noJacket from "@/assets/noJacket.webp";
import api, { getUrl } from "@/client/api";
import { showTransactionalDialog } from "@munet/ui";
import { globalCapture, selectedADir, selectedMusic } from "@/store/refs";
import { globalCapture, isReadOnlyDir, selectedADir, selectedMusic } from "@/store/refs";
import { MusicXmlWithABJacket } from "@/client/apiGen";
import { useI18n } from 'vue-i18n';
import { pickFile } from "@/utils/pickFile";
Expand All @@ -24,6 +24,8 @@ export default defineComponent({
if (props.upload)
upload = async (fileHandle?: FileSystemFileHandle) => {
if (!props.upload) return;
// A000 只读:点封面或拖拽换图都会走到这里
if (isReadOnlyDir.value) return;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
try {
let file: File;
if (!fileHandle) {
Expand All @@ -35,7 +37,8 @@ export default defineComponent({
file = await fileHandle.getFile();
}

const res = await api.SetMusicJacket(props.info.id!, selectedADir.value, { file });
// 用曲目自己的 assetDir:选文件是异步的,期间目录可能被切走/切到 A000
const res = await api.SetMusicJacket(props.info.id!, props.info.assetDir ?? selectedADir.value, { file });
if (res.error) {
const error = res.error as any;
await showTransactionalDialog(t('jacket.setFailed'), error.message || error, undefined, true);
Expand All @@ -56,6 +59,7 @@ export default defineComponent({
}
}

return () => <img src={jacketUrl.value} class={`object-fill rounded-lg ${props.upload && 'cursor-pointer'}`} onClick={props.upload ? () => upload() : undefined} />
const canUpload = computed(() => props.upload && !isReadOnlyDir.value);
return () => <img src={jacketUrl.value} class={`object-fill rounded-lg ${canUpload.value && 'cursor-pointer'}`} onClick={canUpload.value ? () => upload() : undefined} />
}
})
5 changes: 4 additions & 1 deletion MaiChartManager/Front/src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ assetDir:
music:
list:
title: Chart List
searchPlaceholder: Search title / composer / charter / alias / ID
searchPlaceholder: Search title / composer / charter / ID
tableTitle: Title
tableCover: Cover
tableVersion: Version
Expand All @@ -71,6 +71,8 @@ music:
filterStandard: Standard
filterDX: DX
filterUtage: Utage
noMatch: No matching songs
clearSearch: Clear search
unknown: Unknown
filterBasic: Basic
filterMaster: Master
Expand All @@ -81,6 +83,7 @@ music:
sortByName: Name
sortByVersion: Release
edit:
selectHint: Select a chart to edit
name: Song Title
artist: Artist
bpm: BPM
Expand Down
5 changes: 4 additions & 1 deletion MaiChartManager/Front/src/locales/zh-TW.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ assetDir:
music:
list:
title: 譜面列表
searchPlaceholder: 搜尋名稱 / 作曲 / 譜師 / 別名 / ID
searchPlaceholder: 搜尋名稱 / 作曲 / 譜師 / ID
tableTitle: 標題
tableCover: 封面
tableVersion: 版本
Expand All @@ -69,6 +69,8 @@ music:
filterStandard: 標準
filterDX: DX
filterUtage: 宴會場
noMatch: 沒有符合的曲目
clearSearch: 清除搜尋
unknown: 未知
filterBasic: 綠
filterMaster: 紫
Expand All @@ -79,6 +81,7 @@ music:
sortByName: 名稱
sortByVersion: 更新時間
edit:
selectHint: 請選擇譜面以編輯
name: 歌曲名稱
artist: 作者
bpm: BPM
Expand Down
5 changes: 4 additions & 1 deletion MaiChartManager/Front/src/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ assetDir:
music:
list:
title: 谱面列表
searchPlaceholder: 搜索名称 / 作曲 / 谱师 / 别名 / ID
searchPlaceholder: 搜索名称 / 作曲 / 谱师 / ID
tableTitle: 标题
tableCover: 封面
tableVersion: 版本
Expand All @@ -69,6 +69,8 @@ music:
filterStandard: 标准
filterDX: DX
filterUtage: 宴会场
noMatch: 没有匹配的曲目
clearSearch: 清空搜索
unknown: 未知
filterBasic: 绿
filterMaster: 紫
Expand All @@ -79,6 +81,7 @@ music:
sortByName: 名称
sortByVersion: 更新时间
edit:
selectHint: 请选择谱面以编辑
name: 名称
artist: 作者
bpm: BPM
Expand Down
14 changes: 11 additions & 3 deletions MaiChartManager/Front/src/store/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export const musicList = computed(() =>
export const selectedMusic = computed(() => musicList.value.find(m => m.id === selectMusicId.value));
export const selectedLevel = ref(0);

// A000 是游戏本体目录,只读。所有写入入口(保存/删除/导入/创建/替换谱面)都要先看它,
// 只靠禁用样式挡不住键盘和 Ctrl+S 这类快捷操作。
export const isReadOnlyDir = computed(() => selectedADir.value === 'A000');

// 如ReplaceChart等后端接口可能会涉及对MusicXml中的信息进行修改后保存。此时前端updateMusicList时会发现相关数据出现变更,触发了MusicEdit/ChartPanel中的watch,造成发送多余的edit请求、同时modified也被错误设置为true。
// 因此,对于涉及内部对xml进行修改后会自动保存的后端接口,可以在updateMusicList期间打开本选项,以阻止MusicEdit/ChartPanel中的sync动作。
export const disableSync = ref(false);
Expand All @@ -85,9 +89,13 @@ export { aquaMaiConfig } from "@/views/ModManager/refs";

export const saveMusicIfNeeded = async (id: number) => {
if (!id) return;
const music = musicListAll.value.find(m => m.id === id);
if (!music?.modified) return;
await api.SaveMusic(id, selectedADir.value);
// 同 ID 曲目可能同时存在于多个目录(A000 与用户目录),按 id 单独取会命中别目录那条、
// 因它的 modified 是 false 而误判成「不用保存」。modified 只会标在正在编辑的曲目上,拿它定位即可。
const music = musicListAll.value.find(m => m.id === id && m.modified);
// 写入用曲目自己的 assetDir:这里是被失焦 / 切歌自动触发的,那一刻 selectedADir 可能已经变了,
// 切到只读的 A000 时更不能把待保存的内容写进去
if (!music?.assetDir || music.assetDir === 'A000') return;
await api.SaveMusic(id, music.assetDir);
await updateMusicList();
}

Expand Down
6 changes: 5 additions & 1 deletion MaiChartManager/Front/src/utils/musicSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ export const matchMusicKeyword = (music: MusicXmlWithABJacket, keyword: string)
const value = keyword.trim().toLowerCase();
if (!value) return true;

// 列表里的 ID 按 6 位补零显示(000123),所以补零形式也要能搜到
const rawId = (music.id ?? 0).toString();

return (music.name ?? '').toLowerCase().includes(value)
|| (music.artist ?? '').toLowerCase().includes(value)
|| (music.id ?? 0).toString().includes(value)
|| rawId.includes(value)
|| rawId.padStart(6, '0').includes(value)
|| (music.charts ?? []).some(chart => chart.designer?.toLowerCase().includes(value));
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { computed, defineComponent, ref, watch } from "vue";
import { Button, Modal, NumberInput, Radio } from "@munet/ui";
import { assetDirs, musicList, selectedADir, selectMusicId, updateMusicList } from "@/store/refs";
import { assetDirs, isReadOnlyDir, musicList, selectedADir, selectMusicId, updateMusicList } from "@/store/refs";
import dxIcon from "@/assets/dxIcon.png";
import stdIcon from "@/assets/stdIcon.png";
import api from "@/client/api";
Expand Down Expand Up @@ -35,6 +35,8 @@ export default defineComponent({
})

const save = async () => {
// 对话框打开期间目录可能被切到只读的 A000,保存前重新判一次
if (isReadOnlyDir.value) return;
show.value = false;
await api.AddMusic(id.value, selectedADir.value);
await updateMusicList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SelectFileTypeTip from "./SelectFileTypeTip";
import { LicenseStatus, MessageLevel } from "@/client/apiGen";
import CheckingModal from "./CheckingModal";
import api, { getUrl } from "@/client/api";
import { globalCapture, selectedADir, selectMusicId, updateMusicList, version as appVersion } from "@/store/refs";
import { globalCapture, isReadOnlyDir, selectedADir, selectMusicId, updateMusicList, version as appVersion } from "@/store/refs";
import { appSettings } from "@/store/settings";
import ErrorDisplayIdInput from "./ErrorDisplayIdInput";
import ImportStepDisplay from "./ImportStepDisplay";
Expand Down Expand Up @@ -97,14 +97,18 @@ export default defineComponent({
return !reject;
}

// 导入一旦开始就把目标目录固定下来:中途切目录(尤其是切到只读的 A000)
// 也不能把写操作带偏,所有写入都用这个快照
let importDir = selectedADir.value;

const uploadMovie = (id: number, movie: File, offset: number) => new Promise<void>((resolve, reject) => {
currentMovieProgress.value = 0;
const body = new FormData();
body.append('padding', offset.toString());
body.append('file', movie);
body.append('file', movie);
const controller = new AbortController();
fetchEventSource(getUrl(`SetMovieApi/${selectedADir.value}/${id}`), {
fetchEventSource(getUrl(`SetMovieApi/${importDir}/${id}`), {
signal: controller.signal,
method: 'PUT',
body,
Expand Down Expand Up @@ -144,7 +148,7 @@ export default defineComponent({
try {
music.importStep = IMPORT_STEP.create;

const createRet = (await api.AddMusic(music.id, selectedADir.value)).data;
const createRet = (await api.AddMusic(music.id, importDir)).data;
if (createRet) throw new Error(createRet);

music.importStep = IMPORT_STEP.chart;
Expand All @@ -163,13 +167,13 @@ export default defineComponent({
utageRightLevel: music.utageMapping.rightLevel,
} : {}),
debug: import.meta.env.DEV,
assetDir: selectedADir.value,
assetDir: importDir,
})).data;

errors.value.push(...res.errors!.map(it => ({ ...it, name: music.name })));
if (res.fatal) {
try {
await api.DeleteMusic(music.id, selectedADir.value);
await api.DeleteMusic(music.id, importDir);
} catch {
}
return;
Expand All @@ -181,14 +185,14 @@ export default defineComponent({
// 音频上应该应用的延迟audioPadding = 谱面上应用的延迟chartPadding - &first
let audioPadding = chartPadding - music.first;

await api.SetAudio(music.id, selectedADir.value, { file: music.track, padding: audioPadding, ignoreGapless: !!tempOptions.value.ignoreGapless });
await api.SetAudio(music.id, importDir, { file: music.track, padding: audioPadding, ignoreGapless: !!tempOptions.value.ignoreGapless });
if (music.previewTime) {
// 因为&demo_seek和&demo_len都是相对于原始音源的,所以这里的时间必须要扣掉上面SetAudio时应用的padding。
// 根据SetAudioApi的定义,padding为正表示歌曲前面被加了空白,因此原本的预览段在新的裁剪后的音频中的位置也要相应后移;padding为负反之。
// 因此,直接给startTime和endTime都加上audioPadding即可。
music.previewTime.startTime! += audioPadding;
music.previewTime.endTime! += audioPadding
await api.SetAudioPreview(music.id, selectedADir.value, music.previewTime);
await api.SetAudioPreview(music.id, importDir, music.previewTime);
}

if (music.movie && !tempOptions.value.disableBga) {
Expand All @@ -202,7 +206,7 @@ export default defineComponent({
}

music.importStep = IMPORT_STEP.jacket;
if (music.bg) await api.SetMusicJacket(music.id, selectedADir.value, { file: music.bg });
if (music.bg) await api.SetMusicJacket(music.id, importDir, { file: music.bg });

music.importStep = IMPORT_STEP.finish;
} catch (e) {
Expand All @@ -218,14 +222,17 @@ export default defineComponent({
// 如果是在创建乐曲这步就挂了,说明乐曲XML没有创建成功,则不需要删除乐曲。
// 否则,在ID冲突的情况下,会把原本的乐曲给删除掉,见 https://github.com/MuNET-OSS/MaiChartManager/issues/34
try {
await api.DeleteMusic(music.id, selectedADir.value);
await api.DeleteMusic(music.id, importDir);
} catch {
}
}
}
}

startProcess = async (dir?: ImportDirectory | ImportDirectory[]) => {
// A000 只读:拖拽进来的导入也要挡住,不只是点按钮
if (isReadOnlyDir.value) return;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
importDir = selectedADir.value;
let id = getNextUnusedMusicId();
const usedIds = [] as number[];
errors.value = [];
Expand Down Expand Up @@ -305,7 +312,7 @@ export default defineComponent({
}
}

return () => <Button onClick={() => startProcess()} variant="secondary">
return () => <Button onClick={() => startProcess()} disabled={isReadOnlyDir.value} variant="secondary">
{t('chart.import.title')}
<SelectFileTypeTip show={step.value === STEP.selectFile} closeModal={closeModal} />
<CheckingModal title={t('chart.import.checkingTitle')} show={step.value === STEP.checking} closeModal={closeModal} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ import { Button, DropMenu } from "@munet/ui";
import { computed, defineComponent, ref } from "vue";
import ImportChartButton from "./ImportChartButton";
import CreateMusicModal from "@/views/Charts/ImportCreateChartButton/CreateMusicModal";
import { isReadOnlyDir } from "@/store/refs";
import { useI18n } from 'vue-i18n';

export default defineComponent({
setup() {
const showCreate = ref(false);
const { t } = useI18n();
const options = computed(() => [
{ label: t('chart.import.create'), action: () => { showCreate.value = true; } },
{ label: t('chart.import.create'), action: () => { showCreate.value = true; }, disabled: isReadOnlyDir.value },
]);

return () => <div class="flex relative">
<ImportChartButton />
<DropMenu options={options.value} buttonText="" align="right">
{{
trigger: (onClick: any) => <Button variant="secondary" class="px-.5 b-l b-l-solid b-l-[rgba(255,255,255,0.5)]" onClick={onClick}>
trigger: (onClick: any) => <Button variant="secondary" disabled={isReadOnlyDir.value} class="px-.5 b-l b-l-solid b-l-[rgba(255,255,255,0.5)]" onClick={onClick}>
<span class="i-mdi-arrow-down-drop text-6 translate-y-.25" />
</Button>
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import PreviewChartButton from "@/views/Charts/MusicEdit/PreviewChartButton";
import { useI18n } from 'vue-i18n';
import { Button, CheckBox, NumberInput, Select, TextInput } from "@munet/ui";
import { prepareReplaceChart } from "@/components/DragDropDispatcher/ReplaceChartModal";
import { isReadOnlyDir } from "@/store/refs";
import type { ChartSide } from "@/views/Charts/MusicEdit/PreviewChartButton";
import styles from './index.module.sass';

Expand All @@ -31,7 +32,8 @@ export default defineComponent({
})

const sync = (key: keyof Chart, method: Function) => async () => {
if (disableSync.value || !props.chart) return;
// A000 只读:这里目前靠输入组件的 disabled 注入挡住,入口处再拦一层,和保存 / 删除的口径一致
if (disableSync.value || isReadOnlyDir.value || !props.chart) return;
selectedMusic.value!.modified = true;
await method(props.songId, props.chartIndex, selectedADir.value, props.chart[key]!);
}
Expand All @@ -46,15 +48,15 @@ export default defineComponent({
const sideRow = (side: ChartSide, label: string) => <div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-500">{label}</span>
<PreviewChartButton songId={props.songId} level={props.chartIndex} side={side}/>
<Button onClick={() => prepareReplaceChart(undefined, side)}>
<Button onClick={() => prepareReplaceChart(undefined, side)} disabled={isReadOnlyDir.value}>
{t('music.edit.replaceChart')}
</Button>
</div>;

return () => <div class="flex flex-col gap-2">
{!props.doublePlayer && <div class="absolute right-0 top-0 m-xy mt-2 z-2 flex gap-2">
<PreviewChartButton songId={props.songId} level={props.chartIndex}/>
<Button onClick={() => prepareReplaceChart()}>
<Button onClick={() => prepareReplaceChart()} disabled={isReadOnlyDir.value}>
{t('music.edit.replaceChart')}
</Button>
</div>}
Expand Down
Loading
Loading