Skip to content

Commit 56de032

Browse files
committed
fix: 修复 Mod 配置页启用状态的读写
1 parent 01a6e76 commit 56de032

10 files changed

Lines changed: 216 additions & 57 deletions

File tree

ChuChartManager/Front/src/client/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface Manifest {
4848
}
4949

5050
export interface ModConfigSection {
51+
enabled: boolean
5152
entries: Record<string, unknown>
5253
}
5354

ChuChartManager/Front/src/locales/en.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ music:
224224
openExplorer: Open in Explorer
225225
openXml: Edit Music.xml
226226
mods:
227-
experimentalWarning: This feature is experimental and may be unstable
227+
appleChuBetaWarning: AppleChu is currently in closed beta. Features and configuration formats may change between versions
228228
wip: Loading...
229229
noMods: No Mods detected
230230
search: Search features...

ChuChartManager/Front/src/locales/ja.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ music:
224224
openExplorer: エクスプローラーで開く
225225
openXml: Music.xml を編集
226226
mods:
227-
experimentalWarning: この機能は実験的であり、不安定な場合があります
227+
appleChuBetaWarning: AppleChu は現在クローズドベータ版です。機能や設定形式はバージョンによって変更される場合があります
228228
wip: 読み込み中...
229229
noMods: Mod が検出されません
230230
search: 機能を検索...

ChuChartManager/Front/src/locales/zh.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ music:
224224
openExplorer: 在资源管理器中打开
225225
openXml: 编辑 Music.xml
226226
mods:
227-
experimentalWarning: 此功能为实验性,可能存在不稳定的情况
227+
appleChuBetaWarning: AppleChu 目前处于内测阶段,功能和配置格式可能随版本调整
228228
wip: 正在加载...
229229
noMods: 未检测到 Mod
230230
search: 搜索功能...

