feat: internationalization

This commit is contained in:
2025-04-22 21:34:55 +00:00
parent 58c692a879
commit ce03668252
36 changed files with 1069 additions and 563 deletions

View File

@ -221,15 +221,12 @@ listen<DownloadingStatus>('download-progress', (event) => {
>
<div class="fixed w-full flex z-100">
<TabList class="grow" :show-navigators="false">
<Tab value="users"><div class="pi pi-users"></div></Tab>
<Tab value="users"><div class="pi pi-home"></div></Tab>
<Tab :disabled="isProfileDisabled" value="loc"
><div class="pi pi-box"></div
></Tab>
<Tab
v-if="
prf.current?.meta.game === 'chunithm' &&
prf.current.data.sgt.target.length > 0
"
v-if="(prf.current?.data.sgt.target.length ?? 0) > 0"
value="patches"
><div class="pi pi-ticket"></div
></Tab>

View File

@ -12,7 +12,7 @@ invoke('get_changelog').then((s) => (changelog.value = s as string));
<template>
<h1>About</h1>
STARTLINER is a simple launcher, configuration tool and mod manager for
STARTLINER is a launcher, configuration tool and mod manager for
O.N.G.E.K.I. and CHUNITHM.
<h1>Changelog</h1>
<ScrollPanel style="height: 200px">

View File

@ -8,6 +8,9 @@ import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { Package } from '../types';
import { pkgKey } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
search: String,
@ -55,7 +58,7 @@ const missing = computed(() => {
</script>
<template>
<Fieldset legend="Missing" v-if="(missing?.length ?? 0) > 0">
<Fieldset :legend="t('store.missing')" v-if="(missing?.length ?? 0) > 0">
<div class="flex items-center" v-for="p in missing">
<ModTitlecard
show-namespace

View File

@ -10,6 +10,9 @@ import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { Feature, Package } from '../types';
import { hasFeature } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const pkgs = usePkgStore();
@ -38,12 +41,7 @@ if (unsupported.value === true && model.value === true) {
<div class="flex items-center">
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
<UpdateButton :pkg="pkg" />
<span
v-tooltip="
unsupported &&
'This package is currently incompatible with STARTLINER.'
"
>
<span v-tooltip="unsupported && t('store.incompatible')">
<ToggleSwitch
v-if="hasFeature(pkg, Feature.Mod) || unsupported === true"
class="scale-[1.33] shrink-0"

View File

@ -8,6 +8,9 @@ import ModStoreEntry from './ModStoreEntry.vue';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { pkgKey } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const pkgs = usePkgStore();
const prf = usePrfStore();
@ -60,7 +63,7 @@ const getRecommendedTooltip = () => {
return 'segatools-mu3hook';
}
if (prf.current!.meta.game === 'chunithm') {
return 'segatools-chusanhook and mempatcher';
return 'segatools-chusanhook + mempatcher';
}
return '';
};
@ -80,15 +83,17 @@ const installRecommended = () => {
<div class="flex gap-4 items-center">
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<div class="grow">Show installed</div>
<div class="grow">{{ t('store.installed') }}</div>
<ToggleSwitch v-model="pkgs.showInstalled" />
</div>
<div class="flex gap-2">
<div class="text-amber-400 grow">Show deprecated</div>
<div class="text-amber-400 grow">
{{ t('store.deprecated') }}
</div>
<ToggleSwitch v-model="pkgs.showDeprecated" />
</div>
<!-- <div class="flex gap-2">
<div class="text-red-400 grow">Show NSFW</div>
<div class="text-red-400 grow">{{ t('store.nsfw') }}</div>
<ToggleSwitch v-model="pkgs.showNSFW" />
</div> -->
</div>
@ -114,7 +119,7 @@ const installRecommended = () => {
<Divider />
<Button
v-if="shouldShowRecommended"
label="Install recommended packages"
:label="t('store.installRecommended')"
v-tooltip="getRecommendedTooltip"
icon="pi pi-plus"
class="mb-3"

View File

@ -14,6 +14,9 @@ import NetworkOptions from './options/Network.vue';
import SegatoolsOptions from './options/Segatools.vue';
import StartlinerOptions from './options/Startliner.vue';
import { usePrfStore } from '../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
@ -60,7 +63,7 @@ prf.reload();
title="Extensions"
v-if="prf.current!.meta.game === 'chunithm'"
>
<OptionRow title="Saekawa config">
<OptionRow :title="t('cfg.extensions.saekawa')">
<FileEditor
filename="saekawa.toml"
promptname="saekawa config file"
@ -68,31 +71,31 @@ prf.reload();
/> </OptionRow
></OptionCategory>
<OptionCategory
title="Extensions"
:title="t('cfg.extensions.title')"
v-if="prf.current!.meta.game === 'ongeki'"
>
<OptionRow title="Inohara config">
<OptionRow :title="t('cfg.extensions.inohara')">
<FileEditor
filename="inohara.cfg"
promptname="inohara config file"
extension="cfg"
/>
</OptionRow>
<OptionRow title="BepInEx console">
<OptionRow :title="t('cfg.extensions.bepInExConsole')">
<!-- @vue-expect-error -->
<ToggleSwitch v-model="prf.current!.data.bepinex.console" />
</OptionRow>
<OptionRow
title="Audio mode"
tooltip="Exclusive 2-channel mode requires 7EVENDAYSHOLIDAYS-ExclusiveAudio"
:title="t('cfg.extensions.audioMode')"
:tooltip="t('cfg.extensions.audioTooltip')"
>
<SelectButton
v-model="prf.current!.data.mu3_ini!.audio"
:options="[
{ title: 'Shared', value: 'Shared' },
{ title: 'Exclusive 6-channel', value: 'Excl6Ch' },
{ title: 'Exclusive 2-channel', value: 'Excl2Ch' },
{ title: t('cfg.extensions.audioShared'), value: 'Shared' },
{ title: t('cfg.extensions.audio6Ch'), value: 'Excl6Ch' },
{ title: t('cfg.extensions.audio2Ch'), value: 'Excl2Ch' },
]"
:allow-empty="false"
option-label="title"
@ -100,7 +103,7 @@ prf.reload();
/></OptionRow>
<OptionRow
title="Sample rate"
:title="t('cfg.extensions.sampleRate')"
v-if="
prf.current?.data.mods.includes(
'7EVENDAYSHOLIDAYS-ExclusiveAudio'
@ -126,8 +129,8 @@ prf.reload();
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-Blacklist')
"
class="number-input"
title="Song ID Blacklist"
tooltip="Scores on charts within this ID range will not be saved nor uploaded"
:title="t('cfg.extensions.blacklist')"
:tooltip="t('cfg.extensions.blacklistTooltip')"
><InputNumber
class="shrink"
size="small"
@ -165,8 +168,8 @@ prf.reload();
/>
</OptionRow>
<OptionRow
title="Unlock Bonus Tracks"
tooltip="Disabling this option can help declutter the song list"
:title="t('cfg.extensions.bonusTracks')"
:tooltip="t('cfg.extensions.bonusTracksTooltip')"
v-if="
prf.current?.data.mods.includes(
'7EVENDAYSHOLIDAYS-UnlockAllMusic'

View File

@ -6,6 +6,9 @@ import ToggleSwitch from 'primevue/toggleswitch';
import OptionRow from './OptionRow.vue';
import { usePrfStore } from '../stores';
import { Patch } from '../types';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
@ -45,17 +48,20 @@ const hexModel = computed({
};
},
});
// Doesn't need to be reactive
const nameKey = `patch.${props.patch?.id}`;
let name = t(nameKey);
if (name === nameKey) {
name = props.patch?.name ?? 'No name';
}
</script>
<template>
<OptionRow
:title="patch?.name"
:tooltip="patch?.tooltip"
:greytext="patch?.id"
>
<OptionRow :title="name" :tooltip="patch?.tooltip" :greytext="patch?.id">
<ToggleSwitch
v-if="patch?.type === undefined"
:model-value="prf.current!.data.patches[patch!.id!] !== undefined"
:model-value="prf.current!.data.patches?.[patch!.id!] !== undefined"
@update:model-value="(v: boolean) => toggleUnary(patch!.id!, v)"
/>
<InputNumber

View File

@ -6,6 +6,9 @@ import PatchEntry from './PatchEntry.vue';
import { invoke } from '../invoke';
import { usePrfStore } from '../stores';
import { Patch } from '../types';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
@ -30,18 +33,21 @@ invoke('list_patches', { target: prf.current!.data.sgt.target }).then(
})) as Patch[];
})();
const errorMessage =
"No compatible patches found. Make sure you're using unpacked and unpatched files.";
const errorMessage = t('patch.noneFound');
</script>
<template>
<OptionCategory title="chusanApp.exe" always-found>
<OptionCategory
v-if="prf.current?.meta.game === 'chunithm'"
title="chusanApp.exe"
always-found
>
<PatchEntry
v-if="gamePatches !== null"
v-for="p in gamePatches"
:patch="p"
/>
<div v-if="gamePatches === null">Loading...</div>
<div v-if="gamePatches === null">{{ t('patch.loading') }}</div>
<div v-if="gamePatches !== null && gamePatches.length === 0">
{{ errorMessage }}
</div>

View File

@ -2,12 +2,17 @@
import { Ref, ref } from 'vue';
import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import Select from 'primevue/select';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import * as path from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import ProfileListEntry from './ProfileListEntry.vue';
import { invoke } from '../invoke';
import { useClientStore, useGeneralStore, usePrfStore } from '../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const client = useClientStore();
@ -61,7 +66,7 @@ const importPick = async () => {
directory: false,
filters: [
{
name: 'STARTLINER template',
name: t('profile.template'),
extensions: ['zip'],
},
],
@ -117,42 +122,94 @@ const importPick = async () => {
</div>
</div>
</Dialog>
<div v-if="prf.list.length === 0">
Welcome to STARTLINER! Start by creating a profile.
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button
label="O.N.G.E.K.I. profile"
icon="pi pi-file-plus"
class="ongeki-button profile-button"
@click="() => prf.create('ongeki')"
/>
<Button
label="CHUNITHM profile"
icon="pi pi-file-plus"
class="chunithm-button profile-button"
@click="() => prf.create('chunithm')"
/>
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button
label="Import template"
icon="pi pi-file-import"
class="import-button profile-button"
@click="() => importPick()"
/>
<Button
:disabled="prf.current === null"
label="Export template"
icon="pi pi-file-export"
class="profile-button"
@click="() => openExportDialog()"
/>
</div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
<div v-for="p in prf.list">
<ProfileListEntry :p="p" />
<div style="float: left">
<div v-if="prf.list.length === 0">
{{ t('profile.welcome') }}
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button
:label="t('profile.create', { game: t('game.ongeki') })"
icon="pi pi-file-plus"
class="ongeki-button profile-button"
@click="() => prf.create('ongeki')"
/>
<Button
:label="t('profile.create', { game: t('game.chunithm') })"
icon="pi pi-file-plus"
class="chunithm-button profile-button"
@click="() => prf.create('chunithm')"
/>
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button
label="Import template"
icon="pi pi-file-import"
class="import-button profile-button"
@click="() => importPick()"
/>
<Button
:disabled="prf.current === null"
label="Export template"
icon="pi pi-file-export"
class="profile-button"
@click="() => openExportDialog()"
/>
</div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
<div v-for="p in prf.list">
<ProfileListEntry :p="p" />
</div>
</div>
</div>
<div style="float: right" class="mr-5 mt-3 flex flex-col gap-4 items-end">
<div>
<div class="pi pi-language mr-2"></div>
<Select
:model-value="client.locale"
@update:model-value="async (v) => await client.setLocale(v)"
style="width: 200px"
:options="[
{ title: 'English', value: 'en' },
// { title: '日本語', value: 'ja' },
]"
size="small"
option-label="title"
option-value="value"
></Select>
</div>
<SelectButton
style="height: 50px"
v-model="client.scaleModel"
:options="[
{ title: 'S', size: '0.8em', value: 's' },
{ title: 'M', size: '1.0em', value: 'm' },
{ title: 'L', size: '1.2em', value: 'l' },
{ title: 'XL', size: '1.4em', value: 'xl' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
><template #option="slotProps">
<div :style="{ fontSize: slotProps.option.size }">
{{ slotProps.option.title }}
</div>
</template></SelectButton
>
<SelectButton
style="height: 50px"
:model-value="client.theme"
@update:model-value="(v) => client.setTheme(v)"
:options="[
{ title: 'System', value: 'system', icon: 'pi pi-home' },
{ title: 'Light', value: 'light', icon: 'pi pi-sun' },
{ title: 'Dark', value: 'dark', icon: 'pi pi-moon' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
><template #option="slotProps">
<div :class="slotProps.option.icon"></div> </template
></SelectButton>
</div>
</template>

View File

@ -7,6 +7,9 @@ import * as path from '@tauri-apps/api/path';
import { invoke } from '../invoke';
import { useGeneralStore, usePrfStore } from '../stores';
import { ProfileMeta } from '../types';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const general = useGeneralStore();
const prf = usePrfStore();
@ -60,8 +63,10 @@ const deleteProfile = async () => {
const promptDeleteProfile = async () => {
confirmDialog.require({
message: `Are you sure you want to delete ${props.p?.game}-${props.p?.name}?`,
header: 'Delete profile',
message: t('profile.reallyDelete', {
profile: `${props.p?.game}-${props.p?.name}`,
}),
header: t('profile.delete'),
accept: deleteProfile,
});
};

View File

@ -8,6 +8,9 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
import Onboarding from './Onboarding.vue';
import { invoke } from '../invoke';
import { useClientStore, usePrfStore } from '../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const client = useClientStore();
@ -24,22 +27,22 @@ const startline = async (force: boolean, refresh: boolean) => {
if (start_check.length > 0) {
const message = start_check.map((o) => {
if ('MissingRemotePackage' in o) {
return `Package missing: ${o.MissingRemotePackage}`;
return `${t('start.error.package')}: ${o.MissingRemotePackage}`;
} else if ('MissingLocalPackage' in o) {
return `Package missing: ${o.MissingLocalPackage}`;
return `${t('start.error.package')}: ${o.MissingLocalPackage}`;
} else if ('MissingDependency' in o) {
return `Dependency missing: ${(o.MissingDependency as string[]).join(' ')}`;
return `${t('start.error.dependency')}: ${(o.MissingDependency as string[]).join(' ')}`;
} else if ('MissingTool' in o) {
return `Tool missing: ${o.MissingTool}`;
return `${t('start.error.tool')}: ${o.MissingTool}`;
} else {
return 'Unknown error';
return t('start.error.unknown');
}
});
confirmDialog.require({
message: message.join('\n'),
header: 'Start check failed',
acceptLabel: 'Run anyway',
rejectLabel: 'Cancel',
header: t('start.failed'),
acceptLabel: t('start.accept'),
rejectLabel: t('cancel'),
accept: () => {
startline(true, refresh);
},
@ -62,16 +65,16 @@ const kill = async () => {
const disabledTooltip = computed(() => {
if (prf.current?.data.sgt.target.length === 0) {
return 'The game path must be specified';
return t('start.tooltip.game');
}
if (prf.current?.data.sgt.amfs.length === 0) {
return 'The amfs path must be specified';
return t('start.tooltip.amfs');
}
if (
prf.current?.data.sgt.hook === null ||
prf.current?.data.sgt.hook === undefined
) {
return 'A segatools hook package is necessary';
return t('start.tooltip.segatools');
}
return null;
});
@ -96,32 +99,50 @@ const createShortcut = async () => {
}
};
const menuItems = [
{
label: 'Refresh and start',
icon: 'pi pi-sync',
tooltip: 'test',
command: async () => await startline(false, true),
},
{
label: 'Start unchecked',
icon: 'pi pi-exclamation-circle',
command: async () => await startline(true, false),
},
{
label: 'Create desktop shortcut',
icon: 'pi pi-link',
command: createShortcut,
},
{
label: 'Help',
icon: 'pi pi-question-circle',
command: () => {
onboardingFirstTime.value = false;
onboardingVisible.value = true;
const menuItems = computed(() => {
const base = [
{
label: t('start.button.unchecked'),
icon: 'pi pi-exclamation-circle',
command: async () => await startline(true, false),
},
},
];
{
label: t('start.button.shortcut'),
icon: 'pi pi-link',
command: createShortcut,
},
{
label: t('start.button.help'),
icon: 'pi pi-question-circle',
command: () => {
onboardingFirstTime.value = false;
onboardingVisible.value = true;
},
},
];
if (prf.current === null) {
return [];
}
if (prf.current.meta.game === 'chunithm') {
return base;
}
if (prf.current.meta.game === 'ongeki') {
return [
{
label: t('start.button.refresh'),
icon: 'pi pi-sync',
command: async () => await startline(false, true),
},
...base,
{
label: t('start.button.cache'),
icon: 'pi pi-trash',
command: async () => {},
},
];
}
});
const menu = ref();
const showContextMenu = (event: Event) => {
@ -177,7 +198,7 @@ const tryStart = () => {
v-else-if="startStatus === 'preparing'"
disabled
icon="pi pi-spin pi-spinner"
label="START"
:label="t('start.button.start')"
aria-label="start"
size="small"
class="m-2.5"
@ -186,7 +207,7 @@ const tryStart = () => {
v-else
:disabled="false"
icon="pi pi-ban"
label="STOP"
:label="t('start.button.stop')"
aria-label="stop"
size="small"
class="m-2.5"

View File

@ -12,6 +12,9 @@ import { invoke } from '../../invoke';
import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types';
import { hasFeature, pkgKey } from '../../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const pkgs = usePkgStore();
const prf = usePrfStore();
@ -60,14 +63,18 @@ load();
<template>
<OptionCategory title="Aime">
<OptionRow
title="Aime type"
tooltip="Additional Aime plugins can be downloaded from the package store."
:title="t('cfg.aime.type')"
:tooltip="
t('cfg.segatools.installTooltip', {
thing: t('cfg.aime.modules'),
})
"
>
<Select
v-model="prf.current!.data.sgt.aime"
:options="[
{ title: 'hardware', value: 'Disabled' },
{ title: 'segatools built-in emulation', value: 'BuiltIn' },
{ title: t('cfg.hardware'), value: 'Disabled' },
{ title: t('cfg.segatools.builtIn'), value: 'BuiltIn' },
...pkgs.byFeature(Feature.Aime).map((p) => {
return {
title: pkgKey(p),
@ -83,8 +90,8 @@ load();
></Select>
</OptionRow>
<OptionRow
title="Aime code"
tooltip="Only applicable with the segatools built-in emulation or with compatible third-party packages"
:title="t('cfg.aime.code')"
:tooltip="t('cfg.aime.codeTooltip')"
v-if="prf.current!.data.sgt.aime !== 'Disabled'"
>
<InputText
@ -97,7 +104,7 @@ load();
/>
</OptionRow>
<div v-if="prf.current!.data.sgt.aime?.hasOwnProperty('AMNet')">
<OptionRow title="Server name">
<OptionRow :title="t('cfg.aime.serverName')">
<InputText
class="shrink"
size="small"
@ -106,7 +113,7 @@ load();
v-model="prf.current!.data.sgt.amnet.name"
/>
</OptionRow>
<OptionRow title="Server address">
<OptionRow :title="t('cfg.network.address')">
<InputText
class="shrink"
size="small"
@ -116,22 +123,21 @@ load();
/>
</OptionRow>
<OptionRow
title="Use AiMeDB for physical cards"
tooltip="Whether physical cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, enable this option to load the same account data/profile as you would get on a physical cab."
:title="t('cfg.aime.aimedb')"
:tooltip="t('cfg.aime.aimedbTooltip')"
>
<ToggleSwitch v-model="prf.current!.data.sgt.amnet.physical" />
</OptionRow>
</div>
<OptionRow
title="Aime serial port"
tooltip="Ports can be checked in Devices and Printers or at googlechromelabs.github.io/serial-terminal
For AIC Pico, the AIME port should be selected."
:title="t('cfg.aime.serialPort')"
:tooltip="t('cfg.aime.serialPortTooltip')"
v-if="prf.current!.data.sgt.aime === 'Disabled'"
>
<Select
v-model="prf.current!.data.sgt.aime_port"
:options="[
{ title: 'default', value: null },
{ title: t('default'), value: null },
...Object.entries(coms ?? {}).map(([title, value]) => {
return {
title,
@ -139,7 +145,7 @@ load();
};
}),
]"
placeholder="default"
:placeholder="t('default')"
option-label="title"
option-value="value"
></Select>

View File

@ -8,11 +8,14 @@ import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { invoke } from '../../invoke';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const capabilities: Ref<string[]> = ref([]);
const displayList: Ref<{ title: string; value: string }[]> = ref([
{
title: 'Primary',
title: t('cfg.display.primary'),
value: 'default',
},
]);
@ -25,7 +28,7 @@ const extraDisplayOptionsDisabled = computed(() => {
const loadDisplays = () => {
const newList = [
{
title: 'Primary',
title: t('cfg.display.primary'),
value: 'default',
},
];
@ -74,7 +77,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
<OptionCategory title="Display">
<OptionRow
v-if="capabilities.includes('display')"
title="Target display"
:title="t('cfg.display.target')"
>
<Select
v-model="prf.current!.data.display.target"
@ -108,7 +111,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
v-model="prf.current!.data.display.rez[1]"
/>
</OptionRow>
<OptionRow title="Display mode">
<OptionRow :title="t('cfg.display.mode')">
<SelectButton
v-model="prf.current!.data.display.mode"
:options="[
@ -122,7 +125,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
/>
</OptionRow>
<OptionRow
title="Display rotation"
:title="t('cfg.display.rotation')"
v-if="capabilities.includes('display')"
>
<SelectButton
@ -130,12 +133,18 @@ const canSkipPrimarySwitch = game === 'ongeki';
:options="
isVertical
? [
{ title: 'Portrait', value: 90 },
{ title: 'Portrait (flipped)', value: 270 },
{ title: t('cfg.display.portrait'), value: 90 },
{
title: `${t('cfg.display.portrait')} (${t('cfg.display.flipped')})`,
value: 270,
},
]
: [
{ title: 'Landscape', value: 0 },
{ title: 'Landscape (flipped)', value: 180 },
{ title: t('cfg.display.landscape'), value: 0 },
{
title: `${t('cfg.display.landscape')} (${t('cfg.display.flipped')})`,
value: 180,
},
]
"
:allow-empty="true"
@ -147,7 +156,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
<OptionRow
v-if="capabilities.includes('display')"
class="number-input"
title="Refresh Rate"
:title="t('cfg.display.refreshRate')"
>
<InputNumber
v-if="game === 'ongeki'"
@ -173,9 +182,9 @@ const canSkipPrimarySwitch = game === 'ongeki';
/>
</OptionRow>
<OptionRow
title="Borderless fullscreen"
:title="t('cfg.display.borderlessFullscreen')"
v-if="capabilities.includes('display')"
tooltip="Match display resolution with the game."
:tooltip="t('cfg.display.borderlessFullscreenTooltip')"
>
<ToggleSwitch
:disabled="
@ -186,7 +195,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
/>
</OptionRow>
<OptionRow
title="Skip switching primary display"
:title="t('cfg.display.dontSwitchPrimary')"
v-if="
capabilities.includes('display') &&
prf.current?.data.display.target !== 'default' &&
@ -194,7 +203,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
displayList.length > 2) &&
canSkipPrimarySwitch
"
dangerous-tooltip="Only enable this option if switching the primary display causes issues. The monitors must have a matching refresh rate."
:dangerous-tooltip="t('cfg.display.dontSwitchPrimaryTooltip')"
>
<ToggleSwitch
:disabled="extraDisplayOptionsDisabled"
@ -202,7 +211,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
/>
</OptionRow>
<OptionRow
title="Display index"
:title="t('cfg.display.index')"
class="number-input"
v-if="
capabilities.includes('display') &&

View File

@ -5,27 +5,27 @@ import KeyboardKey from '../KeyboardKey.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Keyboard">
<OptionRow
title="Enable"
tooltip="Only applicable if the IO module is set to segatools built-in (keyboard) or a compatible third-party module (like mu3io.NET)"
>
<OptionCategory :title="t('cfg.keyboard.title')">
<OptionRow :title="t('enable')" :tooltip="t('cfg.keyboard.tooltip')">
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
</OptionRow>
<OptionRow
title="Lever mode"
:title="t('cfg.keyboard.leverMode')"
v-if="prf.current!.data.keyboard!.game === 'Ongeki'"
>
<SelectButton
v-model="prf.current!.data.keyboard!.data.use_mouse"
:options="[
{ title: 'XInput', value: false },
{ title: 'Mouse', value: true },
{ title: t('cfg.keyboard.mouse'), value: true },
]"
:allow-empty="false"
option-label="title"

View File

@ -4,18 +4,24 @@ import FileEditor from '../FileEditor.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Misc">
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
<OptionCategory :title="t('cfg.misc.title')">
<OptionRow
:title="t('cfg.misc.intel')"
:dangerous-tooltip="t('cfg.misc.intelTooltip')"
>
<ToggleSwitch v-model="prf.current!.data.sgt.intel" />
</OptionRow>
<OptionRow
title="More segatools options"
tooltip="Advanced options not covered by STARTLINER"
:title="t('cfg.misc.other')"
:tooltip="t('cfg.misc.otherTooltip')"
>
<!-- <Button icon="pi pi-refresh" size="small" /> -->
<FileEditor filename="segatools-base.ini" />

View File

@ -6,18 +6,21 @@ import FilePicker from '../FilePicker.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Network">
<OptionRow title="Network type">
<OptionCategory :title="t('cfg.network.title')">
<OptionRow :title="t('cfg.network.type')">
<SelectButton
v-model="prf.current!.data.network.network_type"
:options="[
{ title: 'Remote', value: 'Remote' },
{ title: 'Local (ARTEMiS)', value: 'Artemis' },
{ title: t('cfg.network.remote'), value: 'Remote' },
{ title: t('cfg.network.localArtemis'), value: 'Artemis' },
]"
:allow-empty="false"
option-label="title"
@ -25,8 +28,8 @@ const prf = usePrfStore();
/>
</OptionRow>
<OptionRow
v-if="prf.current!.data.network.network_type == 'Artemis'"
title="ARTEMiS path"
v-if="prf.current!.data.network.network_type === 'Artemis'"
:title="t('cfg.network.artemisPath')"
>
<FilePicker
:directory="false"
@ -47,7 +50,7 @@ const prf = usePrfStore();
</OptionRow> -->
<OptionRow
v-if="prf.current!.data.network.network_type == 'Remote'"
title="Server address"
:title="t('cfg.network.address')"
>
<InputText
class="shrink"
@ -58,7 +61,7 @@ const prf = usePrfStore();
/> </OptionRow
><OptionRow
v-if="prf.current!.data.network.network_type == 'Remote'"
title="Keychip"
:title="t('cfg.network.keychip')"
>
<InputText
class="shrink"
@ -67,7 +70,7 @@ const prf = usePrfStore();
placeholder="A123-01234567890"
v-model="prf.current!.data.network.keychip"
/> </OptionRow
><OptionRow title="Subnet">
><OptionRow :title="t('cfg.network.subnet')">
<InputText
class="shrink"
size="small"
@ -76,7 +79,7 @@ const prf = usePrfStore();
v-model="prf.current!.data.network.subnet"
/>
</OptionRow>
<OptionRow title="Address suffix">
<OptionRow :title="t('cfg.network.addrSuffix')">
<InputNumber
class="shrink"
size="small"

View File

@ -11,6 +11,9 @@ import { invoke } from '../../invoke';
import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types';
import { pkgKey } from '../../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const pkgs = usePkgStore();
@ -54,10 +57,10 @@ const checkSegatoolsIni = async (target: string) => {
</script>
<template>
<OptionCategory title="General">
<OptionCategory :title="t('cfg.segatools.general')">
<OptionRow
:title="names.exe"
tooltip="STARTLINER expects unpacked executables put into otherwise clean data."
:tooltip="t('cfg.segatools.targetTooltip')"
>
<FilePicker
:directory="false"
@ -104,7 +107,11 @@ const checkSegatoolsIni = async (target: string) => {
</OptionRow>
<OptionRow
:title="names.hook"
tooltip="Hooks can be downloaded from the package store."
:tooltip="
t('cfg.segatools.installTooltip', {
thing: t('cfg.segatools.hooks'),
})
"
>
<Select
v-model="prf.current!.data.sgt.hook"
@ -126,7 +133,11 @@ const checkSegatoolsIni = async (target: string) => {
</OptionRow>
<OptionRow
:title="names.io"
tooltip="IO plugins can be downloaded from the package store."
:tooltip="
t('cfg.segatools.installTooltip', {
thing: t('cfg.segatools.ioModules'),
})
"
>
<Select
v-model="prf.current!.data.sgt.io2"

View File

@ -1,92 +1,43 @@
<script setup lang="ts">
import { computed } from 'vue';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { useClientStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const client = useClientStore();
const offlineModel = computed({
get() {
return client.offlineMode;
},
async set(value: boolean) {
await client.setOfflineMode(value);
},
});
const updatesModel = computed({
get() {
return client.enableAutoupdates;
},
async set(value: boolean) {
await client.setAutoupdates(value);
},
});
const verboseModel = computed({
get() {
return client.verbose;
},
async set(value: boolean) {
await client.setVerbose(value);
},
});
const themeModel = computed({
get() {
return client.theme;
},
async set(value: 'light' | 'dark' | 'system') {
await client.setTheme(value);
},
});
</script>
<template>
<OptionCategory title="STARTLINER">
<OptionRow title="UI scaling">
<SelectButton
v-model="client.scaleModel"
:options="[
{ title: 'S', value: 's' },
{ title: 'M', value: 'm' },
{ title: 'L', value: 'l' },
{ title: 'XL', value: 'xl' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
<OptionRow
:title="t('cfg.startliner.offlineMode')"
:tooltip="`${t('cfg.startliner.offlineModeTooltip')} ${t('cfg.afterRestart')}`"
>
<ToggleSwitch
:model-value="client.offlineMode"
@update:model-value="
async (v) => await client.setOfflineMode(v)
"
/>
</OptionRow>
<OptionRow
title="Offline mode"
tooltip="Disables the package store. Applies after a restart."
>
<ToggleSwitch v-model="offlineModel" />
</OptionRow>
<OptionRow title="Enable automatic updates">
<ToggleSwitch v-model="updatesModel" />
<OptionRow :title="t('cfg.startliner.autoUpdate')">
<ToggleSwitch
:model-value="client.enableAutoupdates"
@update:model-value="
async (v) => await client.setAutoupdates(v)
"
></ToggleSwitch>
</OptionRow>
<OptionRow
title="Enable detailed logs"
tooltip="Applies after a restart."
:title="t('cfg.startliner.verbose')"
:tooltip="t('cfg.afterRestart')"
>
<ToggleSwitch v-model="verboseModel" />
</OptionRow>
<OptionRow title="Theme">
<SelectButton
v-model="themeModel"
:options="[
{ title: 'System', value: 'system' },
{ title: 'Light', value: 'light' },
{ title: 'Dark', value: 'dark' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
<ToggleSwitch
:model-value="client.verbose"
@update:model-value="async (v) => await client.setVerbose(v)"
/>
</OptionRow>
</OptionCategory>