Async downloader, better Proton support (#183)

* feat: async downloader + other fixes

* feat: windows command parsing + use library path for install path

* feat: better proton support

* feat: style fixes and store button now uses in-app

* feat: emulator rename + umu emulator fix

* feat: bring process creation inline with docs

* fix: clippy
This commit is contained in:
DecDuck
2026-02-06 23:24:14 +11:00
committed by GitHub
parent b71809c041
commit 01335dadaf
45 changed files with 1453 additions and 381 deletions
+30
View File
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { StarIcon } from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core";
const props = defineProps<{
path?: string;
}>();
const model = defineModel<string | undefined>({ required: true });
const isDefault = computed(() => props.path == model.value);
async function setDefault() {
if (!props.path) return;
await invoke("set_default", { path: props.path });
model.value = props.path;
}
</script>
<template>
<button
:class="['p-0.5 rounded-full', isDefault ? 'bg-blue-500' : 'bg-zinc-800']"
@click="setDefault"
:disabled="!props.path"
>
<StarIcon
:class="['size-[0.7rem]', isDefault ? 'text-zinc-100' : 'text-zinc-100']"
/>
</button>
</template>
+195 -3
View File
@@ -11,7 +11,7 @@
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline-1 -outline-offset-1 outline-zinc-800 placeholder:text-zinc-400 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
placeholder="{}"
aria-describedby="launch-description"
v-model="model!!.launchString"
v-model="model.launchString"
/>
</div>
<p class="mt-2 text-sm text-zinc-400" id="launch-description">
@@ -21,11 +21,203 @@
>Leaving it blank will cause the game not to start.</span
>
</p>
<Listbox
v-if="props.protonEnabled"
as="div"
v-model="model.overrideProtonPath"
class="mt-6"
>
<ListboxLabel class="block text-sm/6 font-medium text-white"
>Proton override</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
class="grid w-full cursor-default grid-cols-1 rounded-md bg-white/5 py-1.5 pr-2 pl-3 text-left text-white outline-1 -outline-offset-1 outline-white/10 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-500 sm:text-sm/6"
>
<span
v-if="currentProtonPath"
class="col-start-1 row-start-1 truncate pr-6"
>{{ currentProtonPath.name }} ({{ currentProtonPath.path }})</span
>
<span
v-else
class="col-start-1 row-start-1 truncate pr-6 italic text-zinc-400"
>No override configured</span
>
<ChevronUpDownIcon
class="col-start-1 row-start-1 size-5 self-center justify-self-end text-zinc-400 sm:size-4"
aria-hidden="true"
/>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class=""
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-800 py-1 text-base outline-1 -outline-offset-1 outline-white/10 sm:text-sm"
>
<ListboxOption
as="template"
:value="undefined"
v-slot="{ active, selected }"
>
<li
:class="[
active
? 'bg-blue-500 text-white outline-hidden'
: 'text-white',
'relative cursor-default py-2 pr-9 pl-3 select-none',
]"
>
<span
:class="[
selected ? 'font-semibold' : 'font-normal',
'block truncate italic',
]"
>Use global default</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-400',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="size-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
<h1 class="text-white text-sm font-semibold bg-zinc-900 py-2 px-2">
Auto-discovered
</h1>
<ListboxOption
as="template"
v-if="protonPaths.autodiscovered.length > 0"
v-for="proton in protonPaths.autodiscovered"
:key="proton.path"
:value="proton.path"
v-slot="{ active, selected }"
>
<li
:class="[
active
? 'bg-blue-500 text-white outline-hidden'
: 'text-white',
'relative cursor-default py-2 pr-9 pl-3 select-none',
]"
>
<span
:class="[
selected ? 'font-semibold' : 'font-normal',
'block truncate',
]"
>{{ proton.name }} ({{ proton.path }})</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-400',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="size-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
<li v-else class="italic text-zinc-400 py-2 pr-9 pl-3"
>No auto-discovered layers.</li
>
<h1 class="text-white text-sm font-semibold bg-zinc-900 py-2 px-2">
Manually added
</h1>
<ListboxOption
as="template"
v-if="protonPaths.custom.length > 0"
v-for="proton in protonPaths.custom"
:key="proton.path"
:value="proton.path"
v-slot="{ active, selected }"
>
<li
:class="[
active
? 'bg-blue-500 text-white outline-hidden'
: 'text-white',
'relative cursor-default py-2 pr-9 pl-3 select-none',
]"
>
<span
:class="[
selected ? 'font-semibold' : 'font-normal',
'block truncate',
]"
>{{ proton.name }} ({{ proton.path }})</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-400',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="size-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
<li v-else class="italic text-zinc-400 py-2 pr-9 pl-3"
>No manually added layers.</li
>
</ListboxOptions>
</transition>
</div>
<p class="mt-2 text-sm text-zinc-400" id="launch-description">
Override the Proton layer used to launch this game. You can add or
remove your custom Proton layer paths in
<PageWidget to="/settings/compat">
<WrenchIcon class="size-3" />
Settings </PageWidget
>.
</p>
</Listbox>
</div>
</template>
<script setup lang="ts">
import type { FrontendGameConfiguration } from "~/composables/game";
import { invoke } from "@tauri-apps/api/core";
import type { FrontendGameConfiguration, ProtonPath } from "~/composables/game";
import {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
} from "@headlessui/vue";
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
import { CheckIcon } from "@heroicons/vue/20/solid";
import { WrenchIcon } from "@heroicons/vue/24/solid";
const model = defineModel<FrontendGameConfiguration>();
const model = defineModel<FrontendGameConfiguration>({ required: true });
const props = defineProps<{
protonEnabled: boolean;
}>();
const protonPaths = await invoke<{
autodiscovered: ProtonPath[];
custom: ProtonPath[];
default?: string;
}>("fetch_proton_paths");
const currentProtonPath = computed(
() =>
protonPaths.autodiscovered.find(
(v) => v.path == model.value.overrideProtonPath,
) ??
protonPaths.custom.find((v) => v.path == model.value.overrideProtonPath),
);
</script>
+14 -1
View File
@@ -33,6 +33,7 @@
<component
v-model="configuration"
:is="tabs[currentTabIndex]?.page"
:proton-enabled="protonEnabled"
/>
</div>
</div>
@@ -82,14 +83,24 @@ import Launch from "./GameOptions/Launch.vue";
import type { FrontendGameConfiguration } from "~/composables/game";
import { invoke } from "@tauri-apps/api/core";
const appState = useAppState();
const open = defineModel<boolean>();
const props = defineProps<{ gameId: string }>();
const game = await useGame(props.gameId);
const configuration: Ref<FrontendGameConfiguration> = ref({
launchString: game.version!!.launchCommandTemplate,
launchString: game.version!.userConfiguration.launchTemplate,
overrideProtonPath: game.version!.userConfiguration.overrideProtonPath,
});
const hasWindows = !!(
game.version!.setups.find((v) => v.platform === "Windows") ??
game.version!.launches.find((v) => v.platform === "Windows")
);
const protonEnabled = !!(appState.value!.umuState !== "NotNeeded" && hasWindows);
const tabs: Array<{ name: string; icon: Component; page: Component }> = [
{
name: "Launch",
@@ -108,12 +119,14 @@ const saveLoading = ref(false);
const saveError = ref<undefined | string>();
async function save() {
saveLoading.value = true;
saveError.value = undefined;
try {
await invoke("update_game_configuration", {
gameId: game.game.id,
options: configuration.value,
});
open.value = false;
saveError.value = undefined;
} catch (e) {
saveError.value = (e as unknown as string).toString();
}
+1
View File
@@ -28,6 +28,7 @@
/>
<div class="inline-flex items-center">
<ol class="inline-flex gap-3">
<HeaderProtonSupportWidget />
<HeaderQueueWidget :object="currentQueueObject" />
<li v-for="(item, itemIdx) in quickActions">
<HeaderWidget
@@ -0,0 +1,16 @@
<template>
<NuxtLink to="/settings/compat">
<HeaderWidget
v-if="appState && appState.umuState !== 'NotNeeded'"
:problem="notInstalled"
>
<img src="/proton-logo.png" class="relative z-50 size-5 brightness-[30%]" />
</HeaderWidget>
</NuxtLink>
</template>
<script setup lang="ts">
const appState = useAppState();
const notInstalled = appState.value?.umuState === "NotInstalled";
</script>
+2 -2
View File
@@ -18,9 +18,9 @@ const props = defineProps<{ object?: QueueState["queue"][0] }>();
]"
/>
<div
v-if="props.object?.progress"
v-if="props.object?.dl_progress"
class="transition-all absolute left-0 top-0 bottom-0 bg-blue-600 z-10"
:style="{ width: `${props.object.progress * 99 + 1}%` }"
:style="{ width: `${props.object.dl_progress * 99 + 1}%` }"
/>
</NuxtLink>
</template>
+27 -10
View File
@@ -1,17 +1,34 @@
<template>
<div class="transition inline-flex items-center cursor-pointer rounded-sm px-4 py-1.5 bg-zinc-900 text-zinc-600 hover:bg-zinc-800 hover:text-zinc-300 relative">
<slot />
<div v-if="props.notifications !== undefined"
class="text-zinc-900 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] text-xs bg-blue-300 rounded-full w-3.5 h-3.5 text-center">
{{ props.notifications }}
</div>
<div
:class="[
'transition inline-flex items-center cursor-pointer rounded-sm px-4 py-1.5 text-zinc-600 hover:text-zinc-300 relative',
props.notifications !== undefined
? 'bg-blue-400'
: props.problem !== undefined && props.problem
? 'bg-red-400'
: 'bg-zinc-900 hover:bg-zinc-800',
]"
>
<slot />
<div
v-if="props.notifications !== undefined"
class="text-zinc-900 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] text-xs bg-blue-400 rounded-full w-3.5 h-3.5 text-center"
>
{{ props.notifications }}
</div>
<div
v-else-if="props.problem !== undefined && props.problem"
class="text-zinc-100 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] text-sm bg-red-400 rounded-full w-5 h-5 text-center"
>
!
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
notifications?: number
notifications?: number;
problem?: boolean;
class?: string;
}>();
</script>
</script>
+37 -18
View File
@@ -27,7 +27,11 @@
</button>
</div>
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5 h-full">
<TransitionGroup
name="list"
tag="ul"
class="flex flex-col gap-y-1.5 h-full"
>
<Disclosure
as="div"
v-for="(nav, navIndex) in filteredNavigation"
@@ -46,7 +50,10 @@
<span class="ml-6 relative flex size-4">
<MinusIcon class="absolute inset-0 size-4" aria-hidden="true" />
<MinusIcon
:class="[ !open ? 'rotate-90' : 'rotate-0', 'transition-all absolute inset-0 size-4']"
:class="[
!open ? 'rotate-90' : 'rotate-0',
'transition-all absolute inset-0 size-4',
]"
aria-hidden="true"
/>
</span>
@@ -196,6 +203,7 @@ type FetchLibraryResponse = {
library: Game[];
collections: Collection[];
other: Game[];
missing: Game[];
};
async function calculateGamesLogic(clearAll = false, forceRefresh = false) {
@@ -215,6 +223,7 @@ async function calculateGamesLogic(clearAll = false, forceRefresh = false) {
.flat()
.map((e) => e.game),
...library.other,
...library.missing,
].filter((v, i, a) => a.indexOf(v) === i);
for (const game of allGames) {
@@ -237,11 +246,20 @@ async function calculateGamesLogic(clearAll = false, forceRefresh = false) {
entries: library.other.map((v) => ({ gameId: v.id, game: v })),
} satisfies Collection;
const missingCollection = {
id: "missing",
name: "Delisted",
isDefault: false,
isTools: true,
entries: library.missing.map((v) => ({ gameId: v.id, game: v })),
};
loading.value = false;
collections.value = [
libraryCollection,
...library.collections,
...(library.other.length > 0 ? [otherCollection] : []),
...(library.missing.length > 0 ? [missingCollection] : []),
];
}
@@ -259,24 +277,25 @@ await new Promise<void>((r) => {
const navigation = computed(() =>
collections.value.map((collection) => {
const items = collection.entries.map(({ game }) => {
const status = games[game.id].status;
const items = collection.entries
.map(({ game }) => {
const status = games[game.id].status;
const isInstalled = computed(
() => status.value.type != GameStatusEnum.Remote,
);
const isInstalled = computed(
() => status.value.type != GameStatusEnum.Remote,
);
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
icon: game.mIconObjectId,
isInstalled,
id: game.id,
type: game.type,
};
return item;
});
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
icon: game.mIconObjectId,
isInstalled,
id: game.id,
type: game.type,
};
return item;
});
return {
id: collection.id,
+15 -3
View File
@@ -10,7 +10,7 @@ const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } };
export type SerializedGameStatus = [
{ type: GameStatusEnum },
OptionGameStatus | null
OptionGameStatus | null,
];
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
@@ -70,6 +70,7 @@ export const useGame = async (gameId: string) => {
export type FrontendGameConfiguration = {
launchString: string;
overrideProtonPath?: string;
};
export type LaunchResult =
@@ -81,13 +82,24 @@ export type VersionOption = {
displayName?: string;
versionPath: string;
platform: string;
size: number;
size: {
installSize: number;
downloadSize: number;
};
requiredContent: Array<{
gameId: string;
versionId: string;
name: string;
iconObjectId: string;
shortDescription: string;
size: number;
size: {
installSize: number;
downloadSize: number;
};
}>;
};
export type ProtonPath = {
path: string;
name: string;
};
+1
View File
@@ -14,6 +14,7 @@
"@heroicons/vue": "^2.1.5",
"@nuxtjs/tailwindcss": "^6.12.2",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-os": "^2.3.2",
"@tauri-apps/plugin-shell": "^2.3.3",
"koa": "^2.16.1",
+26 -21
View File
@@ -35,15 +35,18 @@
@resume="() => resumeDownload()"
:status="status"
/>
<a
:href="remoteUrl"
target="_blank"
type="button"
<NuxtLink
class="transition-transform duration-300 hover:scale-105 active:scale-95 inline-flex items-center rounded-md bg-zinc-800/50 px-6 font-semibold text-white shadow-xl backdrop-blur-sm hover:bg-zinc-800/80 uppercase font-display"
:to="{
path: '/store',
query: {
gameId: game.id,
},
}"
>
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
Store
</a>
</NuxtLink>
</div>
</div>
@@ -171,7 +174,11 @@
</div>
<div class="space-y-6">
<div v-if="versionOptions && versionOptions.length > 0 && currentVersionOption">
<div
v-if="
versionOptions && versionOptions.length > 0 && currentVersionOption
"
>
<Listbox as="div" v-model="installVersionIndex">
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
>Version</ListboxLabel
@@ -188,7 +195,7 @@
on
{{ currentVersionOption.platform }} ({{
formatKilobytes(
currentVersionOption.size / 1024
currentVersionOption.size.installSize / 1024,
)
}}B)</span
>
@@ -233,7 +240,8 @@
>{{ version.displayName || version.versionPath }} on
{{ version.platform }} ({{
formatKilobytes(
versionOptions[installVersionIndex].size / 1024
versionOptions[installVersionIndex].size
.installSize / 1024,
)
}}B)</span
>
@@ -314,8 +322,7 @@
</div>
<ul role="list" class="mt-2 divide-y divide-white/5">
<li
v-for="content in currentVersionOption
.requiredContent"
v-for="content in currentVersionOption.requiredContent"
:key="content.versionId"
:class="[
!installDepsDisabled[content.versionId]
@@ -353,7 +360,7 @@
<p
class="inline-flex items-center gap-x-1 text-xs/5 text-gray-400"
>
{{ formatKilobytes(content.size / 1024) }}B
{{ formatKilobytes(content.size.installSize / 1024) }}B
<ServerIcon class="size-3" />
</p>
</div>
@@ -566,10 +573,6 @@ const id = route.params.id.toString();
const { game: rawGame, status } = await useGame(id);
const game = ref(rawGame);
const remoteUrl: string = await invoke("gen_drop_url", {
path: `/store/${game.value.id}`,
});
const bannerUrl = await useObject(game.value.mBannerObjectId);
// Get all available images
@@ -577,7 +580,7 @@ const mediaUrls = await Promise.all(
game.value.mImageCarouselObjectIds.map(async (v) => {
const src = await useObject(v);
return src;
})
}),
);
const htmlDescription = micromark(game.value.mDescription);
@@ -611,7 +614,9 @@ const installVersionIndex = ref(0);
const installDir = ref(0);
const installDepsDisabled = ref<{ [key: string]: boolean }>({});
const currentVersionOption = computed(() => versionOptions.value?.[installVersionIndex.value]);
const currentVersionOption = computed(
() => versionOptions.value?.[installVersionIndex.value],
);
async function install() {
try {
if (!versionOptions.value) throw new Error("Versions have not been loaded");
@@ -661,7 +666,7 @@ async function launch() {
try {
const fetchedLaunchOptions = await invoke<Array<{ name: string }>>(
"get_launch_options",
{ id: game.value.id }
{ id: game.value.id },
);
if (fetchedLaunchOptions.length == 1) {
await launchIndex(0);
@@ -676,7 +681,7 @@ async function launch() {
description: `Drop failed to launch "${game.value.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c()
(e, c) => c(),
);
console.error(e);
}
@@ -707,7 +712,7 @@ async function launchIndex(index: number) {
description: `Drop failed to launch "${game.value.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c()
(e, c) => c(),
);
}
}
@@ -731,7 +736,7 @@ async function kill() {
description: `Drop failed to stop "${game.value.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c()
(e, c) => c(),
);
console.error(e);
}
+54 -29
View File
@@ -9,18 +9,25 @@
<nav class="flex flex-col" aria-label="Sidebar">
<ul role="list" class="-mx-2 space-y-1">
<li v-for="(item, itemIdx) in navigation" :key="item.prefix">
<NuxtLink :href="item.route" :class="[
itemIdx === currentNavigation
? 'bg-zinc-800/50 text-zinc-100'
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
]">
<component :is="item.icon" :class="[
<NuxtLink
:href="item.route"
:class="[
itemIdx === currentNavigation
? 'text-zinc-100'
: 'text-zinc-400 group-hover:text-zinc-200',
'transition h-6 w-6 shrink-0',
]" aria-hidden="true" />
? 'bg-zinc-800/50 text-zinc-100'
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
]"
>
<component
:is="item.icon"
:class="[
itemIdx === currentNavigation
? 'text-zinc-100'
: 'text-zinc-400 group-hover:text-zinc-200',
'transition h-6 w-6 shrink-0',
]"
aria-hidden="true"
/>
{{ item.label }}
</NuxtLink>
</li>
@@ -43,7 +50,7 @@ import {
} from "@heroicons/vue/16/solid";
import type { Component } from "vue";
import type { NavigationItem } from "~/types";
import { platform } from '@tauri-apps/plugin-os';
import { platform } from "@tauri-apps/plugin-os";
import { invoke } from "@tauri-apps/api/core";
import { UserIcon } from "@heroicons/vue/20/solid";
@@ -57,25 +64,28 @@ const systemData = await invoke<{
const isDebugMode = ref(systemData.logLevel.toLowerCase() === "debug");
const debugRevealed = ref(false);
const appState = useAppState();
// Track shift key state and debug reveal
onMounted(() => {
window.addEventListener('keydown', (e) => {
if (e.key === 'Shift') {
window.addEventListener("keydown", (e) => {
if (e.key === "Shift") {
isDebugMode.value = true;
debugRevealed.value = true;
}
});
window.addEventListener('keyup', (e) => {
if (e.key === 'Shift') {
isDebugMode.value = debugRevealed.value || systemData.logLevel.toLowerCase() === "debug";
window.addEventListener("keyup", (e) => {
if (e.key === "Shift") {
isDebugMode.value =
debugRevealed.value || systemData.logLevel.toLowerCase() === "debug";
}
});
// Reset debug reveal when leaving the settings page
const router = useRouter();
router.beforeEach((to) => {
if (!to.path.startsWith('/settings')) {
if (!to.path.startsWith("/settings")) {
debugRevealed.value = false;
isDebugMode.value = systemData.logLevel.toLowerCase() === "debug";
}
@@ -91,7 +101,7 @@ const navigation = computed(() => [
icon: HomeIcon,
},
{
label: "Interface",
label: "Interface",
route: "/settings/interface",
prefix: "/settings/interface",
icon: RectangleGroupIcon,
@@ -102,27 +112,42 @@ const navigation = computed(() => [
prefix: "/settings/downloads",
icon: ArrowDownTrayIcon,
},
...(appState.value!.umuState !== "NotNeeded"
? [
{
label: "Proton",
route: "/settings/compat",
prefix: "/settings/compat",
icon: h("img", { src: "/proton-logo.png" }),
},
]
: []),
{
label: "Account",
route: "/settings/account",
prefix: "/settings/account",
icon: UserIcon
icon: UserIcon,
},
...(isDebugMode.value ? [{
label: "Debug Info",
route: "/settings/debug",
prefix: "/settings/debug",
icon: BugAntIcon,
}] : []),
...(isDebugMode.value
? [
{
label: "Debug Info",
route: "/settings/debug",
prefix: "/settings/debug",
icon: BugAntIcon,
},
]
: []),
]);
const currentPlatform = platform();
// Use .value to unwrap the computed ref
const {currentNavigation} = useCurrentNavigationIndex(navigation.value);
const { currentNavigation } = useCurrentNavigationIndex(navigation.value);
// Watch for navigation changes and update currentPageIndex
watch(navigation, (newNav) => {
currentNavigation.value = useCurrentNavigationIndex(newNav).currentNavigation.value;
currentNavigation.value =
useCurrentNavigationIndex(newNav).currentNavigation.value;
});
</script>
+340
View File
@@ -0,0 +1,340 @@
<template>
<div class="border-b border-zinc-700 py-5">
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
Proton Compatibility Layer
</h3>
</div>
<div
v-if="appState!.umuState === 'Installed'"
class="rounded-md bg-green-500/10 p-4 outline outline-green-500/20"
>
<div class="flex">
<div class="shrink-0">
<CheckCircleIcon class="size-5 text-green-400" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-green-200">
UMU Launcher installed
</h3>
<div class="mt-2 text-sm text-green-200/85">
<p>
The necessary component to use the Proton Compatibility Layer is
installed, and detected.
</p>
</div>
</div>
</div>
</div>
<div v-else class="rounded-md bg-red-500/15 p-4 outline outline-red-500/25">
<div class="flex">
<div class="shrink-0">
<XCircleIcon class="size-5 text-red-400" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-200">
UMU Launcher not installed
</h3>
<div class="mt-2 text-sm text-red-200/80">
<p>
You will be unable to install or run games designed for Windows
until you install UMU Launcher and restart Drop.
</p>
</div>
</div>
</div>
</div>
<div v-if="!paths.data.value?.default" class="mt-4 rounded-md bg-yellow-500/15 p-4 outline outline-yellow-500/25">
<div class="flex">
<div class="shrink-0">
<ExclamationTriangleIcon class="size-5 text-yellow-400" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-200">
No default Proton layer
</h3>
<div class="mt-2 text-sm text-yellow-200/80">
<p>
You won't be able to launch any Windows games without overriding their Proton layer in game settings. Please select a default layer below using the stars.
</p>
</div>
</div>
</div>
</div>
<div
v-if="paths.data.value"
class="mt-4 text-zinc-100 gap-x-2 inline-flex p-4 w-full items-center justify-center font-bold"
>
<DefaultProtonButton v-model="paths.data.value.default" />
= Default Proton Layer
</div>
<!-- autodiscovered table -->
<div class="mt-2">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-white">
Auto-discovered Proton Layers
</h1>
<p class="mt-2 text-sm text-gray-300">
All auto-discovered Proton Layers from common paths on your system.
</p>
</div>
</div>
<div class="mt-2 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="relative min-w-full divide-y divide-white/15">
<thead>
<tr>
<th
scope="col"
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-white sm:pl-0"
>
Name
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-white"
>
Path
</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10">
<tr
v-for="path in paths.data.value?.autodiscovered"
:key="path.path"
>
<td
class="py-4 pr-3 pl-4 text-sm font-medium inline-flex items-center gap-x-2 whitespace-nowrap text-white sm:pl-0"
>
<DefaultProtonButton
:path="path.path"
v-model="paths.data.value!.default"
/>
{{ path.name }}
</td>
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
{{ path.path }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- custom table -->
<div class="mt-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-white">Manual Proton Layers</h1>
<p class="mt-2 text-sm text-gray-300">
Add or remove custom Proton compatible layers for your games. We
recommend
<a
href="https://github.com/DavidoTek/ProtonUp-Qt"
target="_blank"
class="text-blue-400 hover:text-blue-500"
>ProtonUp-Qt</a
>
to download and manage your proton layers.
</p>
<p class="mt-2 text-sm text-gray-300">
Note: deleting a custom Proton layer will
<span class="font-bold">not</span> clear it from manually selected
Proton layers.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
@click="pickLayerModal = true"
type="button"
class="block rounded-md bg-blue-500 px-3 py-2 text-center text-sm font-semibold text-white shadow-xs hover:bg-blue-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Add layer
</button>
</div>
</div>
<div class="mt-2 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="relative min-w-full divide-y divide-white/15">
<thead>
<tr>
<th
scope="col"
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-white sm:pl-0"
>
Name
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-white"
>
Path
</th>
<th scope="col" class="py-3.5 pr-4 pl-3 sm:pr-0">
<span class="sr-only">Delete</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10">
<tr
v-for="(path, pathIdx) in paths.data.value?.custom"
:key="path.path"
>
<td
class="py-4 pr-3 pl-4 text-sm font-medium inline-flex items-center gap-x-2 whitespace-nowrap text-white sm:pl-0"
>
<DefaultProtonButton
:path="path.path"
v-model="paths.data.value!.default"
/>
{{ path.name }}
</td>
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
{{ path.path }}
</td>
<td
class="py-4 pr-4 pl-3 text-right text-sm font-medium whitespace-nowrap sm:pr-0"
>
<button
@click="() => deleteCustom(pathIdx)"
class="text-red-400 hover:text-red-300"
>
Delete<span class="sr-only"></span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<ModalTemplate v-model="pickLayerModal">
<template #default>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left">
<h3 class="text-base font-semibold text-zinc-100">
Select your Proton layer
</h3>
<div class="mt-2">
<p class="text-sm text-zinc-400">
Select the path to your Proton layer. It should have at least two
files, one named "proton" and one named "compatibilitytool.vdf",
for Drop to recognise it.
</p>
</div>
</div>
</div>
<div class="p-3 bg-zinc-950 ring-2 ring-zinc-800 rounded-lg text-sm">
<span v-if="path" class="text-zinc-100">{{ path }}</span>
<span v-else class="italic text-zinc-400">No path selected.</span>
</div>
<LoadingButton :loading="false" @click="pickLayer"
>Select path</LoadingButton
>
<div
v-if="pickError"
class="rounded-md bg-red-500/15 p-4 outline outline-red-500/25"
>
<div class="flex">
<div class="shrink-0">
<XCircleIcon class="size-5 text-red-400" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-200">
{{ pickError }}
</h3>
</div>
</div>
</div>
</template>
<template #buttons>
<LoadingButton
@click="() => add()"
:loading="false"
:disabled="!path"
type="submit"
class="ml-2 w-full sm:w-fit"
>
Add
</LoadingButton>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="cancel"
ref="cancelButtonRef"
>
Cancel
</button>
</template>
</ModalTemplate>
</template>
<script setup lang="ts">
import { CheckCircleIcon, XCircleIcon } from "@heroicons/vue/16/solid";
import { ExclamationTriangleIcon } from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
const appState = useAppState();
const paths = useAsyncData<{
autodiscovered: ProtonPath[];
custom: ProtonPath[];
default?: string;
}>("proton_paths", async () => await invoke("fetch_proton_paths"));
const pickLayerModal = ref(false);
const pickError = ref<string | null>(null);
const path = ref<string | null>(null);
async function pickLayer() {
const file = await open({
multiple: false,
directory: true,
canCreateDirectories: true,
});
path.value = file;
pickError.value = null;
}
async function add() {
if (!path.value) return;
pickError.value = null;
try {
await invoke("add_proton_layer", { path: path.value });
path.value = null;
pickLayerModal.value = false;
paths.refresh();
} catch (e) {
pickError.value = (e as string).toString();
}
}
function cancel() {
pickLayerModal.value = false;
path.value = null;
}
async function deleteCustom(index: number) {
if (!paths.data.value) return;
await invoke("remove_proton_layer", { index });
const deleted = paths.data.value.custom.splice(index);
if (paths.data.value.default == deleted[0].path) {
paths.data.value.default = undefined;
}
}
</script>
+10 -6
View File
@@ -2,11 +2,15 @@
<iframe :src="convertedStoreUrl" class="grow w-full h-full" />
</template>
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
BuildingStorefrontIcon,
} from "@heroicons/vue/20/solid";
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { convertFileSrc } from "@tauri-apps/api/core";
const convertedStoreUrl = convertFileSrc("store", "server");
const route = useRoute();
const gameId = route.query.gameId?.toString();
// This is necessary because convertFileSrc encodes the URI
const convertedStoreUrl = convertFileSrc(`dummyvalue`, "server").replace(
"dummyvalue",
gameId ? `store/${gameId}` : "store",
);
</script>
+53
View File
@@ -20,6 +20,9 @@ importers:
'@tauri-apps/api':
specifier: ^2.9.1
version: 2.9.1
'@tauri-apps/plugin-dialog':
specifier: ^2.6.0
version: 2.6.0
'@tauri-apps/plugin-os':
specifier: ^2.3.2
version: 2.3.2
@@ -565,36 +568,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-minify/binding-linux-arm64-musl@0.96.0':
resolution: {integrity: sha512-rNqoFWOWaxwMmUY5fspd/h5HfvgUlA3sv9CUdA2MpnHFiyoJNovR7WU8tGh+Yn0qOAs0SNH0a05gIthHig14IA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-minify/binding-linux-riscv64-gnu@0.96.0':
resolution: {integrity: sha512-3paajIuzGnukHwSI3YBjYVqbd72pZd8NJxaayaNFR0AByIm8rmIT5RqFXbq8j2uhtpmNdZRXiu0em1zOmIScWA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxc-minify/binding-linux-s390x-gnu@0.96.0':
resolution: {integrity: sha512-9ESrpkB2XG0lQ89JlsxlZa86iQCOs+jkDZLl6O+u5wb7ynUy21bpJJ1joauCOSYIOUlSy3+LbtJLiqi7oSQt5Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxc-minify/binding-linux-x64-gnu@0.96.0':
resolution: {integrity: sha512-UMM1jkns+p+WwwmdjC5giI3SfR2BCTga18x3C0cAu6vDVf4W37uTZeTtSIGmwatTBbgiq++Te24/DE0oCdm1iQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-minify/binding-linux-x64-musl@0.96.0':
resolution: {integrity: sha512-8b1naiC7MdP7xeMi7cQ5tb9W1rZAP9Qz/jBRqp1Y5EOZ1yhSGnf1QWuZ/0pCc+XiB9vEHXEY3Aki/H+86m2eOg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-minify/binding-wasm32-wasi@0.96.0':
resolution: {integrity: sha512-bjGDjkGzo3GWU9Vg2qiFUrfoo5QxojPNV/2RHTlbIB5FWkkV4ExVjsfyqihFiAuj0NXIZqd2SAiEq9htVd3RFw==}
@@ -654,36 +663,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-arm64-musl@0.96.0':
resolution: {integrity: sha512-fjDPbZjkqaDSTBe0FM8nZ9zBw4B/NF/I0gH7CfvNDwIj9smISaNFypYeomkvubORpnbX9ORhvhYwg3TxQ60OGA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-parser/binding-linux-riscv64-gnu@0.96.0':
resolution: {integrity: sha512-59KAHd/6/LmjkdSAuJn0piKmwSavMasWNUKuYLX/UnqI5KkGIp14+LBwwaBG6KzOtIq1NrRCnmlL4XSEaNkzTg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-s390x-gnu@0.96.0':
resolution: {integrity: sha512-VtupojtgahY8XmLwpVpM3C1WQEgMD1JxpB8lzUtdSLwosWaaz1EAl+VXWNuxTTZusNuLBtmR+F0qql22ISi/9g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-x64-gnu@0.96.0':
resolution: {integrity: sha512-8XSY9aUYY+5I4I1mhSEWmYqdUrJi3J5cCAInvEVHyTnDAPkhb+tnLGVZD696TpW+lFOLrTFF2V5GMWJVafqIUA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-x64-musl@0.96.0':
resolution: {integrity: sha512-IIVNtqhA0uxKkD8Y6aZinKO/sOD5O62VlduE54FnUU2rzZEszrZQLL8nMGVZhTdPaKW5M1aeLmjcdnOs6er1Jg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-parser/binding-wasm32-wasi@0.96.0':
resolution: {integrity: sha512-TJ/sNPbVD4u6kUwm7sDKa5iRDEB8vd7ZIMjYqFrrAo9US1RGYOSvt6Ie9sDRekUL9fZhNsykvSrpmIj6dg/C2w==}
@@ -746,36 +761,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-transform/binding-linux-arm64-musl@0.96.0':
resolution: {integrity: sha512-EiG/L3wEkPgTm4p906ufptyblBgtiQWTubGg/JEw82f8uLRroayr5zhbUqx40EgH037a3SfJthIyLZi7XPRFJw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-transform/binding-linux-riscv64-gnu@0.96.0':
resolution: {integrity: sha512-r01CY6OxKGtVeYnvH4mGmtkQMlLkXdPWWNXwo5o7fE2s/fgZPMpqh8bAuXEhuMXipZRJrjxTk1+ZQ4KCHpMn3Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxc-transform/binding-linux-s390x-gnu@0.96.0':
resolution: {integrity: sha512-4djg2vYLGbVeS8YiA2K4RPPpZE4fxTGCX5g/bOMbCYyirDbmBAIop4eOAj8vOA9i1CcWbDtmp+PVJ1dSw7f3IQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxc-transform/binding-linux-x64-gnu@0.96.0':
resolution: {integrity: sha512-f6pcWVz57Y8jXa2OS7cz3aRNuks34Q3j61+3nQ4xTE8H1KbalcEvHNmM92OEddaJ8QLs9YcE0kUC6eDTbY34+A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-transform/binding-linux-x64-musl@0.96.0':
resolution: {integrity: sha512-NSiRtFvR7Pbhv3mWyPMkTK38czIjcnK0+K5STo3CuzZRVbX1TM17zGdHzKBUHZu7v6IQ6/XsQ3ELa1BlEHPGWQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-transform/binding-wasm32-wasi@0.96.0':
resolution: {integrity: sha512-A91ARLiuZHGN4hBds9s7bW3czUuLuHLsV+cz44iF9j8e1zX9m2hNGXf/acQRbg/zcFUXmjz5nmk8EkZyob876w==}
@@ -823,36 +844,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-wasm@2.5.1':
resolution: {integrity: sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw==}
@@ -1010,56 +1037,67 @@ packages:
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.53.3':
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.53.3':
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.53.3':
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.53.3':
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.53.3':
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.53.3':
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.53.3':
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.53.3':
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
@@ -1118,6 +1156,9 @@ packages:
'@tauri-apps/api@2.9.1':
resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==}
'@tauri-apps/plugin-dialog@2.6.0':
resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
'@tauri-apps/plugin-os@2.3.2':
resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==}
@@ -3223,48 +3264,56 @@ packages:
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
libc: glibc
sass-embedded-linux-arm@1.93.3:
resolution: {integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
libc: glibc
sass-embedded-linux-musl-arm64@1.93.3:
resolution: {integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
libc: musl
sass-embedded-linux-musl-arm@1.93.3:
resolution: {integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
libc: musl
sass-embedded-linux-musl-riscv64@1.93.3:
resolution: {integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
libc: musl
sass-embedded-linux-musl-x64@1.93.3:
resolution: {integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
libc: musl
sass-embedded-linux-riscv64@1.93.3:
resolution: {integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
libc: glibc
sass-embedded-linux-x64@1.93.3:
resolution: {integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
libc: glibc
sass-embedded-unknown-all@1.93.3:
resolution: {integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==}
@@ -5051,6 +5100,10 @@ snapshots:
'@tauri-apps/api@2.9.1': {}
'@tauri-apps/plugin-dialog@2.6.0':
dependencies:
'@tauri-apps/api': 2.9.1
'@tauri-apps/plugin-os@2.3.2':
dependencies:
'@tauri-apps/api': 2.9.1
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+1
View File
@@ -7,6 +7,7 @@ export default {
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
"../libs/drop-base/**/*.{js,vue,ts}"
],
theme: {
extend: {
+9 -1
View File
@@ -20,8 +20,11 @@ export type User = {
profilePictureObjectId: string;
};
type UmuState = "Installed" | "NotInstalled" | "NotNeeded";
export type AppState = {
status: AppStatus;
umuState: UmuState;
user?: User;
};
@@ -47,7 +50,12 @@ export type Collection = {
};
export type GameVersion = {
launchCommandTemplate: string;
userConfiguration: {
launchTemplate: string;
overrideProtonPath: string;
};
setups: Array<{ platform: string }>;
launches: Array<{ platform: string }>;
};
export enum AppStatus {