Skip to content

Commit bcfa5b5

Browse files
authored
Merge pull request #117 from LuSrackhall/ChaiFen-Keytone_album
#116 Chai fen keytone album
2 parents 727452a + ebac6ef commit bcfa5b5

25 files changed

+7068
-3539
lines changed

frontend/src/components/Keytone_album.vue

Lines changed: 275 additions & 3539 deletions
Large diffs are not rendered by default.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* This file is part of the KeyTone project.
3+
*
4+
* Copyright (C) 2024 LuSrackhall
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
**/
19+
/**
20+
* File: keytoneAlbumMappers.ts
21+
*
22+
* 目的(Why)
23+
* - 收敛“配置数据 → UI 结构”的纯映射逻辑,避免在多个位置重复写 Object.entries + sort。
24+
* - 让这些逻辑保持纯函数(输入 → 输出),便于单独审阅、复用与定位问题。
25+
*
26+
* 使用场景(Where used)
27+
* - `useKeytoneAlbumSseSync.ts`:处理 SSE 推送的全量配置更新。
28+
* - `Keytone_album.vue` 的 initData:初始化读取配置时复用相同映射,确保"初始化"和"SSE 更新"一致。
29+
* - `Keytone_album.vue` 的 watch(audioFiles):当 audioFiles 变化时,复用 mapAudioFilesArrayToSoundFileList。
30+
*
31+
* 设计约束(Constraints)
32+
* - 不直接依赖 Vue 响应式:这里不操作 ref/reactive,仅返回新数组/Map。
33+
* - 不做业务裁剪:除非历史实现里有明确的裁剪规则(例如:单键声效必须 down/up 至少一个存在)。
34+
*
35+
* Debug 指南
36+
* - 映射结果不对:优先检查传入的 config(audio_files/sounds/key_sounds)结构是否与后端一致。
37+
* - 排序不对:检查调用方传入的 naturalSort 是否与原实现一致。
38+
*/
39+
40+
export type NaturalSortFn = (a: string, b: string) => number;
41+
42+
export function mapAudioFilesConfigToArray(audioFilesConfig: any): Array<{ sha256: string; value: any }> {
43+
if (!audioFilesConfig) return [];
44+
return Object.entries(audioFilesConfig).map(([sha256, value]) => ({ sha256, value }));
45+
}
46+
47+
export function mapAudioFilesArrayToSoundFileList(
48+
audioFilesArray: Array<{ sha256: string; value: any }>,
49+
naturalSort: NaturalSortFn
50+
): Array<{ sha256: string; name_id: string; name: string; type: string }> {
51+
const tempSoundFileList: Array<{ sha256: string; name_id: string; name: string; type: string }> = [];
52+
53+
audioFilesArray.forEach((item) => {
54+
if (item?.value?.name !== undefined && item?.value?.name !== null) {
55+
Object.entries(item.value.name).forEach(([name_id, name]) => {
56+
tempSoundFileList.push({
57+
sha256: item.sha256,
58+
name_id,
59+
name: name as string,
60+
type: item.value.type as string,
61+
});
62+
});
63+
}
64+
});
65+
66+
tempSoundFileList.sort((a, b) => naturalSort(a.name + a.type, b.name + b.type));
67+
return tempSoundFileList;
68+
}
69+
70+
export function mapSoundsConfigToList(
71+
soundsConfig: any,
72+
naturalSort: NaturalSortFn
73+
): Array<{ soundKey: string; soundValue: any }> {
74+
if (!soundsConfig) return [];
75+
const sounds = Object.entries(soundsConfig).map(([soundKey, soundValue]) => ({ soundKey, soundValue }));
76+
77+
sounds.sort((a: any, b: any) => {
78+
const aName = (a.soundValue?.name as string) || a.soundKey;
79+
const bName = (b.soundValue?.name as string) || b.soundKey;
80+
return naturalSort(aName, bName);
81+
});
82+
83+
return sounds;
84+
}
85+
86+
export function mapKeySoundsConfigToList(
87+
keySoundsConfig: any,
88+
naturalSort: NaturalSortFn
89+
): Array<{ keySoundKey: string; keySoundValue: any }> {
90+
if (!keySoundsConfig) return [];
91+
const keySounds = Object.entries(keySoundsConfig).map(([keySoundKey, keySoundValue]) => ({
92+
keySoundKey,
93+
keySoundValue,
94+
}));
95+
96+
keySounds.sort((a: any, b: any) => {
97+
const aName = (a.keySoundValue?.name as string) || a.keySoundKey;
98+
const bName = (b.keySoundValue?.name as string) || b.keySoundKey;
99+
return naturalSort(aName, bName);
100+
});
101+
102+
return keySounds;
103+
}
104+
105+
export function mapSingleKeyConfigToKeysWithSoundEffect(singleConfig: any): Map<string, any> {
106+
const keysWithSoundEffect = new Map<string, any>();
107+
if (!singleConfig) return keysWithSoundEffect;
108+
109+
Object.entries(singleConfig).forEach(([dikCode, value]) => {
110+
if ((value as any)?.down?.value || (value as any)?.up?.value) {
111+
keysWithSoundEffect.set(dikCode, value);
112+
}
113+
});
114+
115+
return keysWithSoundEffect;
116+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* This file is part of the KeyTone project.
3+
*
4+
* Copyright (C) 2024 LuSrackhall
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
**/
19+
/**
20+
* File: useKeytoneAlbumDependencyIssues.ts
21+
*
22+
* 目的(Why)
23+
* - 将 `Keytone_album.vue` 中“依赖校验(Dependency validation)”相关的 computed + watch 抽离为 composable。
24+
* - 使父组件只需要拿到 `dependencyIssues` 与 `checkItemDependencyIssues`,用于 UI 展示与标记。
25+
*
26+
* 输入/输出(API)
27+
* - 输入:soundFileList / soundList / keySoundList / keysWithSoundEffect(均来自父组件的响应式状态)
28+
* - 输出:
29+
* - `dependencyIssues`: Ref<DependencyIssue[]> 供 UI 组件展示(例如 DependencyWarning)
30+
* - `allDependencyIssues`: computed(便于调试/扩展)
31+
* - `checkItemDependencyIssues`: 给列表项快速判定是否存在依赖问题
32+
*
33+
* 与哪些文件配合(Integration)
34+
* - 调用方:`frontend/src/components/Keytone_album.vue`
35+
* - 依赖:`src/utils/dependencyValidator`
36+
*
37+
* 关键实现说明(Important details)
38+
* - 历史原因:UI 选择器里 keySound 的 down/up value 可能被转换成“对象形态”(包含 soundKey/keySoundKey)。
39+
* 但依赖校验器期望的是“字符串 key”形态。
40+
* 因此这里对 keySoundList 做深拷贝,并把对象形态还原为字符串 key,再交给 validator。
41+
* - 重要:这里的深拷贝是为了避免污染 UI 状态,确保依赖校验是只读的。
42+
*
43+
* 行为不变约束(Behavior parity)
44+
* - `globalBinding` 目前仍为 undefined(与原实现一致),仅校验 singleKeyBindings。
45+
* - watch 采用 deep: true(与原实现一致)。
46+
*/
47+
48+
import { computed, ref, watch, type Ref } from 'vue';
49+
50+
import {
51+
createDependencyValidator,
52+
hasItemDependencyIssues,
53+
type AudioFile,
54+
type DependencyIssue,
55+
type KeySound,
56+
type Sound,
57+
} from 'src/utils/dependencyValidator';
58+
59+
type KeytoneKeySoundListItem = any;
60+
61+
type Params = {
62+
soundFileList: Ref<Array<any>>;
63+
soundList: Ref<Array<any>>;
64+
keySoundList: Ref<Array<KeytoneKeySoundListItem>>;
65+
keysWithSoundEffect: Ref<Map<string, any>>;
66+
};
67+
68+
export function useKeytoneAlbumDependencyIssues(params: Params) {
69+
const dependencyIssues = ref<DependencyIssue[]>([]);
70+
71+
const allDependencyIssues = computed(() => {
72+
const audioFiles = params.soundFileList.value as AudioFile[];
73+
const sounds = params.soundList.value as Sound[];
74+
75+
const keySounds = params.keySoundList.value
76+
.map((keySound) => {
77+
const keySoundCopy = JSON.parse(JSON.stringify(keySound));
78+
79+
keySoundCopy.keySoundValue.down.value = keySoundCopy.keySoundValue.down.value.map((item: any) => {
80+
if (item.type === 'sounds' && item.value && typeof item.value === 'object' && item.value.soundKey) {
81+
return { type: 'sounds', value: item.value.soundKey };
82+
}
83+
if (item.type === 'key_sounds' && item.value && typeof item.value === 'object' && item.value.keySoundKey) {
84+
return { type: 'key_sounds', value: item.value.keySoundKey };
85+
}
86+
return item;
87+
});
88+
89+
keySoundCopy.keySoundValue.up.value = keySoundCopy.keySoundValue.up.value.map((item: any) => {
90+
if (item.type === 'sounds' && item.value && typeof item.value === 'object' && item.value.soundKey) {
91+
return { type: 'sounds', value: item.value.soundKey };
92+
}
93+
if (item.type === 'key_sounds' && item.value && typeof item.value === 'object' && item.value.keySoundKey) {
94+
return { type: 'key_sounds', value: item.value.keySoundKey };
95+
}
96+
return item;
97+
});
98+
99+
return keySoundCopy;
100+
})
101+
.filter(Boolean) as KeySound[];
102+
103+
if (audioFiles.length === 0 && sounds.length === 0 && keySounds.length === 0) {
104+
return [];
105+
}
106+
107+
const validator = createDependencyValidator(audioFiles, sounds, keySounds);
108+
109+
const globalBinding = undefined;
110+
111+
const singleKeyBindings = params.keysWithSoundEffect.value.size > 0 ? params.keysWithSoundEffect.value : undefined;
112+
113+
return validator.validateAllDependencies(globalBinding, singleKeyBindings);
114+
});
115+
116+
watch(
117+
[params.soundFileList, params.soundList, params.keySoundList, params.keysWithSoundEffect],
118+
() => {
119+
dependencyIssues.value = allDependencyIssues.value;
120+
},
121+
{ deep: true }
122+
);
123+
124+
const checkItemDependencyIssues = (itemType: 'audio_files' | 'sounds' | 'key_sounds', itemId: string) => {
125+
return hasItemDependencyIssues(itemType, itemId, dependencyIssues.value);
126+
};
127+
128+
return {
129+
dependencyIssues,
130+
allDependencyIssues,
131+
checkItemDependencyIssues,
132+
};
133+
}

0 commit comments

Comments
 (0)