forked from akanyan/STARTLINER
feat: more options
This commit is contained in:
@ -17,7 +17,6 @@ const pkg = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
|
||||
pkg.setupListeners();
|
||||
prf.setupListeners();
|
||||
|
||||
const currentTab = ref('3');
|
||||
|
||||
|
114
src/components/FileEditor.vue
Normal file
114
src/components/FileEditor.vue
Normal 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>
|
53
src/components/FilePicker.vue
Normal file
53
src/components/FilePicker.vue
Normal 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>
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
||||
|
192
src/stores.ts
192
src/stores.ts
@ -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,
|
||||
};
|
||||
});
|
||||
|
@ -28,6 +28,7 @@ export interface ProfileMeta {
|
||||
|
||||
export interface Profile extends ProfileMeta {
|
||||
data: {
|
||||
exe_dir: string;
|
||||
mods: string[];
|
||||
cfg: { [key: string]: string | boolean | number };
|
||||
};
|
||||
|
Reference in New Issue
Block a user