feat: new config format

This commit is contained in:
2025-03-13 23:26:00 +00:00
parent 48dc9ec4df
commit fd27000c05
30 changed files with 1447 additions and 833 deletions

View File

@ -30,8 +30,7 @@ onMounted(async () => {
general.dirs = d as Dirs;
});
await prf.reloadList();
await prf.reload();
await Promise.all([prf.reloadList(), prf.reload()]);
if (prf.current !== null) {
await pkg.reloadAll();

View File

@ -2,25 +2,16 @@
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import { open } from '@tauri-apps/plugin-dialog';
import { usePrfStore } from '../stores';
const props = defineProps({
field: String,
default: String,
placeholder: String,
directory: Boolean,
promptname: String,
extension: String,
value: String,
callback: Function,
});
if (props.field === undefined || props.default === undefined) {
throw new Error('Invalid FilePicker');
}
const prf = usePrfStore();
const cfg = prf.cfg(props.field, props.default);
const filePick = async () => {
const res = await open({
multiple: false,
@ -35,9 +26,9 @@ const filePick = async () => {
]
: [],
});
if (res != null) {
cfg.value =
/*path.relative(cfgs.current?.data.exe_dir ?? '', res) */ res;
if (res != null && props.callback !== undefined) {
props.callback(res);
/*path.relative(cfgs.current?.data.exe_dir ?? '', res) */
}
};
</script>
@ -48,6 +39,7 @@ const filePick = async () => {
size="small"
:placeholder="placeholder"
type="text"
v-model="cfg"
:model-value="value"
@update:model-value="(value) => callback && callback(value)"
/>
</template>

View File

@ -66,7 +66,7 @@ const aimeCodeModel = computed({
});
const extraDisplayOptionsDisabled = computed(() => {
return prf.cfg('display', 'default').value === 'default';
return prf.current?.display.target === 'default';
});
(async () => {
@ -79,16 +79,16 @@ const extraDisplayOptionsDisabled = computed(() => {
<OptionCategory title="General">
<OptionRow title="mu3.exe">
<FilePicker
field="target-path"
default=""
:directory="false"
promptname="mu3.exe"
extension="exe"
:value="prf.current!.sgt.target"
:callback="(value: string) => (prf.current!.sgt.target = value)"
></FilePicker>
</OptionRow>
<OptionRow title="mu3hook">
<Select
:model-value="prf.cfg('hook', 'segatools-mu3hook')"
model-value="segatools-mu3hook"
:options="hookList"
option-label="title"
option-value="value"
@ -96,24 +96,27 @@ const extraDisplayOptionsDisabled = computed(() => {
</OptionRow>
<OptionRow title="amfs">
<FilePicker
field="amfs"
default=""
placeholder="amfs"
:directory="true"
placeholder="amfs"
:value="prf.current!.sgt.amfs"
:callback="(value: string) => (prf.current!.sgt.amfs = value)"
></FilePicker>
</OptionRow>
<OptionRow title="option">
<FilePicker
field="option"
default="option"
:directory="true"
placeholder="option"
:value="prf.current!.sgt.option"
:callback="(value: string) => (prf.current!.sgt.option = value)"
></FilePicker>
</OptionRow>
<OptionRow title="appdata">
<FilePicker
field="appdata"
default="appdata"
:directory="true"
:value="prf.current!.sgt.appdata"
:callback="
(value: string) => (prf.current!.sgt.appdata = value)
"
></FilePicker>
</OptionRow>
</OptionCategory>
@ -123,7 +126,7 @@ const extraDisplayOptionsDisabled = computed(() => {
title="Target display"
>
<Select
:model-value="prf.cfg('display', 'default')"
v-model="prf.current!.display.target"
:options="displayList"
option-label="title"
option-value="value"
@ -136,7 +139,7 @@ const extraDisplayOptionsDisabled = computed(() => {
:min="480"
:max="9999"
:use-grouping="false"
:model-value="prf.cfgAny('rez-w', 1080)"
v-model="prf.current!.display.rez[0]"
/>
x
<InputNumber
@ -145,17 +148,18 @@ const extraDisplayOptionsDisabled = computed(() => {
:min="640"
:max="9999"
:use-grouping="false"
:model-value="prf.cfgAny('rez-h', 1920)"
v-model="prf.current!.display.rez[1]"
/>
</OptionRow>
<OptionRow title="Display mode">
<SelectButton
:model-value="prf.cfg('display-mode', 'borderless')"
v-model="prf.current!.display.mode"
:options="[
{ title: 'Window', value: 'window' },
{ title: 'Borderless window', value: 'borderless' },
{ title: 'Fullscreen', value: 'fullscreen' },
{ title: 'Window', value: 'Window' },
{ title: 'Borderless window', value: 'Borderless' },
{ title: 'Fullscreen', value: 'Fullscreen' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
@ -165,12 +169,13 @@ const extraDisplayOptionsDisabled = computed(() => {
v-if="capabilities.includes('display')"
>
<SelectButton
:model-value="prf.cfg('display-rotation', 0)"
v-model="prf.current!.display.rotation"
:options="[
{ title: 'Unchanged', value: 0 },
{ title: 'Portrait', value: 90 },
{ title: 'Portrait (flipped)', value: 270 },
]"
:allow-empty="false"
option-label="title"
option-value="value"
:disabled="extraDisplayOptionsDisabled"
@ -183,7 +188,7 @@ const extraDisplayOptionsDisabled = computed(() => {
:min="60"
:max="999"
:use-grouping="false"
:model-value="prf.cfgAny('frequency', 60)"
v-model="prf.current!.display.frequency"
:disabled="extraDisplayOptionsDisabled"
/>
</OptionRow>
@ -191,32 +196,69 @@ const extraDisplayOptionsDisabled = computed(() => {
title="Match display resolution with the game"
v-if="capabilities.includes('display')"
>
<!-- @vue-expect-error -->
<ToggleSwitch
:disabled="
extraDisplayOptionsDisabled ||
prf.cfg('display-mode', 'borderless').value != 'borderless'
prf.current?.display.mode !== 'Borderless'
"
:model-value="prf.cfg('borderless-fullscreen', false)"
v-model="prf.current!.display.borderless_fullscreen"
/>
</OptionRow>
</OptionCategory>
<OptionCategory title="Network">
<OptionRow title="Server address">
<OptionRow title="Network type">
<SelectButton
v-model="prf.current!.network.network_type"
:options="[
{ title: 'Remote', value: 'Remote' },
{ title: 'Local (ARTEMiS)', value: 'Artemis' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
<OptionRow
v-if="prf.current!.network.network_type == 'Artemis'"
title="ARTEMiS path"
>
<FilePicker
:directory="false"
promptname="index.py"
extension="py"
:value="prf.current!.network.local_path"
:callback="
(value: string) => (prf.current!.network.local_path = value)
"
></FilePicker>
</OptionRow>
<OptionRow
v-if="prf.current!.network.network_type == 'Artemis'"
title="ARTEMiS console"
>
<ToggleSwitch v-model="prf.current!.network.local_console" />
</OptionRow>
<OptionRow
v-if="prf.current!.network.network_type == 'Remote'"
title="Server address"
>
<InputText
class="shrink"
size="small"
:maxlength="40"
placeholder="192.168.1.234"
:model-value="prf.cfgAny<string>('dns-default', '')"
v-model="prf.current!.network.remote_address"
/> </OptionRow
><OptionRow title="Keychip">
><OptionRow
v-if="prf.current!.network.network_type == 'Remote'"
title="Keychip"
>
<InputText
class="shrink"
size="small"
:maxlength="16"
placeholder="A123-01234567890"
:model-value="prf.cfgAny('keychip', '')"
v-model="prf.current!.network.keychip"
/> </OptionRow
><OptionRow title="Subnet">
<InputText
@ -224,33 +266,33 @@ const extraDisplayOptionsDisabled = computed(() => {
size="small"
:maxlength="15"
placeholder="192.168.1.0"
:model-value="prf.cfgAny('subnet', '')"
v-model="prf.current!.network.subnet"
/>
</OptionRow>
<OptionRow title="Address suffix">
<InputText
<InputNumber
class="shrink"
size="small"
:maxlength="3"
:min="0"
:max="255"
placeholder="12"
:model-value="prf.cfgAny('addrsuffix', '')"
v-model="prf.current!.network.suffix"
/>
</OptionRow>
</OptionCategory>
<OptionCategory title="Misc">
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
<!-- @vue-expect-error -->
<ToggleSwitch :model-value="prf.cfg('intel', false)" />
<ToggleSwitch v-model="prf.current!.sgt.intel" />
</OptionRow>
<OptionRow title="Aime emulation">
<!-- @vue-expect-error -->
<ToggleSwitch :model-value="prf.cfg('aime', false)" />
<ToggleSwitch v-model="prf.current!.sgt.enable_aime" />
</OptionRow>
<OptionRow title="Aime code">
<InputText
class="shrink"
size="small"
:disabled="prf.cfg<boolean>('aime', false).value !== true"
:disabled="prf.current?.sgt.enable_aime !== true"
:maxlength="20"
placeholder="00000000000000000000"
v-model="aimeCodeModel"
@ -268,6 +310,9 @@ const extraDisplayOptionsDisabled = computed(() => {
extension="cfg"
/>
</OptionRow>
<OptionRow title="BepInEx console">
<ToggleSwitch v-model="prf.current!.bepinex.console" />
</OptionRow>
</OptionCategory>
</template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import Button from 'primevue/button';
import ProfileListEntry from './ProfileListEntry.vue';
import { usePrfStore } from '../stores';
const prf = usePrfStore();
@ -18,70 +19,25 @@ const prf = usePrfStore();
icon="pi pi-plus"
class="chunithm-button profile-button"
@click="() => prf.create('chunithm')"
:disabled="true"
/>
</div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
<div v-for="p in prf.list">
<div class="flex flex-row flex-wrap align-middle gap-2">
<Button
:disabled="
prf.current?.game === p.game &&
prf.current?.name === p.name
"
:label="p.name"
:class="
(p.game === 'chunithm'
? 'chunithm-button'
: 'ongeki-button') +
' ' +
'self-center profile-button'
"
@click="prf.switchTo(p.game, p.name)"
/>
<Button
rounded
icon="pi pi-trash"
severity="danger"
aria-label="remove"
size="small"
class="self-center ml-2"
style="width: 2rem; height: 2rem"
:disabled="true"
/>
<Button
rounded
icon="pi pi-clone"
severity="warn"
aria-label="duplicate"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
:disabled="true"
/>
<Button
rounded
icon="pi pi-pencil"
severity="help"
aria-label="duplicate"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
:disabled="true"
/>
</div>
<ProfileListEntry :p="p" />
</div>
</div>
</template>
<style scoped>
<style>
.profile-button {
width: 14em;
white-space: nowrap;
}
.ongeki-button {
background-color: var(--p-pink-400);
border-color: var(--p-pink-400);
background-color: var(--p-pink-400) !important;
border-color: var(--p-pink-400) !important;
}
.ongeki-button:hover,
@ -91,8 +47,8 @@ const prf = usePrfStore();
}
.chunithm-button {
background-color: var(--p-yellow-400);
border-color: var(--p-yellow-400);
background-color: var(--p-yellow-400) !important;
border-color: var(--p-yellow-400) !important;
}
.chunithm-button:hover,
.chunithm-button:active {

View File

@ -0,0 +1,113 @@
<script setup lang="ts">
import { ref } from 'vue';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import * as path from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-shell';
import { useGeneralStore, usePrfStore } from '../stores';
import { ProfileMeta } from '../types';
const prf = usePrfStore();
const general = useGeneralStore();
const isEditing = ref(false);
const props = defineProps({
p: Object as () => ProfileMeta,
});
if (props.p === undefined) {
throw new Error('Invalid ProfileListEntry');
}
const rename = async (event: KeyboardEvent) => {
if (event.key !== 'Enter') {
return;
}
isEditing.value = false;
if (
event.target !== null &&
'value' in event.target &&
typeof event.target.value === 'string'
) {
const value = event.target.value
.replaceAll('..', '')
.replaceAll('\\', '')
.replaceAll('/', '');
if (value.length > 0) {
await prf.rename(props.p!, value);
}
}
};
</script>
<template>
<div class="flex flex-row flex-wrap align-middle gap-2">
<Button
:disabled="
prf.current?.game === p!.game && prf.current?.name === p!.name
"
:class="
(p!.game === 'chunithm' ? 'chunithm-button' : 'ongeki-button') +
' ' +
'self-center profile-button'
"
@click="prf.switchTo(p!.game, p!.name)"
>
<div v-if="!isEditing">{{ p!.name }}</div>
<div v-else>
<InputText
:model-value="p!.name"
@vue:mounted="$event?.el?.focus()"
@keyup="rename"
@focusout="isEditing = false"
>
</InputText></div
></Button>
<Button
rounded
icon="pi pi-trash"
severity="danger"
aria-label="remove"
size="small"
class="self-center ml-2"
style="width: 2rem; height: 2rem"
:disabled="true"
/>
<Button
rounded
icon="pi pi-clone"
severity="help"
aria-label="duplicate"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
:disabled="true"
/>
<Button
rounded
icon="pi pi-pencil"
severity="help"
aria-label="rename"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
@click="isEditing = true"
/>
<Button
rounded
icon="pi pi-folder"
severity="help"
aria-label="open-directory"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
@click="
path
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
.then(open)
"
/>
</div>
</template>

View File

@ -25,10 +25,10 @@ const kill = async () => {
};
const disabledTooltip = computed(() => {
if (prf.cfg('target-path', '').value.length === 0) {
if (prf.current?.sgt.target.length === 0) {
return 'The game path must be specified';
}
if (prf.cfg('amfs', '').value.length === 0) {
if (prf.current?.sgt.amfs.length === 0) {
return 'The amfs path must be specified';
}
return null;

View File

@ -1,4 +1,4 @@
import { Ref, computed, ref } from 'vue';
import { Ref, computed, ref, watchEffect } from 'vue';
import { defineStore } from 'pinia';
import { listen } from '@tauri-apps/api/event';
import * as path from '@tauri-apps/api/path';
@ -110,43 +110,25 @@ export const usePrfStore = defineStore('prf', () => {
() =>
pkg !== undefined &&
current.value !== null &&
current.value?.data.mods.includes(pkgKey(pkg))
current.value?.mods.includes(pkgKey(pkg))
);
const reload = async () => {
current.value = await invoke('get_current_profile');
const p: any = await invoke('get_current_profile');
if (p['OngekiProfile'] !== undefined) {
current.value = { ...p.OngekiProfile, game: 'ongeki' };
}
if (current.value !== null) {
changePrimaryColor(current.value.game);
}
};
const save = async () => {
await invoke('save_current_profile');
};
const cfg = <T extends string | boolean | number>(key: string, dflt: T) =>
computed({
get() {
return (current.value?.data.cfg[key] as T | undefined) ?? dflt;
},
async set(value) {
if (value !== undefined) {
await invoke('set_cfg', { key, value: value });
await reload();
await save();
}
},
});
// Hack around PrimeVu not supporting WritableComputedRef
const cfgAny = <T extends string | boolean | number>(
key: string,
dflt: T
) => cfg(key, dflt) as any;
const create = async (game: Game) => {
try {
await invoke('init_profile', { game, name: 'new-profile' });
await invoke('init_profile', {
game,
name: 'new-profile',
});
await reload();
await reloadList();
} catch (e) {
@ -159,6 +141,22 @@ export const usePrfStore = defineStore('prf', () => {
}
};
const rename = async (profile: ProfileMeta, name: string) => {
await invoke('rename_profile', {
profile,
name,
});
if (
current.value?.game === profile.game &&
current.value.name === profile.name
) {
current.value.name = name;
}
await reloadList();
};
const switchTo = async (game: Game, name: string) => {
await invoke('load_profile', { game, name });
await reload();
@ -169,14 +167,9 @@ export const usePrfStore = defineStore('prf', () => {
};
const reloadList = async () => {
const raw = (await invoke('list_profiles')) as [Game, string][];
list.value = raw.map(([game, name]) => {
return {
game,
name,
};
});
// list.value.splice(0, list.value.length);
list.value = (await invoke('list_profiles')) as ProfileMeta[];
console.log(list.value);
};
const togglePkg = async (pkg: Package | undefined, enable: boolean) => {
@ -185,7 +178,6 @@ export const usePrfStore = defineStore('prf', () => {
}
await invoke('toggle_package', { key: pkgKey(pkg), enable });
await reload();
await save();
};
const generalStore = useGeneralStore();
@ -201,15 +193,21 @@ export const usePrfStore = defineStore('prf', () => {
await reload();
});
watchEffect(async () => {
if (current.value !== null) {
await invoke('save_current_profile', {
profile: { OngekiProfile: current.value },
});
}
});
return {
current,
list,
isPkgEnabled,
reload,
save,
cfg,
cfgAny,
create,
rename,
switchTo,
reloadList,
togglePkg,

View File

@ -26,13 +26,47 @@ export interface ProfileMeta {
name: string;
}
export interface Profile extends ProfileMeta {
data: {
mods: string[];
cfg: { [key: string]: string | boolean | number };
};
export interface SegatoolsConfig {
target: string;
amfs: string;
option: string;
appdata: string;
enable_aime: boolean;
intel: boolean;
}
export interface DisplayConfig {
target: String;
rez: [number, number];
mode: 'Window' | 'Borderless' | 'Fullscreen';
rotation: number;
frequency: number;
borderless_fullscreen: boolean;
}
export interface NetworkConfig {
network_type: 'Remote' | 'Artemis';
local_path: string;
local_console: boolean;
remote_address: string;
keychip: string;
subnet: string;
suffix: number | null;
}
export interface BepInExConfig {
console: boolean;
}
export interface Profile extends ProfileMeta {
mods: string[];
sgt: SegatoolsConfig;
display: DisplayConfig;
network: NetworkConfig;
bepinex: BepInExConfig;
}
export type Module = 'sgt' | 'display' | 'network';
export interface Dirs {
config_dir: string;
data_dir: string;