ChuChartManager/Front/src/views/ModManager.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ watch(config, () => {
123123

124124
<template>
125125
<div class="p-xy h-100dvh flex flex-col of-hidden">
126-
<div class="text-sm op-60 mb-3 px-2 py-1.5 bg-orange/10 c-orange rd">{{ t('mods.experimentalWarning') }}</div>
126+
<div class="text-sm op-60 mb-3 px-2 py-1.5 bg-orange/10 c-orange rd">{{ t('mods.appleChuBetaWarning') }}</div>
127127
<div v-if="!loading" class="flex items-center gap-x-6 gap-y-2 flex-wrap shrink-0 px-2 pb-3 border-b border-solid border-[oklch(0.9_0.02_var(--hue))]">
128128
<div class="flex items-center gap-2 min-w-0">
129129
<span :class="modOk ? 'i-mdi-check-circle c-green-6' : 'i-mdi-alert-circle c-red-6'" class="text-5 shrink-0" />

ChuChartManager/Front/src/views/ModManager/ModConfigurator.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,13 @@ const visibleSections = computed(() => {
4848
function ensureState(section: ManifestSection) {
4949
if (!props.config[section.id]) {
5050
props.config[section.id] = {
51+
enabled: !!section.always_enabled || !!section.default_enabled,
5152
entries: {},
5253
}
5354
}
55+
else if (typeof props.config[section.id].enabled !== 'boolean') {
56+
props.config[section.id].enabled = !!section.always_enabled || !!section.default_enabled
57+
}
5458
return props.config[section.id]
5559
}
5660
</script>

ChuChartManager/Front/src/views/ModManager/ModSection.vue

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { computed, ref, useId } from 'vue'
3+
import { CheckBox } from '@munet/ui'
34
import { locale } from '@/locales'
45
import type { ManifestSection, ModConfigSection } from '@/client/mod'
56
import ModEntry from './ModEntry.vue'
@@ -13,64 +14,74 @@ const props = defineProps<{
1314
const language = computed(() => locale.value.startsWith('zh') ? 'zh' : 'en')
1415
const label = computed(() => props.section.label?.[language.value] || props.section.id)
1516
const description = computed(() => props.section.description?.[language.value] || '')
16-
const entries = computed(() => props.section.entries?.filter(entry => !entry.hidden) ?? [])
17+
const entries = computed(() => props.section.entries?.filter(entry =>
18+
!entry.hidden && entry.key.replaceAll('_', '').toLowerCase() !== 'enable') ?? [])
1719
const basicEntries = computed(() => entries.value.filter(entry => !entry.advanced))
1820
const advancedEntries = computed(() => entries.value.filter(entry => entry.advanced))
1921
const advancedExpanded = ref(false)
2022
const advancedOpen = computed(() => props.revealAdvanced || advancedExpanded.value)
2123
const advancedContentId = `advanced-${useId()}`
24+
const enabled = computed({
25+
get: () => props.section.always_enabled ? true : props.state.enabled,
26+
set: value => { props.state.enabled = props.section.always_enabled ? true : value },
27+
})
2228
</script>
2329

2430
<template>
25-
<div class="mod-section">
31+
<div class="mod-section" :class="{ 'section-disabled': !enabled, 'always-enabled': section.always_enabled }">
2632
<div class="section-header">
27-
<div class="section-label">{{ label }}</div>
33+
<CheckBox v-if="!section.always_enabled" v-model:value="enabled" class="section-switch">
34+
<span class="section-label">{{ label }}</span>
35+
</CheckBox>
36+
<div v-else class="section-label">{{ label }}</div>
2837
<div v-if="description" class="text-sm op-80 min-w-0">{{ description }}</div>
2938
</div>
3039

31-
<div v-if="basicEntries.length" class="flex flex-col gap-2 mt-2">
32-
<ModEntry
33-
v-for="entry in basicEntries"
34-
:key="entry.key"
35-
:entry="entry"
36-
:section-state="state"
37-
/>
38-
</div>
39-
40-
<template v-if="advancedEntries.length">
41-
<button
42-
v-if="!revealAdvanced"
43-
type="button"
44-
class="advanced-toggle"
45-
:aria-expanded="advancedOpen"
46-
:aria-controls="advancedContentId"
47-
@click="advancedExpanded = !advancedExpanded"
48-
>
49-
<span
50-
class="i-mdi-chevron-down advanced-chevron"
51-
:class="{ expanded: advancedOpen }"
52-
aria-hidden="true"
53-
/>
54-
<span>{{ $t('mods.advancedOptions') }}</span>
55-
<span class="op-55">{{ advancedEntries.length }}</span>
56-
</button>
57-
<div v-else class="advanced-label">
58-
<span class="i-mdi-tune-variant text-4" aria-hidden="true" />
59-
<span>{{ $t('mods.advancedOptions') }}</span>
60-
</div>
61-
<div
62-
v-show="advancedOpen"
63-
:id="advancedContentId"
64-
class="flex flex-col gap-2 mt-2"
65-
>
40+
<div class="section-options">
41+
<div v-if="basicEntries.length" class="flex flex-col gap-2 mt-2">
6642
<ModEntry
67-
v-for="entry in advancedEntries"
43+
v-for="entry in basicEntries"
6844
:key="entry.key"
6945
:entry="entry"
7046
:section-state="state"
7147
/>
7248
</div>
73-
</template>
49+
50+
<template v-if="advancedEntries.length">
51+
<button
52+
v-if="!revealAdvanced"
53+
type="button"
54+
class="advanced-toggle"
55+
:aria-expanded="advancedOpen"
56+
:aria-controls="advancedContentId"
57+
@click="advancedExpanded = !advancedExpanded"
58+
>
59+
<span
60+
class="i-mdi-chevron-down advanced-chevron"
61+
:class="{ expanded: advancedOpen }"
62+
aria-hidden="true"
63+
/>
64+
<span>{{ $t('mods.advancedOptions') }}</span>
65+
<span class="op-55">{{ advancedEntries.length }}</span>
66+
</button>
67+
<div v-else class="advanced-label">
68+
<span class="i-mdi-tune-variant text-4" aria-hidden="true" />
69+
<span>{{ $t('mods.advancedOptions') }}</span>
70+
</div>
71+
<div
72+
v-show="advancedOpen"
73+
:id="advancedContentId"
74+
class="flex flex-col gap-2 mt-2"
75+
>
76+
<ModEntry
77+
v-for="entry in advancedEntries"
78+
:key="entry.key"
79+
:entry="entry"
80+
:section-state="state"
81+
/>
82+
</div>
83+
</template>
84+
</div>
7485
</div>
7586
</template>
7687

@@ -86,6 +97,15 @@ const advancedContentId = `advanced-${useId()}`
8697
&:hover
8798
border-color: oklch(0.68 0.17 var(--hue) / 0.45)
8899
100+
&.always-enabled
101+
background: oklch(0.68 0.17 var(--hue) / 0.05)
102+
103+
&.section-disabled .section-options
104+
opacity: 0.48
105+
106+
.section-options
107+
transition: opacity 0.2s ease
108+
89109
.section-header
90110
display: grid
91111
grid-template-columns: minmax(8rem, 12rem) minmax(0, 1fr)
@@ -97,6 +117,9 @@ const advancedContentId = `advanced-${useId()}`
97117
font-size: 1rem
98118
overflow-wrap: anywhere
99119
120+
.section-switch
121+
min-width: 0
122+
100123
.advanced-toggle,
101124
.advanced-label
102125
display: flex

ChuChartManager/Services/AppleChuConfigSchema.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ internal sealed record AppleChuConfigEntrySchema(
1717

1818
internal sealed record AppleChuConfigSectionSchema(
1919
string Id,
20+
bool DefaultEnabled,
21+
bool AlwaysEnabled,
2022
IReadOnlyList<AppleChuConfigEntrySchema> Entries);
2123

2224
internal static class AppleChuConfigSchema
@@ -38,7 +40,15 @@ internal static IReadOnlyList<AppleChuConfigSectionSchema> Parse(
3840
foreach (var rawEntry in rawEntries)
3941
entries.Add(ParseEntry(rawEntry, id));
4042
}
41-
result.Add(new AppleChuConfigSectionSchema(id, entries));
43+
var alwaysEnabled = ReadBooleanAlias(rawSection, id, false, "always_enabled", "alwaysEnabled");
44+
var defaultEnabled = ReadBooleanAlias(
45+
rawSection,
46+
id,
47+
alwaysEnabled,
48+
"default_enabled",
49+
"default_on",
50+
"defaultOn");
51+
result.Add(new AppleChuConfigSectionSchema(id, defaultEnabled, alwaysEnabled, entries));
4252
}
4353

4454
return result;
@@ -78,6 +88,29 @@ internal static bool KeysEqual(string left, string right) => string.Equals(
7888
right.Replace("_", "", StringComparison.Ordinal),
7989
StringComparison.OrdinalIgnoreCase);
8090

91+
internal static bool IsEnableEntry(AppleChuConfigEntrySchema entry) => KeysEqual(entry.Key, "enable");
92+
93+
private static bool ReadBooleanAlias(
94+
TomlTable table,
95+
string sectionId,
96+
bool fallback,
97+
params string[] keys)
98+
{
99+
bool? result = null;
100+
foreach (var key in keys)
101+
{
102+
if (!table.TryGetValue(key, out var value))
103+
continue;
104+
if (value is not bool boolean)
105+
throw new InvalidDataException($"manifest 中 {sectionId}.{key} 必须是布尔值");
106+
if (result.HasValue && result.Value != boolean)
107+
throw new InvalidDataException($"manifest 中 {sectionId} 的启用状态元数据存在冲突");
108+
result = boolean;
109+
}
110+
111+
return result ?? fallback;
112+
}
113+
81114
private static AppleChuConfigEntrySchema ParseEntry(TomlTable rawEntry, string sectionId)
82115
{
83116
var key = rawEntry.TryGetValue("key", out var keyValue) ? keyValue as string : null;

ChuChartManager/Services/AppleChuConfigService.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace ChuChartManager.Services;
66

77
public sealed class AppleChuConfigService
88
{
9-
public sealed record SectionState(Dictionary<string, object?> Entries);
9+
public sealed record SectionState(bool Enabled, Dictionary<string, object?> Entries);
1010

1111
private readonly object writeLock = new();
1212

@@ -28,17 +28,20 @@ public Dictionary<string, SectionState> ReadOrCreate(
2828
foreach (var section in schema)
2929
{
3030
var table = GetTable(document, section.Id);
31+
var enabled = ReadEnabled(section, table);
3132
var entries = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
3233
foreach (var entry in section.Entries)
3334
{
35+
if (AppleChuConfigSchema.IsEnableEntry(entry))
36+
continue;
3437
if (AppleChuConfigSchema.TryGetValue(table, entry.Key, out var configured)
3538
&& AppleChuConfigSchema.TryNormalize(entry, configured, out var normalized))
3639
{
3740
entries[entry.Key] = normalized;
3841
}
3942
}
4043

41-
result[section.Id] = new SectionState(entries);
44+
result[section.Id] = new SectionState(enabled, entries);
4245
}
4346

4447
return result;
@@ -81,19 +84,22 @@ private static IReadOnlyDictionary<string, SectionState> NormalizeRequest(
8184
foreach (var section in schema)
8285
{
8386
requestedSections.TryGetValue(section.Id, out var requested);
87+
var enabled = section.AlwaysEnabled || (requested?.Enabled ?? section.DefaultEnabled);
8488
var entries = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
8589
foreach (var (key, supplied) in requested?.Entries ?? [])
8690
{
8791
var entry = section.Entries.FirstOrDefault(item =>
8892
AppleChuConfigSchema.KeysEqual(item.Key, key));
8993
if (entry == null)
9094
throw new ArgumentException($"未知的 AppleChu 配置项: {section.Id}.{key}");
95+
if (AppleChuConfigSchema.IsEnableEntry(entry))
96+
throw new ArgumentException($"{section.Id}.Enable 必须通过栏目开关设置");
9197
if (!AppleChuConfigSchema.TryNormalize(entry, supplied, out var normalized))
9298
throw new ArgumentException($"{section.Id}.{entry.Key} 的值不符合 {entry.Type} 类型、范围或候选值");
9399
entries[entry.Key] = normalized;
94100
}
95101

96-
result[section.Id] = new SectionState(entries);
102+
result[section.Id] = new SectionState(enabled, entries);
97103
}
98104

99105
return result;
@@ -117,6 +123,33 @@ private static TomlTable ParseDocument(string source, string name)
117123
return AppleChuConfigSchema.TryGetValue(document, key, out var value) ? value as TomlTable : null;
118124
}
119125

126+
private static bool ReadEnabled(AppleChuConfigSectionSchema section, TomlTable? table)
127+
{
128+
if (section.AlwaysEnabled)
129+
return true;
130+
131+
var enableEntry = section.Entries.FirstOrDefault(AppleChuConfigSchema.IsEnableEntry);
132+
if (enableEntry != null)
133+
{
134+
if (AppleChuConfigSchema.TryGetValue(table, enableEntry.Key, out var configured)
135+
&& AppleChuConfigSchema.TryNormalize(enableEntry, configured, out var normalized)
136+
&& normalized is bool enabled)
137+
{
138+
return enabled;
139+
}
140+
141+
return section.DefaultEnabled;
142+
}
143+
144+
if (AppleChuConfigSchema.TryGetValue(table, "Disabled", out var disabledValue)
145+
&& disabledValue is bool disabled)
146+
{
147+
return !disabled;
148+
}
149+
150+
return table != null || section.DefaultEnabled;
151+
}
152+
120153
private static string GetConfigPath(string gamePath) => Path.Combine(gamePath, "bin", "AppleChu.toml");
121154

122155
private static void WriteAtomically(string path, string contents)

0 commit comments

Comments
 (0)