forked from akanyan/STARTLINER
feat: ui scaling, update all
This commit is contained in:
@ -14,12 +14,13 @@ import OptionList from './OptionList.vue';
|
||||
import ProfileList from './ProfileList.vue';
|
||||
import StartButton from './StartButton.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { useGeneralStore, usePkgStore, usePrfStore } from '../stores';
|
||||
import { useClientStore, useGeneralStore, usePkgStore, usePrfStore } from '../stores';
|
||||
import { Dirs } from '../types';
|
||||
|
||||
const pkg = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
const general = useGeneralStore();
|
||||
const client = useClientStore();
|
||||
|
||||
pkg.setupListeners();
|
||||
|
||||
@ -31,6 +32,7 @@ const isProfileDisabled = computed(() => prf.current === null);
|
||||
onMounted(async () => {
|
||||
invoke('list_directories').then((d) => {
|
||||
general.dirs = d as Dirs;
|
||||
client.load();
|
||||
});
|
||||
|
||||
const fetch_promise = pkg.fetch(true);
|
||||
@ -57,17 +59,17 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<main :class="client.scaleFactor === 's' ? 'main-scale-s' : client.scaleFactor === 'm' ? 'main-scale-m' : client.scaleFactor === 'l' ? 'main-scale-l' : 'main-scale-xl'">
|
||||
<Tabs
|
||||
lazy
|
||||
:value="currentTab"
|
||||
v-on:update:value="(value) => (currentTab = value)"
|
||||
v-on:update:value="(value) => { currentTab = value; }"
|
||||
class="h-screen"
|
||||
>
|
||||
<div class="fixed w-full flex z-100">
|
||||
<TabList class="grow">
|
||||
<TabList class="grow" :show-navigators="false">
|
||||
<Tab :value="3"
|
||||
><div class="pi pi-question-circle"></div
|
||||
><div class="pi pi-users"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled" :value="0"
|
||||
><div class="pi pi-box"></div
|
||||
@ -86,6 +88,7 @@ onMounted(async () => {
|
||||
></Tab>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex" v-if="currentTab !== 3">
|
||||
<InputIcon class="self-center mr-2">
|
||||
@ -93,6 +96,7 @@ onMounted(async () => {
|
||||
</InputIcon>
|
||||
<InputText
|
||||
v-if="currentTab === 2"
|
||||
style="min-width: 0; width: 25dvw;"
|
||||
class="self-center"
|
||||
size="small"
|
||||
placeholder="Search"
|
||||
@ -100,6 +104,7 @@ onMounted(async () => {
|
||||
/>
|
||||
<InputText
|
||||
v-else
|
||||
style="min-width: 0; width: 25dvw;"
|
||||
class="self-center"
|
||||
size="small"
|
||||
placeholder="Search"
|
||||
@ -114,12 +119,20 @@ onMounted(async () => {
|
||||
:disabled="true"
|
||||
/>
|
||||
<Button
|
||||
v-if="pkg.networkStatus === 'offline'"
|
||||
v-if="pkg.networkStatus === 'offline' && !client.offlineMode"
|
||||
class="shrink self-center"
|
||||
icon="pi pi-sync"
|
||||
size="small"
|
||||
@click="pkg.fetch(false)"
|
||||
/>
|
||||
<Button
|
||||
v-if="pkg.networkStatus === 'online' && pkg.hasAvailableUpdates"
|
||||
icon="pi pi-download"
|
||||
label="UPDATE ALL"
|
||||
size="small"
|
||||
class="mr-4 m-2.5"
|
||||
@click="pkg.updateAll()"
|
||||
></Button>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<StartButton />
|
||||
@ -136,9 +149,6 @@ onMounted(async () => {
|
||||
<OptionList />
|
||||
</TabPanel>
|
||||
<TabPanel :value="3">
|
||||
<strong>UNDER CONSTRUCTION</strong><br />Some features are
|
||||
missing.<br />Existing features are expected to break
|
||||
sometimes.
|
||||
<ProfileList />
|
||||
<br /><br /><br />
|
||||
<footer>
|
||||
@ -184,4 +194,32 @@ body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-scale-s {
|
||||
zoom: 1.0
|
||||
}
|
||||
|
||||
.main-scale-m {
|
||||
zoom: 1.25
|
||||
}
|
||||
|
||||
.main-scale-l {
|
||||
zoom: 1.4
|
||||
}
|
||||
|
||||
.main-scale-xl {
|
||||
zoom: 1.7
|
||||
}
|
||||
|
||||
.p-tablist {
|
||||
background-color: #ffffff !important;
|
||||
border-bottom: white 10px !important;
|
||||
}
|
||||
|
||||
.p-tab {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
.p-tablist-active-bar {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,33 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore } from '../stores';
|
||||
import { Package } from '../types';
|
||||
import { pkgKey } from '../util';
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
|
||||
const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
});
|
||||
|
||||
const install = async () => {
|
||||
if (props.pkg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('install_package', {
|
||||
key: pkgKey(props.pkg),
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (props.pkg !== undefined) {
|
||||
props.pkg.js.busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
//if (rv === 'Deferred') { /* download progress */ }
|
||||
};
|
||||
|
||||
const remove = async () => {
|
||||
if (props.pkg === undefined) {
|
||||
return;
|
||||
@ -41,7 +24,7 @@ const remove = async () => {
|
||||
|
||||
<template>
|
||||
<Button
|
||||
v-if="pkg?.loc"
|
||||
v-if="pkg?.loc && !pkg?.js.busy"
|
||||
rounded
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
@ -63,6 +46,6 @@ const remove = async () => {
|
||||
class="self-center ml-4"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:loading="pkg?.js.busy"
|
||||
v-on:click="install()"
|
||||
v-on:click="async () => await pkgs.install(pkg)"
|
||||
/>
|
||||
</template>
|
||||
|
@ -10,6 +10,7 @@ import DisplayOptions from './options/Display.vue';
|
||||
import MiscOptions from './options/Misc.vue';
|
||||
import NetworkOptions from './options/Network.vue';
|
||||
import SegatoolsOptions from './options/Segatools.vue';
|
||||
import StartlinerOptions from './options/Startliner.vue';
|
||||
import { usePrfStore } from '../stores';
|
||||
|
||||
const prf = usePrfStore();
|
||||
@ -128,6 +129,7 @@ prf.reload();
|
||||
v-model="blacklistMaxModel"
|
||||
/></OptionRow> -->
|
||||
</OptionCategory>
|
||||
<StartlinerOptions />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
@ -74,6 +74,8 @@ const deleteProfile = async () => {
|
||||
<div v-if="!isEditing">{{ p!.name }}</div>
|
||||
<div v-else>
|
||||
<InputText
|
||||
unstyled
|
||||
class="text-center"
|
||||
:model-value="p!.name"
|
||||
@vue:mounted="$event?.el?.focus()"
|
||||
@keyup="renameProfile"
|
||||
@ -127,3 +129,11 @@ const deleteProfile = async () => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
.p-tablist-tab-list {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -32,7 +32,7 @@ const install = async () => {
|
||||
<Button
|
||||
v-if="needsUpdate(pkg) && !pkg?.js.busy"
|
||||
rounded
|
||||
icon="pi pi-sync"
|
||||
icon="pi pi-download"
|
||||
severity="success"
|
||||
aria-label="remove"
|
||||
size="small"
|
||||
|
@ -35,7 +35,10 @@ const names = computed(() => {
|
||||
|
||||
<template>
|
||||
<OptionCategory title="General">
|
||||
<OptionRow :title="names.exe">
|
||||
<OptionRow
|
||||
:title="names.exe"
|
||||
tooltip="STARTLINER expects unpacked executables put into otherwise clean data."
|
||||
>
|
||||
<FilePicker
|
||||
:directory="false"
|
||||
:promptname="names.exe"
|
||||
|
41
src/components/options/Startliner.vue
Normal file
41
src/components/options/Startliner.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<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';
|
||||
|
||||
const client = useClientStore();
|
||||
|
||||
const offlineModel = computed({
|
||||
get() {
|
||||
return client.offlineMode;
|
||||
},
|
||||
async set(value: boolean) {
|
||||
await client.setOfflineMode(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>
|
||||
<OptionRow title="Offline mode" tooltip="Applies after a restart">
|
||||
<ToggleSwitch v-model="offlineModel" />
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
149
src/stores.ts
149
src/stores.ts
@ -2,6 +2,8 @@ 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 } from './util';
|
||||
@ -102,6 +104,10 @@ export const usePkgStore = defineStore('pkg', {
|
||||
),
|
||||
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() {
|
||||
@ -170,6 +176,36 @@ export const usePkgStore = defineStore('pkg', {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//if (rv === 'Deferred') { /* download progress */ }
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -295,3 +331,116 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
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(true);
|
||||
|
||||
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) * 760);
|
||||
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);
|
||||
}
|
||||
|
||||
offlineMode.value = await invoke('is_offline');
|
||||
} catch (e) {
|
||||
console.error(`Error reading client options: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
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),
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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_offline', { value });
|
||||
};
|
||||
|
||||
getCurrentWindow().onResized(async ({ payload }) => {
|
||||
// For whatever reason this is 0 when minimized
|
||||
if (payload.width > 0) {
|
||||
await queueSave();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
scaleFactor,
|
||||
offlineMode,
|
||||
timeout,
|
||||
scaleModel,
|
||||
load,
|
||||
save,
|
||||
queueSave,
|
||||
setOfflineMode,
|
||||
};
|
||||
});
|
||||
|
Reference in New Issue
Block a user