Files
STARTLINER/src/stores.ts
2025-04-22 21:34:55 +00:00

565 lines
16 KiB
TypeScript

import { Ref, computed, ref, watchEffect } from 'vue';
import { defineStore } from 'pinia';
import { listen } from '@tauri-apps/api/event';
import * as path from '@tauri-apps/api/path';
import { PhysicalSize, getCurrentWindow } from '@tauri-apps/api/window';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
import { Locale, setLocale as actualSetLocale } from './i18n';
import { invoke, invoke_nopopup } from './invoke';
import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types';
import {
changePrimaryColor,
hasFeature,
pkgKey,
shouldPreferDark,
} from './util';
type InstallStatus = {
pkg: string;
};
export const useGeneralStore = defineStore('general', () => {
const dirs: Ref<Dirs | null> = ref(null);
const cfgCategories = ref(new Set<string>());
const _cfgSearchTerm = ref('');
const cfgSearchTerm = computed({
set(value: string) {
cfgCategories.value.clear();
_cfgSearchTerm.value = value;
},
get() {
return _cfgSearchTerm.value;
},
});
const configDir = computed(() => {
if (dirs.value === null) {
throw new Error('Invalid directory access');
}
return dirs.value.config_dir;
});
const dataDir = computed(() => {
if (dirs.value === null) {
throw new Error('Invalid directory access');
}
return dirs.value.data_dir;
});
const cacheDir = computed(() => {
if (dirs.value === null) {
throw new Error('Invalid directory access');
}
return dirs.value.cache_dir;
});
return {
dirs,
_cfgSearchTerm,
cfgSearchTerm,
cfgCategories,
configDir,
dataDir,
cacheDir,
};
});
export const usePkgStore = defineStore('pkg', {
state: (): {
pkg: { [key: string]: Package };
networkStatus: 'connecting' | 'offline' | 'online';
showInstalled: boolean;
showDeprecated: boolean;
showNSFW: boolean;
availableCategories: Set<string>;
includeCategories: string[];
excludeCategories: string[];
} => {
return {
pkg: {},
networkStatus: 'connecting',
showInstalled: false,
showDeprecated: false,
showNSFW: false,
availableCategories: new Set(),
includeCategories: [],
excludeCategories: [],
};
},
getters: {
fromDepString: (state) => (str: string) => state.pkg[str] ?? null,
fromName: (state) => (namespace: string, name: string) =>
state.pkg[`${namespace}-${name}`] ?? null,
all: (state) => Object.values(state),
allLocal: (state) => Object.values(state.pkg).filter((p) => p.loc),
hasLocal: (state) => (key: string) =>
state.pkg.hasOwnProperty(key) && state.pkg[key].loc,
allRemote: (state) =>
Object.values(state.pkg).filter(
(p) =>
p.rmt !== null &&
(state.showInstalled || !p.loc) &&
(state.showDeprecated || !p.rmt.deprecated) &&
(state.showNSFW || !p.rmt.nsfw) &&
(state.includeCategories.length === 0 ||
p.rmt.categories.some((c) =>
state.includeCategories.includes(c)
)) &&
(state.excludeCategories.length === 0 ||
p.rmt.categories.every(
(c) => !state.excludeCategories.includes(c)
))
),
byFeature: (state) => (feature: Feature) =>
Object.values(state.pkg).filter((p) => hasFeature(p, feature)),
hasAvailableUpdates: (state) =>
Object.values(state.pkg).some(
(p) => p.loc && (p.rmt?.version ?? 0) > p.loc.version
),
},
actions: {
setupListeners() {
listen<InstallStatus>('install-start', async (ev) => {
const key = ev.payload.pkg;
await this.reload(key);
this.pkg[key].js.downloading = true;
});
listen<InstallStatus>('install-end', async (ev) => {
const key = ev.payload.pkg;
await this.reload(key);
this.pkg[key].js.downloading = false;
});
},
async reloadAll() {
await invoke('reload_all_packages');
const data = (await invoke('get_all_packages')) as {
[key: string]: Package;
};
this.availableCategories.clear();
for (const [k, v] of Object.entries(data)) {
this.reloadWith(k, v);
}
},
async reload(pkgOrKey: string | Package) {
const key =
typeof pkgOrKey === 'string' ? pkgOrKey : pkgKey(pkgOrKey);
const pkg: Package = await invoke('get_package', {
key,
});
this.reloadWith(key, pkg);
},
async reloadWith(key: string, pkg: Package) {
if (this.pkg[key] === undefined) {
this.pkg[key] = { js: { downloading: false } } as Package;
} else {
this.pkg[key].loc = null;
this.pkg[key].rmt = null;
}
Object.assign(this.pkg[key], pkg);
if (!pkg.js) {
pkg.js = { downloading: false };
}
if (pkg.rmt !== null) {
pkg.rmt.categories.forEach((c) =>
this.availableCategories.add(c)
);
pkg.js.downloading = false;
}
},
async fetch(nopopup: boolean) {
this.networkStatus = 'connecting';
try {
if (nopopup) {
await invoke_nopopup('fetch_listings');
} else {
await invoke('fetch_listings');
}
this.networkStatus = 'online';
} catch (e) {
this.networkStatus = 'offline';
return;
}
await this.reloadAll();
},
async install(pkg: Package | undefined) {
if (pkg === undefined) {
return;
}
try {
await invoke('install_package', {
key: pkgKey(pkg),
force: true,
});
} catch (err) {
if (pkg !== undefined) {
pkg.js.downloading = false;
}
}
},
async installFromKey(key: string) {
try {
await invoke('install_package', {
key,
force: true,
});
} catch (err) {
console.error(err);
}
},
async updateAll() {
const list = [];
for (const pkg of this.allLocal) {
if (pkg.rmt && pkg.rmt.version > pkg.loc!.version) {
list.push(this.install(pkg));
}
}
await Promise.all(list);
},
},
});
export const usePrfStore = defineStore('prf', () => {
const current: Ref<Profile | null> = ref(null);
const list: Ref<ProfileMeta[]> = ref([]);
let timeout: NodeJS.Timeout | null = null;
const isPkgEnabled = (pkg: Package | undefined) =>
computed(
() =>
pkg !== undefined &&
current.value !== null &&
current.value?.data.mods.includes(pkgKey(pkg))
);
const isPkgKeyEnabled = (pkg: string) =>
computed(
() =>
current.value !== null && current.value?.data.mods.includes(pkg)
);
const reload = async () => {
const p = (await invoke('get_current_profile')) as Profile;
current.value = p;
if (current.value !== null) {
changePrimaryColor(current.value.meta.game);
} else {
changePrimaryColor(null);
}
};
const create = async (game: Game) => {
try {
await invoke('init_profile', {
game,
name: 'new-profile',
});
await reload();
await reloadList();
} catch (e) {
current.value = null;
}
if (current.value !== null) {
const pkgs = usePkgStore();
pkgs.reloadAll();
}
};
const rename = async (profile: ProfileMeta, name: string) => {
await invoke('rename_profile', {
profile,
name,
});
if (
current.value?.meta.game === profile.game &&
current.value.meta.name === profile.name
) {
current.value.meta.name = name;
}
await reloadList();
};
const switchTo = async (game: Game, name: string) => {
await invoke('load_profile', { game, name });
await reload();
if (current.value !== null) {
const pkgs = usePkgStore();
pkgs.reloadAll();
}
};
const reloadList = async () => {
list.value = (await invoke('list_profiles')) as ProfileMeta[];
};
const togglePkg = async (
pkg: Package | string | undefined,
enable: boolean
) => {
if (pkg === undefined) {
return;
}
await invoke('toggle_package', {
key: typeof pkg === 'string' ? pkg : pkgKey(pkg),
enable,
});
await reload();
};
const generalStore = useGeneralStore();
const configDir = computed(async () => {
return await path.join(
generalStore.configDir,
`profile-${current.value?.meta.game}-${current.value?.meta.name}`
);
});
listen<InstallStatus>('install-end', async () => {
await reload();
});
watchEffect(async () => {
if (current.value !== null) {
await invoke('sync_current_profile', {
data: current.value.data,
});
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(() => invoke('save_current_profile'), 600);
}
});
return {
current,
list,
isPkgEnabled,
isPkgKeyEnabled,
reload,
create,
rename,
switchTo,
reloadList,
togglePkg,
configDir,
};
});
export enum ClientData {
Onboarded,
}
export const useClientStore = defineStore('client', () => {
type ScaleType = 's' | 'm' | 'l' | 'xl';
const scaleFactor: Ref<ScaleType> = ref('s');
const timeout: Ref<NodeJS.Timeout | null> = ref(null);
const offlineMode = ref(false);
const enableAutoupdates = ref(true);
const verbose = ref(false);
const theme: Ref<'light' | 'dark' | 'system'> = ref('system');
const onboarded: Ref<Game[]> = ref([]);
const locale: Ref<Locale> = ref('en');
const _scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
const scaleValue = computed(() => {
return _scaleValue(scaleFactor.value);
});
const setScaleFactor = async (value: ScaleType) => {
scaleFactor.value = value;
const window = getCurrentWindow();
const w = Math.floor(_scaleValue(value) * 900);
const h = Math.floor(_scaleValue(value) * 600);
let size = await window.innerSize();
window.setMinSize(new PhysicalSize(w, h));
window.setSize(
new PhysicalSize(Math.max(w, size.width), Math.max(h, size.height))
);
};
const scaleModel = computed({
get() {
return scaleFactor;
},
async set(value: ScaleType) {
await setScaleFactor(value);
await save();
},
});
const load = async () => {
const generalStore = useGeneralStore();
try {
const input = JSON.parse(
await readTextFile(
await path.join(
generalStore.configDir,
'client-options.json'
)
)
);
if (input.windowSize) {
getCurrentWindow().setSize(
new PhysicalSize(input.windowSize.w, input.windowSize.h)
);
}
if (input.scaleFactor) {
await setScaleFactor(input.scaleFactor);
}
if (input.theme) {
theme.value = input.theme;
}
if (input.onboarded) {
onboarded.value = input.onboarded;
}
if (input.locale) {
locale.value = input.locale;
}
await setLocale(locale.value);
await setTheme(theme.value);
} catch (e) {
console.error(`Error reading client options: ${e}`);
}
offlineMode.value = await invoke('get_global_config', {
field: 'offline_mode',
});
enableAutoupdates.value = await invoke('get_global_config', {
field: 'enable_autoupdates',
});
verbose.value = await invoke('get_global_config', {
field: 'verbose',
});
};
const save = async () => {
const generalStore = useGeneralStore();
const w = getCurrentWindow();
const size = await w.innerSize();
await writeTextFile(
await path.join(generalStore.configDir, 'client-options.json'),
JSON.stringify({
scaleFactor: scaleFactor.value,
windowSize: {
w: Math.floor(size.width),
h: Math.floor(size.height),
},
theme: theme.value,
onboarded: onboarded.value,
locale: locale.value,
})
);
};
const queueSave = async () => {
if (timeout.value !== null) {
clearTimeout(timeout.value);
}
timeout.value = setTimeout(async () => {
timeout.value = null;
await save();
}, 1000);
};
const setOfflineMode = async (value: boolean) => {
offlineMode.value = value;
await invoke('set_global_config', { field: 'offline_mode', value });
};
const setAutoupdates = async (value: boolean) => {
enableAutoupdates.value = value;
await invoke('set_global_config', {
field: 'enable_autoupdates',
value,
});
};
const setVerbose = async (value: boolean) => {
verbose.value = value;
await invoke('set_global_config', { field: 'verbose', value });
};
const setTheme = async (value: 'light' | 'dark' | 'system') => {
if (value === 'dark') {
document.documentElement.classList.add('use-dark-mode');
} else if (value === 'light') {
document.documentElement.classList.remove('use-dark-mode');
} else {
document.documentElement.classList.toggle(
'use-dark-mode',
shouldPreferDark()
);
}
theme.value = value;
await save();
};
const setOnboarded = async (game: Game) => {
onboarded.value = [...onboarded.value, game];
await save();
};
const setLocale = async (loc: Locale) => {
locale.value = loc;
await save();
await actualSetLocale(loc);
};
getCurrentWindow().onResized(async ({ payload }) => {
// For whatever reason this is 0 when minimized
if (payload.width > 0) {
await queueSave();
}
});
return {
scaleFactor,
offlineMode,
enableAutoupdates,
verbose,
theme,
onboarded,
locale,
timeout,
scaleModel,
_scaleValue,
scaleValue,
load,
save,
queueSave,
setOfflineMode,
setAutoupdates,
setVerbose,
setTheme,
setOnboarded,
setLocale,
};
});