feat: more options

This commit is contained in:
2025-03-05 00:40:59 +01:00
parent daafe1856b
commit 39ba6a5891
14 changed files with 1260 additions and 163 deletions

View File

@ -17,7 +17,6 @@ const pkg = usePkgStore();
const prf = usePrfStore();
pkg.setupListeners();
prf.setupListeners();
const currentTab = ref('3');

View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import { ref } from 'vue';
import Button from 'primevue/button';
import * as path from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
import { invoke } from '../invoke';
const props = defineProps({
filename: String,
promptname: String,
extension: String,
});
const exists = ref(false);
const contents = ref('');
const enabled = ref(false);
const target_path = ref('');
const load = async (p: string) => {
try {
contents.value = await readTextFile(p);
exists.value = true;
} catch (_) {
exists.value = false;
}
};
const save = async () => {
if (target_path.value.length > 0) {
await writeTextFile(target_path.value, contents.value);
}
};
const filePick = async () => {
const p = await open({
multiple: false,
directory: false,
filters:
props.promptname && props.extension
? [
{
name: props.promptname,
extensions: [props.extension],
},
]
: [],
});
if (p != null) {
await load(p);
await save();
}
};
(async () => {
const profileDir: string = await invoke('get_current_profile_dir');
if (props.filename === undefined) {
throw new Error('FileEditor without a filename');
}
target_path.value = await path.join(profileDir, props.filename);
await load(target_path.value);
})();
</script>
<template>
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
<div v-else>
<Button
v-if="exists"
icon="pi pi-pen-to-square"
size="small"
@click="enabled = true"
/>
<div v-if="enabled" class="backdrop">
<textarea
class="primitive-editor"
@vue:mounted="$event.el.focus()"
@input="(event) => (contents = (event.target as any).value)"
@focusout="
save();
enabled = false;
"
>{{ contents }}</textarea
>
</div>
</div>
</template>
<style lang="css">
.backdrop {
position: fixed;
left: 0;
top: 0;
z-index: 900;
width: 100vw;
background-color: rgba(1, 1, 1, 0.7);
height: 100vh;
}
.primitive-editor {
font-family: monospace;
white-space: nowrap;
position: fixed;
top: 10vh;
left: 10vw;
height: 80vh;
width: 80vw;
z-index: 1000;
background-color: #151515;
color: #ddd;
}
</style>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
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,
});
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,
directory: props.directory,
filters:
props.promptname && props.extension
? [
{
name: props.promptname,
extensions: [props.extension],
},
]
: [],
});
if (res != null) {
cfg.value =
/*path.relative(cfgs.current?.data.exe_dir ?? '', res) */ res;
}
};
</script>
<template>
<Button icon="pi pi-folder-open" size="small" @click="filePick" />
<InputText
size="small"
:placeholder="placeholder"
type="text"
v-model="cfg"
/>
</template>

View File

