Game updates (#187)

* refactor: split umu launcher

* feat: latest version picker + fixes

* feat: frontend latest changes

* feat: game update detection w/ setting

* feat: fixes and refactor for game update

* fix: windows ui

* fix: deps

* feat: update modifications

* feat: missing ui and lock update

* fix: create install dir on init

* fix: clippy

* fix: clippy x2

* feat: add configuration option to toggle updates

* feat: uninstall dropdown on partiallyinstalled
This commit is contained in:
DecDuck
2026-02-25 23:27:30 +11:00
committed by GitHub
parent fe9a88b9b2
commit 4728ea177c
38 changed files with 1193 additions and 573 deletions
+10 -9
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.launchTemplate"
/>
</div>
<p class="mt-2 text-sm text-zinc-400" id="launch-description">
@@ -129,9 +129,9 @@
</span>
</li>
</ListboxOption>
<li v-else class="italic text-zinc-400 py-2 pr-9 pl-3"
>No auto-discovered layers.</li
>
<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>
@@ -170,9 +170,9 @@
</span>
</li>
</ListboxOption>
<li v-else class="italic text-zinc-400 py-2 pr-9 pl-3"
>No manually added layers.</li
>
<li v-else class="italic text-zinc-400 py-2 pr-9 pl-3">
No manually added layers.
</li>
</ListboxOptions>
</transition>
</div>
@@ -190,7 +190,7 @@
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import type { FrontendGameConfiguration, ProtonPath } from "~/composables/game";
import type { ProtonPath } from "~/composables/game";
import {
Listbox,
ListboxButton,
@@ -201,8 +201,9 @@ import {
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
import { CheckIcon } from "@heroicons/vue/20/solid";
import { WrenchIcon } from "@heroicons/vue/24/solid";
import type { GameVersion } from "~/types";
const model = defineModel<FrontendGameConfiguration>({ required: true });
const model = defineModel<GameVersion["userConfiguration"]>({ required: true });
const props = defineProps<{
protonEnabled: boolean;
+36
View File
@@ -0,0 +1,36 @@
<template>
<div class="space-y-8">
<div class="flex flex-row items-center justify-between">
<div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">
Enable update checks
</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Drop will automatically check for updates from your server
</p>
</div>
<Switch
v-model="model.enableUpdates"
:class="[
model.enableUpdates ? 'bg-blue-600' : 'bg-zinc-700',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out',
]"
>
<span
:class="[
model.enableUpdates ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
]"
/>
</Switch>
</div>
</div>
</template>
<script setup lang="ts">
import { Switch } from "@headlessui/vue";
import type { GameVersion } from '~/types';
const model = defineModel<GameVersion["userConfiguration"]>({ required: true });
</script>
+16 -10
View File
@@ -1,7 +1,7 @@
<template>
<ModalTemplate size-class="max-w-4xl" v-model="open">
<template #default>
<div class="flex flex-row gap-x-4">
<div class="flex flex-row gap-x-4 h-96">
<nav class="flex flex-1 flex-col" aria-label="Sidebar">
<ul role="list" class="-mx-2 space-y-1">
<li v-for="(tab, tabIdx) in tabs" :key="tab.name">
@@ -29,7 +29,7 @@
</li>
</ul>
</nav>
<div class="border-l-2 border-zinc-800 w-full grow pl-4">
<div class="border-l-2 border-zinc-800 w-full grow pl-4 overflow-y-scroll">
<component
v-model="configuration"
:is="tabs[currentTabIndex]?.page"
@@ -80,8 +80,10 @@ import {
XCircleIcon,
} from "@heroicons/vue/20/solid";
import Launch from "./GameOptions/Launch.vue";
import type { FrontendGameConfiguration } from "~/composables/game";
import Updates from "./GameOptions/Updates.vue";
import { invoke } from "@tauri-apps/api/core";
import { ArrowPathIcon } from "@heroicons/vue/24/solid";
import type { GameVersion } from "~/types";
const appState = useAppState();
@@ -89,17 +91,16 @@ const open = defineModel<boolean>();
const props = defineProps<{ gameId: string }>();
const game = await useGame(props.gameId);
const configuration: Ref<FrontendGameConfiguration> = ref({
launchString: game.version!.userConfiguration.launchTemplate,
overrideProtonPath: game.version!.userConfiguration.overrideProtonPath,
});
const configuration: Ref<GameVersion["userConfiguration"]> = ref(game.version.value!.userConfiguration);
const hasWindows = !!(
game.version!.setups.find((v) => v.platform === "Windows") ??
game.version!.launches.find((v) => v.platform === "Windows")
game.version.value!.setups.find((v) => v.platform === "Windows") ??
game.version.value!.launches.find((v) => v.platform === "Windows")
);
const protonEnabled = !!(appState.value!.umuState !== "NotNeeded" && hasWindows);
const protonEnabled = !!(
appState.value!.umuState !== "NotNeeded" && hasWindows
);
const tabs: Array<{ name: string; icon: Component; page: Component }> = [
{
@@ -107,6 +108,11 @@ const tabs: Array<{ name: string; icon: Component; page: Component }> = [
icon: RocketLaunchIcon,
page: Launch,
},
{
name: "Updates",
icon: ArrowPathIcon,
page: Updates,
},
{
name: "Storage",
icon: ServerIcon,
+111 -62
View File
@@ -3,19 +3,19 @@
<div class="inline-flex divide-x divide-zinc-900">
<button
type="button"
@click="() => buttonActions[props.status.type]()"
@click="() => fetchStatusStyleData($props.status).action()"
:class="[
styles[props.status.type],
fetchStatusStyleData($props.status).style,
showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<component
:is="buttonIcons[props.status.type]"
:is="fetchStatusStyleData($props.status).icon"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
{{ buttonNames[props.status.type] }}
{{ fetchStatusStyleData($props.status).buttonName }}
</button>
<Menu
v-if="showDropdown"
@@ -25,7 +25,7 @@
<div class="h-full">
<MenuButton
:class="[
styles[props.status.type],
fetchStatusStyleData($props.status).style,
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
@@ -46,6 +46,21 @@
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none"
>
<div class="py-1">
<MenuItem v-slot="{ active }">
<button
@click="() => emit('install')"
:class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full px-4 py-2 text-sm inline-flex justify-between',
]"
>
Install
<ArrowDownTrayIcon class="size-5" />
</button>
</MenuItem>
<MenuItem v-if="showOptions" v-slot="{ active }">
<button
@click="() => emit('options')"
@@ -53,7 +68,7 @@
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
'w-full px-4 py-2 text-sm inline-flex justify-between',
]"
>
Options
@@ -67,7 +82,7 @@
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
'w-full inline-flex px-4 py-2 text-sm justify-between',
]"
>
Uninstall
@@ -93,9 +108,14 @@ import {
} from "@heroicons/vue/20/solid";
import type { Component } from "vue";
import { GameStatusEnum, type GameStatus } from "~/types.js";
import {
type EmptyGameStatusEnum,
InstalledType,
type GameStatus,
} from "~/types.js";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import { Cog6ToothIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { ArrowsRightLeftIcon, ArrowUpTrayIcon } from "@heroicons/vue/24/solid";
const props = defineProps<{ status: GameStatus }>();
const emit = defineEmits<{
@@ -108,76 +128,105 @@ const emit = defineEmits<{
(e: "resume"): void;
}>();
const showDropdown = computed(
() =>
props.status.type === GameStatusEnum.Installed ||
props.status.type === GameStatusEnum.SetupRequired ||
props.status.type === GameStatusEnum.PartiallyInstalled
);
interface StatusStyleData {
style: string;
buttonName: string;
icon: Component;
action: () => void;
}
function fetchStatusStyleData(status: GameStatus): StatusStyleData {
if (status.type === "Installed") {
if (status.install_type.type === InstalledType.Installed) {
return {
style:
"bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600 hover:bg-green-500",
buttonName: "Play",
icon: PlayIcon,
action: () => emit("launch"),
};
}
if (status.install_type.type === InstalledType.SetupRequired) {
return {
style:
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
buttonName: "Setup",
icon: WrenchIcon,
action: () => emit("launch"),
};
}
if (status.install_type.type === InstalledType.PartiallyInstalled) {
return {
style:
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
buttonName: "Resume",
icon: ArrowDownTrayIcon,
action: () => emit("resume"),
};
}
throw "Non-exhaustive install type: " + JSON.stringify(status.install_type);
}
return {
style: styles[status.type],
buttonName: buttonNames[status.type],
icon: buttonIcons[status.type],
action: buttonActions[status.type],
};
}
const showDropdown = computed(() => props.status.type === "Installed");
const showOptions = computed(
() => props.status.type === GameStatusEnum.Installed
() =>
showDropdown.value &&
props.status.type === "Installed" &&
props.status.install_type.type !== InstalledType.PartiallyInstalled,
);
const styles: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]:
const styles: { [key in EmptyGameStatusEnum]: string } = {
Remote:
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
[GameStatusEnum.Queued]:
Queued:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Downloading]:
Downloading:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Validating]:
Validating:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.SetupRequired]:
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
[GameStatusEnum.Installed]:
"bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600 hover:bg-green-500",
[GameStatusEnum.Updating]:
Updating:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Uninstalling]:
Uninstalling:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Running]:
Running:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.PartiallyInstalled]:
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
};
const buttonNames: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Install",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading",
[GameStatusEnum.Validating]: "Validating",
[GameStatusEnum.SetupRequired]: "Setup",
[GameStatusEnum.Installed]: "Play",
[GameStatusEnum.Updating]: "Updating",
[GameStatusEnum.Uninstalling]: "Uninstalling",
[GameStatusEnum.Running]: "Stop",
[GameStatusEnum.PartiallyInstalled]: "Resume",
const buttonNames: { [key in EmptyGameStatusEnum]: string } = {
Remote: "Install",
Queued: "Queued",
Downloading: "Downloading",
Validating: "Validating",
Updating: "Updating",
Uninstalling: "Uninstalling",
Running: "Stop",
};
const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
[GameStatusEnum.Queued]: QueueListIcon,
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
[GameStatusEnum.Validating]: ServerIcon,
[GameStatusEnum.SetupRequired]: WrenchIcon,
[GameStatusEnum.Installed]: PlayIcon,
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: StopIcon,
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon,
const buttonIcons: { [key in EmptyGameStatusEnum]: Component } = {
Remote: ArrowDownTrayIcon,
Queued: QueueListIcon,
Downloading: ArrowDownTrayIcon,
Validating: ServerIcon,
Updating: ArrowDownTrayIcon,
Uninstalling: TrashIcon,
Running: StopIcon,
};
const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.Remote]: () => emit("install"),
[GameStatusEnum.Queued]: () => emit("queue"),
[GameStatusEnum.Downloading]: () => emit("queue"),
[GameStatusEnum.Validating]: () => emit("queue"),
[GameStatusEnum.SetupRequired]: () => emit("launch"),
[GameStatusEnum.Installed]: () => emit("launch"),
[GameStatusEnum.Updating]: () => emit("queue"),
[GameStatusEnum.Uninstalling]: () => {},
[GameStatusEnum.Running]: () => emit("kill"),
[GameStatusEnum.PartiallyInstalled]: () => emit("resume"),
const buttonActions: { [key in EmptyGameStatusEnum]: () => void } = {
Remote: () => emit("install"),
Queued: () => emit("queue"),
Downloading: () => emit("queue"),
Validating: () => emit("queue"),
Updating: () => emit("queue"),
Uninstalling: () => {},
Running: () => emit("kill"),
};
</script>
+15 -7
View File
@@ -1,16 +1,24 @@
<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%]" />
<NuxtLink
v-if="onLinux"
to="/settings/compat"
>
<HeaderWidget :problem="protonError">
<img
src="/proton-logo.png"
class="relative z-50 size-5 brightness-[30%]"
/>
</HeaderWidget>
</NuxtLink>
</template>
<script setup lang="ts">
const appState = useAppState();
const onLinux = appState.value?.umuState !== "NotNeeded";
const paths = onLinux ? await useProtonPaths() : undefined;
const notInstalled = appState.value?.umuState === "NotInstalled";
const protonError = computed(
() =>
appState.value?.umuState === "NotInstalled" || !paths?.data.value.default,
);
</script>
+58 -44
View File
@@ -64,7 +64,7 @@
v-for="item in nav.items"
:key="nav.id"
:class="[
'transition-all duration-300 rounded-lg flex items-center px-1 py-1.5 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
'transition-all duration-300 rounded-lg flex items-center px-1 py-0.5 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
currentNavigation == item.id
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: item.isInstalled.value
@@ -83,21 +83,24 @@
alt=""
/>
</div>
<div class="truncate inline-flex items-center gap-x-2">
<div class="truncate flex flex-col">
<p class="text-sm whitespace-nowrap font-display font-semibold">
{{ item.label }}
</p>
<p
class="truncate text-[10px] font-bold uppercase font-display"
:class="[
gameStatusTextStyle[games[item.id].status.value.type],
getGameStatusStyleText(games[item.id].status.value)[0],
]"
>
{{ gameStatusText[games[item.id].status.value.type] }}
{{ getGameStatusStyleText(games[item.id].status.value)[1] }}
</p>
</div>
</div>
</NuxtLink>
<span v-if="nav.items.length == 0" class="text-xs text-zinc-500 mx-auto"
>No games in this category</span
>
</DisclosurePanel>
</Disclosure>
</TransitionGroup>
@@ -138,7 +141,8 @@ import {
} from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import {
GameStatusEnum,
type EmptyGameStatusEnum,
InstalledType,
type Collection as Collection,
type Game,
type GameStatus,
@@ -147,31 +151,44 @@ import { TransitionGroup } from "vue";
import { listen } from "@tauri-apps/api/event";
// Style information
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Installed]: "text-green-500",
[GameStatusEnum.Downloading]: "text-zinc-400",
[GameStatusEnum.Validating]: "text-blue-300",
[GameStatusEnum.Running]: "text-blue-500",
[GameStatusEnum.Remote]: "text-zinc-700",
[GameStatusEnum.Queued]: "text-zinc-400",
[GameStatusEnum.Updating]: "text-zinc-400",
[GameStatusEnum.Uninstalling]: "text-zinc-100",
[GameStatusEnum.SetupRequired]: "text-yellow-500",
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
const gameStatusTextStyle: { [key in EmptyGameStatusEnum]: string } = {
Downloading: "text-zinc-400",
Validating: "text-blue-300",
Running: "text-blue-500",
Remote: "text-zinc-700",
Queued: "text-zinc-400",
Updating: "text-zinc-400",
Uninstalling: "text-zinc-100",
};
const gameStatusText: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Not installed",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading...",
[GameStatusEnum.Validating]: "Validating...",
[GameStatusEnum.Installed]: "Installed",
[GameStatusEnum.Updating]: "Updating...",
[GameStatusEnum.Uninstalling]: "Uninstalling...",
[GameStatusEnum.SetupRequired]: "Setup required",
[GameStatusEnum.Running]: "Running",
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
const gameStatusText: { [key in EmptyGameStatusEnum]: string } = {
Remote: "Not installed",
Queued: "Queued",
Downloading: "Downloading...",
Validating: "Validating...",
Updating: "Updating...",
Uninstalling: "Uninstalling...",
Running: "Running",
};
function getGameStatusStyleText(status: GameStatus): [string, string] {
if (status.type === "Installed") {
if (status.install_type.type === InstalledType.Installed) {
return ["text-green-500", "Installed"];
}
if (status.install_type.type === InstalledType.PartiallyInstalled) {
return ["text-gray-400", "Partially installed"];
}
if (status.install_type.type === InstalledType.SetupRequired) {
return ["text-yellow-500", "Setup required"];
}
throw (
"Non-exhaustive installed type, missing: " +
JSON.stringify(status.install_type)
);
}
return [gameStatusTextStyle[status.type], gameStatusText[status.type]];
}
const router = useRouter();
const searchQuery = ref("");
@@ -277,25 +294,22 @@ 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 != "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,
+20 -30
View File
@@ -1,50 +1,45 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { Game, GameStatus, GameStatusEnum, GameVersion } from "~/types";
import type {
Game,
GameStatus,
GameStatusEnum,
GameVersion,
RawGameStatus,
} from "~/types";
const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } =
const gameRegistry: { [key: string]: { game: Game; version: Ref<GameVersion | undefined> } } =
{};
const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } };
export type SerializedGameStatus = [
{ type: GameStatusEnum },
OptionGameStatus | null,
];
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
export const parseStatus = (status: RawGameStatus): GameStatus => {
console.log(status[0]);
if (status[0]) {
return {
type: status[0].type,
};
} else if (status[1]) {
const [[gameStatus, options]] = Object.entries(status[1]);
return {
type: gameStatus as GameStatusEnum,
...options,
};
} else {
throw new Error("No game status");
return status[0];
}
if (status[1]) {
return status[1];
}
throw new Error("No game status: " + JSON.stringify(status));
};
export const useGame = async (gameId: string) => {
if (!gameRegistry[gameId]) {
const data: {
game: Game;
status: SerializedGameStatus;
status: RawGameStatus;
version?: GameVersion;
} = await invoke("fetch_game", {
gameId,
});
gameRegistry[gameId] = { game: data.game, version: data.version };
gameRegistry[gameId] = { game: data.game, version: ref(data.version) };
if (!gameStatusRegistry[gameId]) {
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
listen(`update_game/${gameId}`, (event) => {
const payload: {
status: SerializedGameStatus;
status: RawGameStatus;
version?: GameVersion;
} = event.payload as any;
gameStatusRegistry[gameId].value = parseStatus(payload.status);
@@ -57,7 +52,7 @@ export const useGame = async (gameId: string) => {
* on transient state updates.
*/
if (payload.version) {
gameRegistry[gameId].version = payload.version;
gameRegistry[gameId].version.value = payload.version;
}
});
}
@@ -68,11 +63,6 @@ export const useGame = async (gameId: string) => {
return { ...game, status };
};
export type FrontendGameConfiguration = {
launchString: string;
overrideProtonPath?: string;
};
export type LaunchResult =
| { result: "Success" }
| { result: "InstallRequired"; data: [string, string] };
@@ -102,4 +92,4 @@ export type VersionOption = {
export type ProtonPath = {
path: string;
name: string;
};
};
+32
View File
@@ -0,0 +1,32 @@
import { invoke } from "@tauri-apps/api/core";
interface ProtonPaths {
data: Ref<{
autodiscovered: ProtonPath[];
custom: ProtonPath[];
default?: string;
}>;
refresh: () => Promise<void>;
}
const protonPaths = useState<ProtonPaths["data"]["value"]>(
"proton_paths",
undefined,
);
export const useProtonPaths = async (): Promise<ProtonPaths> => {
const refresh = async () => {
protonPaths.value = await invoke("fetch_proton_paths");
};
if (protonPaths.value)
return {
data: protonPaths,
refresh,
};
await refresh();
return {
data: protonPaths,
refresh,
};
};
+2 -1
View File
@@ -7,7 +7,8 @@
"build": "nuxt generate",
"dev": "nuxt dev",
"postinstall": "nuxt prepare",
"tauri": "tauri"
"tauri": "tauri",
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
+1
View File
@@ -6,6 +6,7 @@
>
<LibrarySearch />
</div>
<div class="grow overflow-y-auto">
<NuxtErrorBoundary>
<NuxtPage />
+197 -76
View File
@@ -16,14 +16,45 @@
</div>
<div class="relative z-10">
<div class="px-8 pb-4">
<div class="px-8">
<h1
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8"
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg"
>
{{ game.mName }}
</h1>
<div class="relative" v-if="status.type === 'Installed' && status.install_type.type != InstalledType.PartiallyInstalled">
<div
v-if="!version?.userConfiguration?.enableUpdates"
class="absolute mt-1 inline-flex items-center gap-x-1 text-xs text-zinc-400"
>
Version pinned
<svg
class="size-3 text-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.1835 7.80516L16.2188 4.83755C14.1921 2.8089 13.1788 1.79457 12.0904 2.03468C11.0021 2.2748 10.5086 3.62155 9.5217 6.31506L8.85373 8.1381C8.59063 8.85617 8.45908 9.2152 8.22239 9.49292C8.11619 9.61754 7.99536 9.72887 7.86251 9.82451C7.56644 10.0377 7.19811 10.1392 6.46145 10.3423C4.80107 10.8 3.97088 11.0289 3.65804 11.5721C3.5228 11.8069 3.45242 12.0735 3.45413 12.3446C3.45809 12.9715 4.06698 13.581 5.28476 14.8L6.69935 16.2163L2.22345 20.6964C1.92552 20.9946 1.92552 21.4782 2.22345 21.7764C2.52138 22.0746 3.00443 22.0746 3.30236 21.7764L7.77841 17.2961L9.24441 18.7635C10.4699 19.9902 11.0827 20.6036 11.7134 20.6045C11.9792 20.6049 12.2404 20.5358 12.4713 20.4041C13.0192 20.0914 13.2493 19.2551 13.7095 17.5825C13.9119 16.8472 14.013 16.4795 14.2254 16.1835C14.3184 16.054 14.4262 15.9358 14.5468 15.8314C14.8221 15.593 15.1788 15.459 15.8922 15.191L17.7362 14.4981C20.4 13.4973 21.7319 12.9969 21.9667 11.9115C22.2014 10.826 21.1954 9.81905 19.1835 7.80516Z"
fill="currentColor"
/>
</svg>
</div>
<div
v-else-if="!status.update_available"
class="absolute mt-1 inline-flex items-center gap-x-1 text-xs text-zinc-400"
>
Up to date <CheckCircleIcon class="size-3 text-green-600" />
</div>
<div
v-else-if="status.update_available"
class="absolute mt-1 inline-flex items-center gap-x-1 text-xs text-zinc-400"
>
Update available <ArrowDownTrayIcon class="size-3 text-blue-600" />
</div>
</div>
<div class="flex flex-row gap-x-4 items-stretch mb-8">
<div class="mt-8 flex flex-row gap-x-4 items-stretch">
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
<GameStatusButton
@install="() => installFlow()"
@@ -35,6 +66,13 @@
@resume="() => resumeDownload()"
:status="status"
/>
<button
v-if="status.type === 'Installed' && status.update_available"
class="transition-transform duration-300 hover:scale-105 active:scale-95 inline-flex gap-x-2 items-center rounded-md bg-blue-600 px-6 font-semibold text-white shadow-xl backdrop-blur-sm hover:bg-blue-700 uppercase font-display"
@click="() => installFlow()"
>
Update <ArrowDownTrayIcon class="size-5" />
</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="{
@@ -51,7 +89,7 @@
</div>
<!-- Main content -->
<div class="w-full bg-zinc-900 px-8 py-6">
<div class="mt-8 w-full bg-zinc-900 px-8">
<div class="grid grid-cols-[2fr,1fr] gap-8">
<div class="space-y-6">
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
@@ -68,19 +106,22 @@
Game Images
</h2>
<div class="relative">
<div v-if="mediaUrls.length > 0">
<div v-if="game.mImageCarouselObjectIds.length > 0">
<div
class="relative aspect-video rounded-lg overflow-hidden cursor-pointer group"
>
<div
class="absolute inset-0"
@click="fullscreenImage = mediaUrls[currentImageIndex]"
@click="
fullscreenImage =
game.mImageCarouselObjectIds[currentImageIndex]
"
>
<TransitionGroup name="slide" tag="div" class="h-full">
<img
v-for="(url, index) in mediaUrls"
v-for="(url, index) in game.mImageCarouselObjectIds"
:key="index"
:src="url"
:src="useObject(url)"
class="absolute inset-0 w-full h-full object-cover"
v-show="index === currentImageIndex"
/>
@@ -92,7 +133,7 @@
>
<div class="pointer-events-auto">
<button
v-if="mediaUrls.length > 1"
v-if="game.mImageCarouselObjectIds.length > 1"
@click.stop="previousImage()"
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
>
@@ -101,7 +142,7 @@
</div>
<div class="pointer-events-auto">
<button
v-if="mediaUrls.length > 1"
v-if="game.mImageCarouselObjectIds.length > 1"
@click.stop="nextImage()"
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
>
@@ -125,7 +166,7 @@
class="absolute -bottom-2 left-1/2 -translate-x-1/2 flex gap-x-2"
>
<button
v-for="(_, index) in mediaUrls"
v-for="(_, index) in game.mImageCarouselObjectIds"
:key="index"
@click.stop="currentImageIndex = index"
class="w-1.5 h-1.5 rounded-full transition-all"
@@ -174,11 +215,7 @@
</div>
<div class="space-y-6">
<div
v-if="
versionOptions && versionOptions.length > 0 && currentVersionOption
"
>
<div v-if="versionOptions && versionOptions.length > 0">
<Listbox as="div" v-model="installVersionIndex">
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
>Version</ListboxLabel
@@ -187,18 +224,9 @@
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
>
<span class="block truncate"
>{{
currentVersionOption.displayName ||
currentVersionOption.versionPath
}}
on
{{ currentVersionOption.platform }} ({{
formatKilobytes(
currentVersionOption.size.installSize / 1024,
)
}}B)</span
>
<span class="block truncate">{{
formatVersionOptionText(installVersionIndex)
}}</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
@@ -209,6 +237,48 @@
</span>
</ListboxButton>
<div
v-if="installVersionIndex == -1"
class="mt-3 rounded-md bg-blue-500/10 p-2 outline outline-blue-500/20"
>
<div class="flex">
<div class="shrink-0">
<InformationCircleIcon
class="size-4 text-blue-400"
aria-hidden="true"
/>
</div>
<div class="ml-2 flex-1 md:flex md:justify-between">
<p class="text-xs text-blue-300">
"Latest" will notify you when there is a new version
available. Choose another version to pin this game's
version.
</p>
</div>
</div>
</div>
<div
v-else
class="mt-3 rounded-md bg-blue-500/10 p-2 outline outline-blue-500/20"
>
<div class="flex">
<div class="shrink-0">
<InformationCircleIcon
class="size-4 text-blue-400"
aria-hidden="true"
/>
</div>
<div class="ml-2 flex-1 md:flex md:justify-between">
<p class="text-xs text-blue-300">
This game will be pinned to "{{
currentVersionOption?.displayName ||
currentVersionOption?.versionPath
}}"
</p>
</div>
</div>
</div>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
@@ -217,6 +287,39 @@
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
:value="-1"
v-slot="{ active, selected }"
>
<li
:class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
>
<span
:class="[
selected
? 'font-semibold text-zinc-100'
: 'font-normal',
'block truncate',
]"
>{{ formatVersionOptionText(-1) }}</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
<ListboxOption
as="template"
v-for="(version, versionIdx) in versionOptions"
@@ -237,13 +340,7 @@
: 'font-normal',
'block truncate',
]"
>{{ version.displayName || version.versionPath }} on
{{ version.platform }} ({{
formatKilobytes(
versionOptions[installVersionIndex].size
.installSize / 1024,
)
}}B)</span
>{{ formatVersionOptionText(versionIdx) }}</span
>
<span
@@ -464,7 +561,11 @@
Don't.
-->
<GameOptionsModal
v-if="status.type === GameStatusEnum.Installed"
v-if="
status.type === 'Installed' &&
(status.install_type.type == InstalledType.Installed ||
status.install_type.type == InstalledType.SetupRequired)
"
v-model="configureModalOpen"
:game-id="game.id"
/>
@@ -494,14 +595,14 @@
</button>
<button
v-if="mediaUrls.length > 1"
v-if="game.mImageCarouselObjectIds.length > 1"
@click.stop="previousImage()"
class="absolute left-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
>
<ChevronLeftIcon class="size-6" />
</button>
<button
v-if="mediaUrls.length > 1"
v-if="game.mImageCarouselObjectIds.length > 1"
@click.stop="nextImage()"
class="absolute right-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
>
@@ -515,10 +616,10 @@
@click.stop
>
<img
v-for="(url, index) in mediaUrls"
v-for="(url, index) in game.mImageCarouselObjectIds"
v-show="currentImageIndex === index"
:key="index"
:src="url"
:src="useObject(url)"
class="max-h-[90vh] max-w-[90vw] object-contain"
:alt="`${game.mName} screenshot ${index + 1}`"
/>
@@ -528,7 +629,8 @@
class="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 rounded-full bg-zinc-900/50 backdrop-blur-sm"
>
<p class="text-zinc-100 text-sm font-medium">
{{ currentImageIndex + 1 }} / {{ mediaUrls.length }}
{{ currentImageIndex + 1 }} /
{{ game.mImageCarouselObjectIds.length }}
</p>
</div>
</div>
@@ -559,31 +661,30 @@ import {
ArrowsPointingOutIcon,
PhotoIcon,
PlayIcon,
InformationCircleIcon,
} from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { MinusIcon, ServerIcon, XCircleIcon } from "@heroicons/vue/24/solid";
import {
ArrowDownTrayIcon,
CheckCircleIcon,
MapPinIcon,
MinusIcon,
ServerIcon,
XCircleIcon,
} from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core";
import { micromark } from "micromark";
import { GameStatusEnum } from "~/types";
import { InstalledType } from "~/types";
const route = useRoute();
const router = useRouter();
const id = route.params.id.toString();
const { game: rawGame, status } = await useGame(id);
const game = ref(rawGame);
const { game, status, version } = await useGame(id);
const bannerUrl = await useObject(game.value.mBannerObjectId);
const bannerUrl = await useObject(game.mBannerObjectId);
// Get all available images
const mediaUrls = await Promise.all(
game.value.mImageCarouselObjectIds.map(async (v) => {
const src = await useObject(v);
return src;
}),
);
const htmlDescription = micromark(game.value.mDescription);
const htmlDescription = micromark(game.mDescription);
const installFlowOpen = ref(false);
const versionOptions = ref<undefined | Array<VersionOption>>();
@@ -596,10 +697,11 @@ async function installFlow() {
installFlowOpen.value = true;
versionOptions.value = undefined;
installDirs.value = undefined;
installError.value = undefined;
try {
versionOptions.value = await invoke("fetch_game_version_options", {
gameId: game.value.id,
gameId: game.id,
});
installDirs.value = await invoke("fetch_download_dir_stats");
} catch (error) {
@@ -610,21 +712,20 @@ async function installFlow() {
const installLoading = ref(false);
const installError = ref<string | undefined>();
const installVersionIndex = ref(0);
const installVersionIndex = ref(-1);
const installDir = ref(0);
const installDepsDisabled = ref<{ [key: string]: boolean }>({});
const currentVersionOption = computed(
() => versionOptions.value?.[installVersionIndex.value],
);
async function install() {
try {
if (!versionOptions.value) throw new Error("Versions have not been loaded");
installLoading.value = true;
const versionOption = versionOptions.value[installVersionIndex.value];
const versionOption =
versionOptions.value[Math.max(installVersionIndex.value, 0)];
const isLatest = installVersionIndex.value == -1;
const games = [
{ gameId: game.value.id, versionId: versionOption.versionId },
{ gameId: game.id, versionId: versionOption.versionId },
...versionOption.requiredContent
.filter((v) => !installDepsDisabled.value[v.versionId])
.map((v) => ({ gameId: v.gameId, versionId: v.versionId })),
@@ -636,6 +737,7 @@ async function install() {
versionId: game.versionId,
installDir: installDir.value,
targetPlatform: versionOption.platform,
enableUpdates: isLatest,
});
}
@@ -647,9 +749,23 @@ async function install() {
installLoading.value = false;
}
const currentVersionOption = computed(
() => versionOptions.value?.[Math.max(installVersionIndex.value, 0)],
);
function formatVersionOptionText(index: number) {
if (!versionOptions.value) return undefined;
const versionOption = versionOptions.value[Math.max(index, 0)];
const template = `${versionOption.displayName || versionOption.versionPath} on ${versionOption.platform}, ${formatKilobytes(versionOption.size.installSize / 1024)}B`;
if (index == -1) {
return `Latest (${template})`;
}
return template;
}
async function resumeDownload() {
try {
await invoke("resume_download", { gameId: game.value.id });
await invoke("resume_download", { gameId: game.id });
} catch (e) {
console.error(e);
}
@@ -659,14 +775,17 @@ const launchOptions = ref<Array<{ name: string }> | undefined>(undefined);
const launchOptionsOpen = computed(() => launchOptions.value !== undefined);
async function launch() {
if (status.value.type == GameStatusEnum.SetupRequired) {
if (
status.value.type == "Installed" &&
status.value.install_type.type == InstalledType.SetupRequired
) {
await launchIndex(0);
return;
}
try {
const fetchedLaunchOptions = await invoke<Array<{ name: string }>>(
"get_launch_options",
{ id: game.value.id },
{ id: game.id },
);
if (fetchedLaunchOptions.length == 1) {
await launchIndex(0);
@@ -677,8 +796,8 @@ async function launch() {
createModal(
ModalType.Notification,
{
title: `Couldn't run "${game.value.mName}"`,
description: `Drop failed to launch "${game.value.mName}": ${e}`,
title: `Couldn't run "${game.mName}"`,
description: `Drop failed to launch "${game.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c(),
@@ -695,7 +814,7 @@ async function launchIndex(index: number) {
launchOptions.value = undefined;
try {
const result = await invoke<LaunchResult>("launch_game", {
id: game.value.id,
id: game.id,
index,
});
if (result.result == "InstallRequired") {
@@ -708,8 +827,8 @@ async function launchIndex(index: number) {
createModal(
ModalType.Notification,
{
title: `Couldn't run "${game.value.mName}"`,
description: `Drop failed to launch "${game.value.mName}": ${e}`,
title: `Couldn't run "${game.mName}"`,
description: `Drop failed to launch "${game.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c(),
@@ -722,18 +841,18 @@ async function queue() {
}
async function uninstall() {
await invoke("uninstall_game", { gameId: game.value.id });
await invoke("uninstall_game", { gameId: game.id });
}
async function kill() {
try {
await invoke("kill_game", { gameId: game.value.id });
await invoke("kill_game", { gameId: game.id });
} catch (e) {
createModal(
ModalType.Notification,
{
title: `Couldn't stop "${game.value.mName}"`,
description: `Drop failed to stop "${game.value.mName}": ${e}`,
title: `Couldn't stop "${game.mName}"`,
description: `Drop failed to stop "${game.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c(),
@@ -743,12 +862,14 @@ async function kill() {
}
function nextImage() {
currentImageIndex.value = (currentImageIndex.value + 1) % mediaUrls.length;
currentImageIndex.value =
(currentImageIndex.value + 1) % game.mImageCarouselObjectIds.length;
}
function previousImage() {
currentImageIndex.value =
(currentImageIndex.value - 1 + mediaUrls.length) % mediaUrls.length;
(currentImageIndex.value - 1 + game.mImageCarouselObjectIds.length) %
game.mImageCarouselObjectIds.length;
}
const fullscreenImage = ref<string | null>(null);
+11 -11
View File
@@ -45,18 +45,23 @@
</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
v-if="!paths.data.value?.default"
class="mt-4 rounded-md bg-red-500/15 p-4 outline outline-red-500/25"
>
<div class="flex">
<div class="shrink-0">
<ExclamationTriangleIcon class="size-5 text-yellow-400" aria-hidden="true" />
<XCircleIcon class="size-5 text-red-400" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-200">
<h3 class="text-sm font-medium text-red-200">
No default Proton layer
</h3>
<div class="mt-2 text-sm text-yellow-200/80">
<div class="mt-2 text-sm text-red-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.
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>
@@ -290,12 +295,7 @@ 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 paths = await useProtonPaths();
const pickLayerModal = ref(false);
const pickError = ref<string | null>(null);
+66 -42
View File
@@ -4,6 +4,20 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
devalue@>=5.1.0 <5.6.2: '>=5.6.2'
devalue@>=5.3.0 <=5.6.1: '>=5.6.2'
diff@>=6.0.0 <8.0.3: '>=8.0.3'
h3@<=1.15.4: '>=1.15.5'
lodash@>=4.0.0 <=4.17.22: '>=4.17.23'
markdown-it@>=13.0.0 <14.1.1: '>=14.1.1'
node-forge@<1.3.2: '>=1.3.2'
seroval@<1.4.1: '>=1.4.1'
seroval@<=1.4.0: '>=1.4.1'
tar@<7.5.7: '>=7.5.7'
tar@<=7.5.2: '>=7.5.3'
tar@<=7.5.3: '>=7.5.4'
importers:
.:
@@ -33,8 +47,8 @@ importers:
specifier: ^2.16.1
version: 2.16.3
markdown-it:
specifier: ^14.1.0
version: 14.1.0
specifier: '>=14.1.1'
version: 14.1.1
micromark:
specifier: ^4.0.1
version: 4.0.2
@@ -1820,8 +1834,8 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
devalue@5.5.0:
resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==}
devalue@5.6.2:
resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
@@ -1829,8 +1843,8 @@ packages:
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
diff@8.0.2:
resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==}
diff@8.0.3:
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
engines: {node: '>=0.3.1'}
dlv@1.1.3:
@@ -2102,8 +2116,8 @@ packages:
resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
h3@1.15.4:
resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==}
h3@1.15.5:
resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
@@ -2404,8 +2418,8 @@ packages:
lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -2426,8 +2440,8 @@ packages:
magicast@0.5.1:
resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
markdown-it@14.1.1:
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
hasBin: true
math-intrinsics@1.1.0:
@@ -2639,8 +2653,8 @@ packages:
encoding:
optional: true
node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
node-forge@1.3.3:
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
engines: {node: '>= 6.13.0'}
node-gyp-build@4.8.4:
@@ -2650,6 +2664,9 @@ packages:
node-mock-http@1.0.3:
resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==}
node-mock-http@1.0.4:
resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@@ -3367,8 +3384,8 @@ packages:
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
seroval@1.4.0:
resolution: {integrity: sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==}
seroval@1.5.0:
resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==}
engines: {node: '>=10'}
serve-placeholder@2.0.2:
@@ -3568,8 +3585,8 @@ packages:
tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
tar@7.5.2:
resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==}
tar@7.5.9:
resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==}
engines: {node: '>=18'}
terser@5.44.1:
@@ -3645,6 +3662,9 @@ packages:
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
ultrahtml@1.6.0:
resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==}
@@ -4375,7 +4395,7 @@ snapshots:
node-fetch: 2.7.0
nopt: 8.1.0
semver: 7.7.3
tar: 7.5.2
tar: 7.5.9
transitivePeerDependencies:
- encoding
- supports-color
@@ -4441,7 +4461,7 @@ snapshots:
'@nuxt/devtools-wizard@3.1.0':
dependencies:
consola: 3.4.2
diff: 8.0.2
diff: 8.0.3
execa: 8.0.1
magicast: 0.5.1
pathe: 2.0.3
@@ -4550,11 +4570,11 @@ snapshots:
consola: 3.4.2
defu: 6.1.4
destr: 2.0.5
devalue: 5.5.0
devalue: 5.6.2
errx: 0.1.0
escape-string-regexp: 5.0.0
exsolve: 1.0.8
h3: 1.15.4
h3: 1.15.5
impound: 1.0.0
klona: 2.0.6
mocked-exports: 0.1.1
@@ -4645,7 +4665,7 @@ snapshots:
exsolve: 1.0.8
externality: 1.0.2
get-port-please: 3.2.0
h3: 1.15.4
h3: 1.15.5
jiti: 2.6.1
knitwork: 1.3.0
magic-string: 0.30.21
@@ -4658,7 +4678,7 @@ snapshots:
pkg-types: 2.3.0
postcss: 8.5.6
rollup-plugin-visualizer: 6.0.5(rollup@4.53.3)
seroval: 1.4.0
seroval: 1.5.0
std-env: 3.10.0
ufo: 1.6.1
unenv: 2.0.0-rc.24
@@ -4700,7 +4720,7 @@ snapshots:
c12: 3.3.2(magicast@0.5.1)
consola: 3.4.2
defu: 6.1.4
h3: 1.15.4
h3: 1.15.5
klona: 2.0.6
ohash: 2.0.11
pathe: 2.0.3
@@ -5400,7 +5420,7 @@ snapshots:
graceful-fs: 4.2.11
is-stream: 2.0.1
lazystream: 1.0.1
lodash: 4.17.21
lodash: 4.17.23
normalize-path: 3.0.0
readable-stream: 4.7.0
@@ -5804,7 +5824,7 @@ snapshots:
detect-libc@2.1.2: {}
devalue@5.5.0: {}
devalue@5.6.2: {}
devlop@1.1.0:
dependencies:
@@ -5812,7 +5832,7 @@ snapshots:
didyoumean@1.2.2: {}
diff@8.0.2: {}
diff@8.0.3: {}
dlv@1.1.3: {}
@@ -6105,16 +6125,16 @@ snapshots:
dependencies:
duplexer: 0.1.2
h3@1.15.4:
h3@1.15.5:
dependencies:
cookie-es: 1.2.2
crossws: 0.3.5
defu: 6.1.4
destr: 2.0.5
iron-webcrypto: 1.2.1
node-mock-http: 1.0.3
node-mock-http: 1.0.4
radix3: 1.1.2
ufo: 1.6.1
ufo: 1.6.3
uncrypto: 0.1.3
has-flag@4.0.0: {}
@@ -6416,11 +6436,11 @@ snapshots:
crossws: 0.3.5
defu: 6.1.4
get-port-please: 3.2.0
h3: 1.15.4
h3: 1.15.5
http-shutdown: 1.2.2
jiti: 2.6.1
mlly: 1.8.0
node-forge: 1.3.1
node-forge: 1.3.3
pathe: 1.1.2
std-env: 3.10.0
ufo: 1.6.1
@@ -6441,7 +6461,7 @@ snapshots:
lodash.uniq@4.5.0: {}
lodash@4.17.21: {}
lodash@4.17.23: {}
lru-cache@10.4.3: {}
@@ -6473,7 +6493,7 @@ snapshots:
'@babel/types': 7.28.5
source-map-js: 1.2.1
markdown-it@14.1.0:
markdown-it@14.1.1:
dependencies:
argparse: 2.0.1
entities: 4.5.0
@@ -6729,7 +6749,7 @@ snapshots:
exsolve: 1.0.8
globby: 15.0.0
gzip-size: 7.0.0
h3: 1.15.4
h3: 1.15.5
hookable: 5.5.3
httpxy: 0.1.7
ioredis: 5.8.2
@@ -6808,12 +6828,14 @@ snapshots:
dependencies:
whatwg-url: 5.0.0
node-forge@1.3.1: {}
node-forge@1.3.3: {}
node-gyp-build@4.8.4: {}
node-mock-http@1.0.3: {}
node-mock-http@1.0.4: {}
node-releases@2.0.27: {}
nopt@8.1.0:
@@ -6856,11 +6878,11 @@ snapshots:
cookie-es: 2.0.0
defu: 6.1.4
destr: 2.0.5
devalue: 5.5.0
devalue: 5.6.2
errx: 0.1.0
escape-string-regexp: 5.0.0
exsolve: 1.0.8
h3: 1.15.4
h3: 1.15.5
hookable: 5.5.3
ignore: 7.0.5
impound: 1.0.0
@@ -7637,7 +7659,7 @@ snapshots:
dependencies:
randombytes: 2.1.0
seroval@1.4.0: {}
seroval@1.5.0: {}
serve-placeholder@2.0.2:
dependencies:
@@ -7863,7 +7885,7 @@ snapshots:
- bare-abort-controller
- react-native-b4a
tar@7.5.2:
tar@7.5.9:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
@@ -7934,6 +7956,8 @@ snapshots:
ufo@1.6.1: {}
ufo@1.6.3: {}
ultrahtml@1.6.0: {}
uncrypto@0.1.3: {}
@@ -8022,7 +8046,7 @@ snapshots:
anymatch: 3.1.3
chokidar: 4.0.3
destr: 2.0.5
h3: 1.15.4
h3: 1.15.5
lru-cache: 10.4.3
node-fetch-native: 1.6.7
ofetch: 1.5.1
+14
View File
@@ -1,3 +1,17 @@
onlyBuiltDependencies:
- '@parcel/watcher'
- esbuild
overrides:
devalue@>=5.1.0 <5.6.2: '>=5.6.2'
devalue@>=5.3.0 <=5.6.1: '>=5.6.2'
diff@>=6.0.0 <8.0.3: '>=8.0.3'
h3@<=1.15.4: '>=1.15.5'
lodash@>=4.0.0 <=4.17.22: '>=4.17.23'
markdown-it@>=13.0.0 <14.1.1: '>=14.1.1'
node-forge@<1.3.2: '>=1.3.2'
seroval@<1.4.1: '>=1.4.1'
seroval@<=1.4.0: '>=1.4.1'
tar@<7.5.7: '>=7.5.7'
tar@<=7.5.2: '>=7.5.3'
tar@<=7.5.3: '>=7.5.4'
+2 -2
View File
@@ -7,7 +7,7 @@ export default {
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
"../libs/drop-base/**/*.{js,vue,ts}"
"../libs/drop-base/**/*.{js,vue,ts}",
],
theme: {
extend: {
@@ -17,5 +17,5 @@ export default {
},
},
},
plugins: [require("@tailwindcss/forms"), require('@tailwindcss/typography')],
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
};
+31 -15
View File
@@ -53,6 +53,7 @@ export type GameVersion = {
userConfiguration: {
launchTemplate: string;
overrideProtonPath: string;
enableUpdates: boolean
};
setups: Array<{ platform: string }>;
launches: Array<{ platform: string }>;
@@ -67,24 +68,39 @@ export enum AppStatus {
ServerUnavailable = "ServerUnavailable",
}
export enum GameStatusEnum {
Remote = "Remote",
Queued = "Queued",
Downloading = "Downloading",
Validating = "Validating",
Installed = "Installed",
Updating = "Updating",
Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired",
Running = "Running",
export type EmptyGameStatusEnum =
| "Remote"
| "Queued"
| "Downloading"
| "Validating"
| "Updating"
| "Uninstalling"
| "Running";
export enum InstalledType {
PartiallyInstalled = "PartiallyInstalled",
SetupRequired = "SetupRequired",
Installed = "Installed",
}
export type GameStatus = {
type: GameStatusEnum;
version_name?: string;
install_dir?: string;
};
export interface InstalledGameStatusData {
install_type: { type: InstalledType };
version_id: string;
install_dir: string;
update_available: boolean;
}
export type GameStatus =
| {
type: EmptyGameStatusEnum;
}
| ({
type: "Installed";
} & InstalledGameStatusData);
export type GameStatusEnum = GameStatus["type"];
export type RawGameStatus = [GameStatus | null, GameStatus | null];
export enum DownloadableType {
Game = "Game",