forked from akanyan/STARTLINER
feat: groundwork for multi-profile support
This commit is contained in:
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
@ -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"
|
||||
|
73
src/components/ProfileList.vue
Normal file
73
src/components/ProfileList.vue
Normal 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>
|
121
src/stores.ts
121
src/stores.ts
@ -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();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
15
src/types.ts
15
src/types.ts
@ -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 };
|
||||
};
|
||||
}
|
||||
|
@ -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}`,
|
||||
|
Reference in New Issue
Block a user