@ -23,11 +23,12 @@ const toggle = async (value: boolean) => {
<div class="flex items-center">
<ModTitlecard showVersion :pkg="pkg" />
<UpdateButton :pkg="pkg" />
<!-- @vue-expect-error Can't 'as any' because it breaks VSCode -->
<ToggleSwitch
class="scale-[1.33] shrink-0"
inputId="switch"
:disabled="!pkg?.loc"
:modelValue="prf.isPkgEnabled(pkg)"
:model-value="prf.isPkgEnabled(pkg)"
v-on:value-change="toggle"
/>
<InstallButton :pkg="pkg" />

View File

@ -4,8 +4,10 @@ import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext';
import Select from 'primevue/select';
import SelectButton from 'primevue/selectbutton';
import Toggle from 'primevue/toggleswitch';
import ToggleSwitch from 'primevue/toggleswitch';
import { invoke as unproxied_invoke } from '@tauri-apps/api/core';
import FileEditor from './FileEditor.vue';
import FilePicker from './FilePicker.vue';
import OptionCategory from './OptionCategory.vue';
import OptionRow from './OptionRow.vue';
import { invoke } from '../invoke';
@ -13,37 +15,9 @@ import { usePrfStore } from '../stores';
const prf = usePrfStore();
const _cfg = <T extends string | number | boolean>(key: string, dflt: T) =>
computed({
get() {
return (prf.cfg(key) as T) ?? dflt;
},
async set(value) {
await prf.setCfg(key, value ?? dflt);
},
});
const cfgIntel = _cfg('intel', false);
const cfgRezW = _cfg('rez-w', 1080);
const cfgRezH = _cfg('rez-h', 1920);
const cfgDisplayMode = _cfg('display-mode', 'borderless');
const displayModeList = [
{ title: 'Window', value: 'window' },
{ title: 'Borderless window', value: 'borderless' },
{ title: 'Fullscreen', value: 'fullscreen' },
];
const cfgDisplay = _cfg('display', 'default');
const cfgDisplayRotation = _cfg('display-rotation', 0);
const displayRotationList = [
{ title: 'Unchanged', value: 0 },
{ title: 'Portrait', value: 90 },
{ title: 'Portrait (flipped)', value: 270 },
];
const cfgBorderlessFullscreen = _cfg('borderless-fullscreen', false);
const cfgAime = _cfg('aime', false);
const aimeCode = ref('');
const capabilities: Ref<string[]> = ref([]);
const displayList: Ref<{ title: string; value: string }[]> = ref([
{
title: 'Primary',
@ -51,6 +25,13 @@ const displayList: Ref<{ title: string; value: string }[]> = ref([
},
]);
const hookList: Ref<{ title: string; value: string }[]> = ref([
{
title: 'segatools-mu3hook',
value: 'segatools-mu3hook',
},
]);
unproxied_invoke('read_profile_data', {
path: 'aime.txt',
})
@ -101,13 +82,54 @@ const aimeCodeModel = computed({
</script>
<template>
<OptionCategory title="Display options">
<OptionCategory title="General">
<!-- <OptionRow title="mu3.exe">
<FilePicker
field="game-dir"
default=""
:directory="false"
promptname="mu3.exe"
extension="exe"
></FilePicker>
</OptionRow> -->
<OptionRow title="mu3hook">
<Select
:model-value="prf.cfg('hook', 'segatools-mu3hook')"
:options="hookList"
option-label="title"
option-value="value"
></Select>
</OptionRow>
<OptionRow title="amfs">
<FilePicker
field="amfs"
default=""
placeholder="amfs"
:directory="true"
></FilePicker>
</OptionRow>
<OptionRow title="option">
<FilePicker
field="option"
default="option"
:directory="true"
></FilePicker>
</OptionRow>
<OptionRow title="appdata">
<FilePicker
field="appdata"
default="appdata"
:directory="true"
></FilePicker>
</OptionRow>
</OptionCategory>
<OptionCategory title="Display">
<OptionRow
v-if="capabilities.includes('display')"
title="Target display"
>
<Select
v-model="cfgDisplay"
:model-value="prf.cfg('display', 'default')"
:options="displayList"
option-label="title"
option-value="value"
@ -120,7 +142,10 @@ const aimeCodeModel = computed({
:min="480"
:max="9999"
:use-grouping="false"
v-model="cfgRezW"
:model-value="
// prettier-ignore Because primevue fucked up
prf.cfg('rez-w', 1080) as any
"
/>
x
<InputNumber
@ -129,13 +154,20 @@ const aimeCodeModel = computed({
:min="640"
:max="9999"
:use-grouping="false"
v-model="cfgRezH"
:model-value="
// prettier-ignore
prf.cfg('rez-h', 1920) as any
"
/>
</OptionRow>
<OptionRow title="Display mode">
<SelectButton
v-model="cfgDisplayMode"
:options="displayModeList"
:model-value="prf.cfg('display-mode', 'borderless')"
:options="[
{ title: 'Window', value: 'window' },
{ title: 'Borderless window', value: 'borderless' },
{ title: 'Fullscreen', value: 'fullscreen' },
]"
option-label="title"
option-value="value"
/>
@ -145,42 +177,96 @@ const aimeCodeModel = computed({
v-if="capabilities.includes('display')"
>
<SelectButton
v-model="cfgDisplayRotation"
:options="displayRotationList"
:model-value="prf.cfg('display-rotation', 0)"
:options="[
{ title: 'Unchanged', value: 0 },
{ title: 'Portrait', value: 90 },
{ title: 'Portrait (flipped)', value: 270 },
]"
option-label="title"
option-value="value"
:disabled="cfgDisplay === 'default'"
:disabled="prf.cfg('display', 'default').value === 'default'"
/>
</OptionRow>
<OptionRow
title="Match display resolution with the game"
v-if="capabilities.includes('display')"
>
<Toggle
<!-- @vue-expect-error -->
<ToggleSwitch
:disabled="
cfgDisplay === 'default' || cfgDisplayMode != 'borderless'
prf.cfg('display', 'default').value === 'default' ||
prf.cfg('display-mode', 'borderless').value != 'borderless'
"
:model-value="prf.cfg('borderless-fullscreen', false)"
/>
</OptionRow>
</OptionCategory>
<OptionCategory title="Network">
<OptionRow title="Server address">
<InputText
class="shrink"
size="small"
:maxlength="40"
placeholder="192.168.1.234"
:model-value="
// prettier-ignore
prf.cfg<string>('dns-default', '') as any
"
/> </OptionRow
><OptionRow title="Keychip">
<InputText
class="shrink"
size="small"
:maxlength="16"
placeholder="A123-01234567890"
:model-value="
// prettier-ignore
prf.cfg('keychip', '') as any
"
/> </OptionRow
><OptionRow title="Subnet">
<InputText
class="shrink"
size="small"
:maxlength="15"
placeholder="192.168.1.0"
:model-value="
// prettier-ignore
prf.cfg('subnet', '') as any
"
v-model="cfgBorderlessFullscreen"
/>
</OptionRow>
</OptionCategory>
<OptionCategory title="Misc">
<OptionRow title="OpenSSL bug workaround for Intel 10th gen">
<Toggle v-model="cfgIntel" />
<!-- @vue-expect-error -->
<ToggleSwitch :model-value="prf.cfg('intel', false)" />
</OptionRow>
<OptionRow title="Aime emulation">
<Toggle v-model="cfgAime" />
<!-- @vue-expect-error -->
<ToggleSwitch :model-value="prf.cfg('aime', false)" />
</OptionRow>
<OptionRow title="Aime code">
<InputText
class="shrink"
size="small"
:disabled="prf.cfg('aime') !== true"
:disabled="prf.cfg<boolean>('aime', false).value !== true"
:maxlength="20"
placeholder="00000000000000000000"
v-model="aimeCodeModel"
/>
</OptionRow>
<OptionRow title="More segatools options">
<FileEditor filename="segatools-base.ini" />
</OptionRow>
<OptionRow title="Inohara config">
<FileEditor
filename="inohara.cfg"
promptname="inohara config file"
extension="cfg"
/>
</OptionRow>
</OptionCategory>
</template>

View File

@ -1,3 +1,4 @@
import { Ref, computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { listen } from '@tauri-apps/api/event';
import { open } from '@tauri-apps/plugin-dialog';
@ -75,101 +76,118 @@ export const usePkgStore = defineStore('pkg', {
},
});
export const usePrfStore = defineStore('prf', {
state: (): { prf: Profile | null; list: ProfileMeta[] } => {
return {
prf: null,
list: [],
};
},
getters: {
current: (state) => state.prf,
isPkgEnabled: (state) => (pkg: Package | undefined) =>
pkg !== undefined && state.prf?.data.mods.includes(pkgKey(pkg)),
cfg: (state) => (key: string) => state.prf?.data.cfg[key],
},
actions: {
setupListeners() {
listen<InstallStatus>('install-end', async () => {
await this.reload();
});
},
export const usePrfStore = defineStore('prf', () => {
const current: Ref<Profile | null> = ref(null);
const list: Ref<ProfileMeta[]> = ref([]);
async prompt() {
const exePath = await open({
multiple: false,
directory: false,
filters: [
{
name: 'mu3.exe or chusanApp.exe',
extensions: ['exe'],
},
],
});
if (exePath !== null) {
await this.create(exePath);
}
},
const isPkgEnabled = (pkg: Package | undefined) =>
computed(
() =>
pkg !== undefined &&
current.value !== null &&
current.value?.data.mods.includes(pkgKey(pkg))
);
async create(exePath: string) {
try {
await invoke('init_profile', { exePath });
await this.reload();
await this.reloadList();
} catch (e) {
this.prf = null;
}
const reload = async () => {
current.value = await invoke('get_current_profile');
if (current.value !== null) {
changePrimaryColor(current.value.game);
}
};
if (this.prf !== null) {
const pkgs = usePkgStore();
pkgs.reloadAll();
}
},
const save = async () => {
await invoke('save_current_profile');
};
async switchTo(game: Game, name: string) {
await invoke('load_profile', { game, name });
await this.reload();
if (this.prf !== null) {
const pkgs = usePkgStore();
pkgs.reloadAll();
}
},
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();
}
},
});
async save() {
await invoke('save_current_profile');
},
const prompt = async () => {
const exePath = await open({
multiple: false,
directory: false,
filters: [
{
name: 'mu3.exe or chusanApp.exe',
extensions: ['exe'],
},
],
});
if (exePath !== null) {
await create(exePath);
}
};
async reload() {
this.prf = await invoke('get_current_profile');
if (this.prf !== null) {
changePrimaryColor(this.prf.game);
}
},
const create = async (exePath: string) => {
try {
await invoke('init_profile', { exePath });
await reload();
await reloadList();
} catch (e) {
current.value = null;
}
async reloadList() {
const raw = (await invoke('list_profiles')) as [Game, string][];
if (current.value !== null) {
const pkgs = usePkgStore();
pkgs.reloadAll();
}
};
this.list = raw.map(([game, name]) => {
return {
game,
name,
};
});
},
const switchTo = async (game: Game, name: string) => {
await invoke('load_profile', { game, name });
await reload();
if (current.value !== null) {
const pkgs = usePkgStore();
pkgs.reloadAll();
}
};
async togglePkg(pkg: Package | undefined, enable: boolean) {
if (pkg === undefined) {
return;
}
await invoke('toggle_package', { key: pkgKey(pkg), enable });
await this.reload();
await this.save();
},
const reloadList = async () => {
const raw = (await invoke('list_profiles')) as [Game, string][];
async setCfg(key: string, value: string | boolean | number) {
await invoke('set_cfg', { key, value });
await this.reload();
await this.save();
},
},
list.value = raw.map(([game, name]) => {
return {
game,
name,
};
});
};
const togglePkg = async (pkg: Package | undefined, enable: boolean) => {
if (pkg === undefined) {
return;
}
await invoke('toggle_package', { key: pkgKey(pkg), enable });
await reload();
await save();
};
listen<InstallStatus>('install-end', async () => {
await reload();
});
return {
current,
list,
isPkgEnabled,
reload,
save,
cfg,
prompt,
create,
switchTo,
reloadList,
togglePkg,
};
});

View File

@ -28,6 +28,7 @@ export interface ProfileMeta {
export interface Profile extends ProfileMeta {
data: {
exe_dir: string;
mods: string[];
cfg: { [key: string]: string | boolean | number };
};