feat: groundwork for multi-profile support

This commit is contained in:
2025-03-03 02:07:15 +01:00
parent d25841853c
commit 6410ca2721
16 changed files with 744 additions and 184 deletions

View File

@ -7,53 +7,31 @@ import TabPanel from 'primevue/tabpanel';
import TabPanels from 'primevue/tabpanels';
import Tabs from 'primevue/tabs';
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { open } from '@tauri-apps/plugin-dialog';
import ModList from './ModList.vue';
import ModStore from './ModStore.vue';
import Options from './Options.vue';
import ProfileList from './ProfileList.vue';
import StartButton from './StartButton.vue';
import { usePkgStore } from '../stores';
import { changePrimaryColor } from '../util';
import { usePkgStore, usePrfStore } from '../stores';
const store = usePkgStore();
store.setupListeners();
const pkg = usePkgStore();
const prf = usePrfStore();
pkg.setupListeners();
prf.setupListeners();
const currentTab = ref('3');
const loadProfile = async (openWindow: boolean) => {
await store.reloadProfile();
if (store.profile === null && openWindow) {
const exePath = await open({
multiple: false,
directory: false,
filters: [
{
name: 'mu3.exe' /* or chusanApp.exe'*/,
extensions: ['exe'],
},
],
});
if (exePath !== null) {
await store.initProfile(exePath);
}
}
if (store.profile !== null) {
changePrimaryColor(store.profile.game);
currentTab.value = '0';
}
await store.reloadAll();
};
const isProfileDisabled = computed(() => store.profile === null);
onOpenUrl((urls) => {
console.log('deep link:', urls);
});
const isProfileDisabled = computed(() => prf.current === null);
onMounted(async () => {
await loadProfile(false);
await prf.reloadList();
await prf.reload();
if (prf.current !== null) {
await pkg.reloadAll();
currentTab.value = '0';
}
});
</script>
@ -93,14 +71,10 @@ onMounted(async () => {
missing.<br />Existing features are expected to break any
time.
<div v-if="isProfileDisabled">
<br />Select <code>mu3.exe</code> to create a
profile:<br />
<Button
label="Create profile"
icon="pi pi-plus"
aria-label="open-executable"
@click="loadProfile(true)"
/><br />
<br />Select <code>mu3.exe</code> to create a profile:
</div>
<ProfileList />
<div v-if="isProfileDisabled">
<div
style="
margin-top: 5px;
@ -115,10 +89,15 @@ onMounted(async () => {
(this will change in the future)
</div>
<img
v-if="store.profile?.game === 'Ongeki'"
v-if="prf.current?.game === 'ongeki'"
src="/sticker-ongeki.svg"
class="fixed bottom-0 right-0"
/>
<img
v-else-if="prf.current?.game === 'chunithm'"
src="/sticker-chunithm.svg"
class="fixed bottom-0 right-0"
/>
<br /><br /><br />
<Button
style="position: fixed; left: 10px; bottom: 10px"

View File

@ -1,20 +1,16 @@
<script setup lang="ts">
import Fieldset from 'primevue/fieldset';
import ModListEntry from './ModListEntry.vue';
import { usePkgStore } from '../stores';
import { Profile } from '../types';
import { usePkgStore, usePrfStore } from '../stores';
defineProps({
profile: Object as () => Profile,
});
const pkgs = usePkgStore();
const pkg = usePkgStore();
const prf = usePrfStore();
const group = () => {
const a = Object.assign(
{},
Object.groupBy(
pkgs.allLocal
pkg.allLocal
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
({ namespace }) => namespace
@ -23,7 +19,7 @@ const group = () => {
return a;
};
pkgs.reloadProfile();
prf.reload();
</script>
<template>

View File

@ -5,17 +5,17 @@ import { open } from '@tauri-apps/plugin-shell';
import InstallButton from './InstallButton.vue';
import ModTitlecard from './ModTitlecard.vue';
import UpdateButton from './UpdateButton.vue';
import { usePkgStore } from '../stores';
import { usePrfStore } from '../stores';
import { Package } from '../types';
const store = usePkgStore();
const prf = usePrfStore();
const props = defineProps({
pkg: Object as () => Package,
});
const toggle = async (value: boolean) => {
await store.toggle(props.pkg, value);
await prf.togglePkg(props.pkg, value);
};
</script>
@ -27,7 +27,7 @@ const toggle = async (value: boolean) => {
class="scale-[1.33] shrink-0"
inputId="switch"
:disabled="!pkg?.loc"
:modelValue="store.isEnabled(pkg)"
:modelValue="prf.isPkgEnabled(pkg)"
v-on:value-change="toggle"
/>
<InstallButton :pkg="pkg" />

View File

@ -6,18 +6,17 @@ import InputText from 'primevue/inputtext';
import RadioButton from 'primevue/radiobutton';
import Toggle from 'primevue/toggleswitch';
import { invoke } from '@tauri-apps/api/core';
import * as path from '@tauri-apps/api/path';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
import { usePkgStore } from '../stores';
import { usePrfStore } from '../stores';
const prf = usePrfStore();
const store = usePkgStore();
const _cfg = <T extends string | number | boolean>(key: string, dflt: T) =>
computed({
get() {
return (store.cfg(key) as T) ?? dflt;
return (prf.cfg(key) as T) ?? dflt;
},
async set(value) {
await store.set_cfg(key, value ?? dflt);
await prf.setCfg(key, value ?? dflt);
},
});
@ -126,7 +125,7 @@ const aimeCodeModel = computed({
<InputText
class="shrink"
size="small"
:disabled="store.cfg('aime') !== true"
:disabled="prf.cfg('aime') !== true"
:maxlength="20"
placeholder="00000000000000000000"
v-model="aimeCodeModel"

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import Button from 'primevue/button';
import { usePrfStore } from '../stores';
const prf = usePrfStore();
</script>
<template>
<div class="mt-4 flex flex-wrap align-middle gap-4">
<Button
:disabled="prf.list.length > 0"
label="Create profile"
icon="pi pi-plus"
aria-label="open-executable"
class="create-button"
@click="prf.prompt"
/>
<div v-for="p in prf.list">
<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 grow'
"
@click="prf.switchTo(p.game, p.name)"
/>
</div>
</div>
</template>
<style scoped>
.create-button {
background-color: var(--p-green-400);
border-color: var(--p-green-400);
width: 10em;
}
.create-button:hover,
.create-button:active {
background-color: var(--p-green-300) !important;
border-color: var(--p-green-300) !important;
}
.ongeki-button {
background-color: var(--p-pink-400);
border-color: var(--p-pink-400);
width: 10em;
}
.ongeki-button:hover,
.ongeki-button:active {
background-color: var(--p-pink-300) !important;
border-color: var(--p-pink-300) !important;
}
.chunithm-button {
background-color: var(--p-yellow-400);
border-color: var(--p-yellow-400);
width: 10em;
}
.chunithm-button:hover,
.chunithm-button:active {
background-color: var(--p-yellow-300) !important;
border-color: var(--p-yellow-300) !important;
}
</style>

View File

@ -1,18 +1,18 @@
import { defineStore } from 'pinia';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { Package, Profile } from './types';
import { pkgKey } from './util';
import { open } from '@tauri-apps/plugin-dialog';
import { Game, Package, Profile, ProfileMeta } from './types';
import { changePrimaryColor, pkgKey } from './util';
type InstallStatus = {
pkg: string;
};
export const usePkgStore = defineStore('pkg', {
state: (): { pkg: { [key: string]: Package }; prf: Profile | null } => {
state: (): { pkg: { [key: string]: Package } } => {
return {
pkg: {},
prf: null,
};
},
getters: {
@ -22,10 +22,6 @@ export const usePkgStore = defineStore('pkg', {
all: (state) => Object.values(state),
allLocal: (state) => Object.values(state.pkg).filter((p) => p.loc),
allRemote: (state) => Object.values(state.pkg).filter((p) => p.rmt),
profile: (state) => state.prf,
isEnabled: (state) => (pkg: Package | undefined) =>
pkg !== undefined && state.prf?.mods.includes(pkgKey(pkg)),
cfg: (state) => (key: string) => state.prf?.cfg[key],
},
actions: {
setupListeners() {
@ -38,7 +34,6 @@ export const usePkgStore = defineStore('pkg', {
listen<InstallStatus>('install-end', async (ev) => {
const key = ev.payload.pkg;
await this.reload(key);
await this.reloadProfile();
this.pkg[key].js.busy = false;
});
},
@ -73,36 +68,108 @@ export const usePkgStore = defineStore('pkg', {
Object.assign(this.pkg[key], pkg);
},
async initProfile(exePath: string) {
this.prf = await invoke('init_profile', { exePath });
},
async saveProfile() {
await invoke('save_profile');
},
async reloadProfile() {
this.prf = await invoke('get_current_profile');
},
async fetch() {
await invoke('fetch_listings');
await this.reloadAll();
},
},
});
async toggle(pkg: Package | undefined, enable: boolean) {
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();
});
},
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);
}
},
async create(exePath: string) {
try {
await invoke('init_profile', { exePath });
await this.reload();
await this.reloadList();
} catch (e) {
this.prf = null;
}
if (this.prf !== null) {
const pkgs = usePkgStore();
pkgs.reloadAll();
}
},
async switchTo(game: Game, name: string) {
await invoke('load_profile', { game, name });
await this.reload();
if (this.prf !== null) {
const pkgs = usePkgStore();
pkgs.reloadAll();
}
},
async save() {
await invoke('save_current_profile');
},
async reload() {
this.prf = await invoke('get_current_profile');
if (this.prf !== null) {
changePrimaryColor(this.prf.game);
}
},
async reloadList() {
const raw = (await invoke('list_profiles')) as [Game, string][];
this.list = raw.map(([game, name]) => {
return {
game,
name,
};
});
},
async togglePkg(pkg: Package | undefined, enable: boolean) {
if (pkg === undefined) {
return;
}
await invoke('toggle_package', { key: pkgKey(pkg), enable });
await this.reloadProfile();
await this.saveProfile();
await this.reload();
await this.save();
},
async set_cfg(key: string, value: string | boolean | number) {
async setCfg(key: string, value: string | boolean | number) {
await invoke('set_cfg', { key, value });
await this.reloadProfile();
await this.saveProfile();
await this.reload();
await this.save();
},
},
});

View File

@ -19,11 +19,16 @@ export interface Package {
};
}
export interface Profile {
export type Game = 'ongeki' | 'chunithm';
export interface ProfileMeta {
game: Game;
name: string;
game: 'Ongeki' | 'Chunithm';
mods: string[];
cfg: { [key: string]: string | boolean | number };
}
export type PackageC = Map<string, Package>;
export interface Profile extends ProfileMeta {
data: {
mods: string[];
cfg: { [key: string]: string | boolean | number };
};
}

View File

@ -1,8 +1,8 @@
import { updatePrimaryPalette } from '@primevue/themes';
import { Package } from './types';
export const changePrimaryColor = (game: 'Ongeki' | 'Chunithm') => {
const color = game === 'Ongeki' ? 'pink' : 'yellow';
export const changePrimaryColor = (game: 'ongeki' | 'chunithm') => {
const color = game === 'ongeki' ? 'pink' : 'yellow';
updatePrimaryPalette({
50: `{${color}.50}`,