522 lines
15 KiB
TypeScript
522 lines
15 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 { 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.busy = true;
|
|
});
|
|
|
|
listen<InstallStatus>('install-end', async (ev) => {
|
|
const key = ev.payload.pkg;
|
|
await this.reload(key);
|
|
this.pkg[key].js.busy = 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: { busy: false } } as Package;
|
|
} else {
|
|
this.pkg[key].loc = null;
|
|
this.pkg[key].rmt = null;
|
|
}
|
|
Object.assign(this.pkg[key], pkg);
|
|
|
|
if (pkg.rmt !== null) {
|
|
pkg.rmt.categories.forEach((c) =>
|
|
this.availableCategories.add(c)
|
|
);
|
|
}
|
|
},
|
|
|
|
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) {
|
|
console.error(err);
|
|
if (pkg !== undefined) {
|
|
pkg.js.busy = 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 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 scaleValue = (value: ScaleType) =>
|
|
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
|
|
|
|
const setScaleFactor = async (value: ScaleType) => {
|
|
scaleFactor.value = value;
|
|
|
|
const window = getCurrentWindow();
|
|
const w = Math.floor(scaleValue(value) * 900);
|
|
const h = Math.floor(scaleValue(value) * 480);
|
|
|
|
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;
|
|
}
|
|
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,
|
|
})
|
|
);
|
|
};
|
|
|
|
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();
|
|
};
|
|
|
|
getCurrentWindow().onResized(async ({ payload }) => {
|
|
// For whatever reason this is 0 when minimized
|
|
if (payload.width > 0) {
|
|
await queueSave();
|
|
}
|
|
});
|
|
|
|
return {
|
|
scaleFactor,
|
|
offlineMode,
|
|
enableAutoupdates,
|
|
verbose,
|
|
theme,
|
|
timeout,
|
|
scaleModel,
|
|
load,
|
|
save,
|
|
queueSave,
|
|
setOfflineMode,
|
|
setAutoupdates,
|
|
setVerbose,
|
|
setTheme,
|
|
};
|
|
});
|