mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-06-22 04:11:37 +10:00
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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
>
|
||||
<LibrarySearch />
|
||||
</div>
|
||||
|
||||
<div class="grow overflow-y-auto">
|
||||
<NuxtErrorBoundary>
|
||||
<NuxtPage />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Generated
+66
-42
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+29
-7
@@ -54,9 +54,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@@ -607,9 +607,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1465,9 +1465,10 @@ dependencies = [
|
||||
name = "drop-app"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"atomic-instant-full",
|
||||
"bitcode",
|
||||
"boxcar",
|
||||
"bytes",
|
||||
"cacache 13.1.0",
|
||||
"chrono",
|
||||
@@ -2072,11 +2073,10 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hex 0.4.3",
|
||||
"log",
|
||||
"native_model",
|
||||
"pot",
|
||||
"rayon",
|
||||
"remote",
|
||||
"reqwest 0.12.28",
|
||||
"rustix 1.1.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
@@ -2471,6 +2471,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -4591,6 +4602,17 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pot"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf741fa415952eb20f27fbc210dc85f31cc7cdc80aa3ce81d5e27d28a6f45dc2"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"half",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
|
||||
@@ -29,9 +29,10 @@ rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.101"
|
||||
async-trait = "0.1.89"
|
||||
atomic-instant-full = "0.1.0"
|
||||
bitcode = "0.6.6"
|
||||
boxcar = "0.2.7"
|
||||
bytes = "1.10.1"
|
||||
cacache = "13.1.0"
|
||||
chrono = "0.4.38"
|
||||
|
||||
@@ -2,10 +2,11 @@ use serde::Serialize;
|
||||
|
||||
use crate::{app_status::AppStatus, user::User};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[derive(Clone, Serialize, PartialEq, Eq)]
|
||||
pub enum UmuState {
|
||||
NotNeeded,
|
||||
NotInstalled,
|
||||
NoDefault,
|
||||
Installed,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = LazyLock::new(|| {
|
||||
)
|
||||
});
|
||||
|
||||
/*
|
||||
pub(crate) static KEY_IV: LazyLock<([u8; 16], [u8; 16])> = LazyLock::new(|| {
|
||||
let entry = Entry::new("drop", "database_key").expect("failed to open keyring");
|
||||
let mut key = entry.get_secret().unwrap_or_else(|_| {
|
||||
@@ -38,3 +39,7 @@ pub(crate) static KEY_IV: LazyLock<([u8; 16], [u8; 16])> = LazyLock::new(|| {
|
||||
iv[0..16].try_into().expect("iv wrong length"),
|
||||
)
|
||||
});
|
||||
*/
|
||||
|
||||
// TODO: fix keyring
|
||||
pub(crate) static KEY_IV: LazyLock<([u8; 16], [u8; 16])> = LazyLock::new(|| ([0; 16], [0; 16]));
|
||||
@@ -12,6 +12,7 @@ pub mod data {
|
||||
pub type DatabaseAuth = v1::DatabaseAuth;
|
||||
|
||||
pub type GameDownloadStatus = v1::GameDownloadStatus;
|
||||
pub type InstalledGameType = v1::InstalledGameType;
|
||||
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
|
||||
/**
|
||||
* Need to be universally accessible by the ID, and the version is just a couple sprinkles on top
|
||||
@@ -19,6 +20,7 @@ pub mod data {
|
||||
pub type DownloadableMetadata = v1::DownloadableMetadata;
|
||||
pub type DownloadType = v1::DownloadType;
|
||||
pub type DatabaseApplications = v1::DatabaseApplications;
|
||||
pub type UserConfiguration = v1::UserConfiguration;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -77,6 +79,7 @@ pub mod data {
|
||||
UserConfiguration {
|
||||
launch_template: "{}".to_owned(),
|
||||
override_proton_path: None,
|
||||
enable_updates: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +88,13 @@ pub mod data {
|
||||
pub struct UserConfiguration {
|
||||
pub launch_template: String,
|
||||
pub override_proton_path: Option<String>,
|
||||
pub enable_updates: bool,
|
||||
}
|
||||
|
||||
impl Default for UserConfiguration {
|
||||
fn default() -> Self {
|
||||
default_template()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -156,25 +166,31 @@ pub mod data {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum InstalledGameType {
|
||||
SetupRequired,
|
||||
Installed,
|
||||
PartiallyInstalled {
|
||||
#[serde(skip)]
|
||||
configuration: UserConfiguration,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum GameDownloadStatus {
|
||||
Remote {},
|
||||
SetupRequired {
|
||||
version_name: String,
|
||||
install_dir: String,
|
||||
},
|
||||
Installed {
|
||||
version_name: String,
|
||||
install_dir: String,
|
||||
},
|
||||
PartiallyInstalled {
|
||||
version_name: String,
|
||||
install_type: InstalledGameType,
|
||||
version_id: String,
|
||||
install_dir: String,
|
||||
update_available: bool,
|
||||
},
|
||||
}
|
||||
// Stuff that shouldn't be synced to disk
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ApplicationTransientStatus {
|
||||
Queued { version_id: String },
|
||||
Downloading { version_id: String },
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use log::warn;
|
||||
use log::{info, warn};
|
||||
use remote::{
|
||||
error::RemoteAccessError,
|
||||
requests::{generate_url, make_authenticated_get},
|
||||
@@ -108,6 +108,8 @@ impl DepotManager {
|
||||
})
|
||||
.collect::<Vec<Depot>>();
|
||||
|
||||
info!("syncing {} depots...", new_depots.len());
|
||||
|
||||
for depot in &mut new_depots {
|
||||
if let Err(sync_error) = self.sync_depot(depot).await {
|
||||
warn!("failed to sync depot {}: {:?}", depot.endpoint, sync_error);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use humansize::{BINARY, format_size};
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
io,
|
||||
sync::{Arc, mpsc::SendError},
|
||||
fmt::{Display, Formatter}, io, path::StripPrefixError, sync::{Arc, mpsc::SendError}
|
||||
};
|
||||
|
||||
use remote::error::RemoteAccessError;
|
||||
@@ -85,4 +83,10 @@ impl From<RemoteAccessError> for ApplicationDownloadError {
|
||||
fn from(value: RemoteAccessError) -> Self {
|
||||
ApplicationDownloadError::Communication(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StripPrefixError> for ApplicationDownloadError {
|
||||
fn from(value: StripPrefixError) -> Self {
|
||||
ApplicationDownloadError::IoError(Arc::new(io::Error::other(value)))
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,10 @@ droplet-rs = { git = "https://github.com/Drop-OSS/droplet-rs" }
|
||||
futures-util = "*"
|
||||
hex = "0.4.3"
|
||||
log = "0.4.28"
|
||||
native_model = { git = "https://github.com/Drop-OSS/native_model.git", version = "0.6.4", features = [
|
||||
"rmp_serde_1_3"
|
||||
] }
|
||||
pot = "3.0.1"
|
||||
rayon = "1.11.0"
|
||||
remote = { path = "../remote", version = "0.1.0" }
|
||||
reqwest = "0.12.23"
|
||||
rustix = "1.1.2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
serde_with = "3.15.0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use database::models::data::UserConfiguration;
|
||||
use database::{
|
||||
ApplicationTransientStatus, DownloadableMetadata, borrow_db_checked, borrow_db_mut_checked,
|
||||
};
|
||||
@@ -22,6 +23,8 @@ use remote::utils::DROP_CLIENT_ASYNC;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::fs::{create_dir_all, remove_file};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
@@ -49,6 +52,7 @@ pub struct DownloadInformation {
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub metadata: DownloadableMetadata,
|
||||
pub configuration: UserConfiguration,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
pub dl_info: Mutex<Option<DownloadInformation>>,
|
||||
pub download_progress: Arc<ProgressObject>,
|
||||
@@ -66,41 +70,33 @@ impl Debug for GameDownloadAgent {
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
pub async fn new_from_index(
|
||||
metadata: DownloadableMetadata,
|
||||
target_download_dir: usize,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
depot_manager: Arc<DepotManager>,
|
||||
) -> Result<Self, ApplicationDownloadError> {
|
||||
let base_dir = {
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
db_lock.applications.install_dirs[target_download_dir].clone()
|
||||
};
|
||||
|
||||
Self::new(metadata, base_dir, sender, depot_manager).await
|
||||
}
|
||||
pub async fn new(
|
||||
metadata: DownloadableMetadata,
|
||||
base_dir: PathBuf,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
depot_manager: Arc<DepotManager>,
|
||||
configuration: UserConfiguration,
|
||||
) -> Result<Self, ApplicationDownloadError> {
|
||||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let game_name = get_cached_object::<Game>(&format!("game/{}", metadata.id)).map(|v| v.library_path).unwrap_or(metadata.id.clone());
|
||||
let game_name = get_cached_object::<Game>(&format!("game/{}", metadata.id))
|
||||
.map(|v| v.library_path)
|
||||
.unwrap_or(metadata.id.clone());
|
||||
|
||||
let base_dir_path = Path::new(&base_dir);
|
||||
info!("base dir {}", base_dir_path.display());
|
||||
let data_base_dir_path = base_dir_path.join(game_name);
|
||||
info!("data dir path {}", data_base_dir_path.display());
|
||||
|
||||
create_dir_all(data_base_dir_path.clone())?;
|
||||
|
||||
let stored_manifest = DropData::generate(
|
||||
metadata.id.clone(),
|
||||
metadata.version.clone(),
|
||||
metadata.target_platform,
|
||||
data_base_dir_path.clone(),
|
||||
configuration.clone(),
|
||||
);
|
||||
|
||||
let result = Self {
|
||||
@@ -123,6 +119,7 @@ impl GameDownloadAgent {
|
||||
dropdata: stored_manifest,
|
||||
status: Mutex::new(DownloadStatus::Queued),
|
||||
depot_manager,
|
||||
configuration,
|
||||
};
|
||||
|
||||
result.ensure_manifest_exists().await?;
|
||||
@@ -141,6 +138,21 @@ impl GameDownloadAgent {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn scan_filetree(&self, path: &Path) -> Result<Vec<PathBuf>, io::Error> {
|
||||
if !path.is_dir() {
|
||||
return Ok(vec![path.into()]);
|
||||
};
|
||||
|
||||
let subdirs = path.read_dir()?;
|
||||
let mut results = Vec::new();
|
||||
for subdir in subdirs {
|
||||
let subdir = subdir?;
|
||||
let subfiles = self.scan_filetree(&subdir.path())?;
|
||||
results.extend(subfiles);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
@@ -199,6 +211,13 @@ impl GameDownloadAgent {
|
||||
&[
|
||||
("id", &self.metadata.id),
|
||||
("version", &self.metadata.version),
|
||||
(
|
||||
"previous",
|
||||
self.dropdata
|
||||
.previously_installed_version
|
||||
.as_ref()
|
||||
.map_or("", |v| v),
|
||||
),
|
||||
],
|
||||
)
|
||||
.map_err(ApplicationDownloadError::Communication)?;
|
||||
@@ -277,13 +296,25 @@ impl GameDownloadAgent {
|
||||
let completed_chunks = lock!(self.dropdata.contexts);
|
||||
completed_chunks.clone()
|
||||
};
|
||||
info!("started with {} existing chunks", completed_chunks.len());
|
||||
let chunk_len = manifests_chunks.iter().map(|v| v.1.len()).sum::<usize>();
|
||||
let mut max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
if max_download_threads <= 0 {
|
||||
if max_download_threads == 0 {
|
||||
max_download_threads = 1;
|
||||
}
|
||||
|
||||
let file_list = &file_list;
|
||||
let base_path = &self.dropdata.base_path;
|
||||
let current_file_tree = self.scan_filetree(base_path)?;
|
||||
|
||||
for file in current_file_tree {
|
||||
let filename = file.strip_prefix(base_path)?.to_string_lossy().to_string();
|
||||
let needed = file_list.contains_key(&filename) || filename == ".dropdata";
|
||||
if !needed {
|
||||
debug!("deleted {}", file.display());
|
||||
remove_file(file)?;
|
||||
}
|
||||
}
|
||||
|
||||
let local_completed_chunks = completed_chunks.clone();
|
||||
|
||||
@@ -299,7 +330,7 @@ impl GameDownloadAgent {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
let mut index = 0;
|
||||
@@ -310,10 +341,8 @@ impl GameDownloadAgent {
|
||||
self.download_progress.get(index),
|
||||
self.download_progress.clone(),
|
||||
);
|
||||
let disk_progress_handle = ProgressHandle::new(
|
||||
self.disk_progress.get(index),
|
||||
self.disk_progress.clone(),
|
||||
);
|
||||
let disk_progress_handle =
|
||||
ProgressHandle::new(self.disk_progress.get(index), self.disk_progress.clone());
|
||||
index += 1;
|
||||
|
||||
let chunk_length = chunk_data.files.iter().map(|v| v.length).sum();
|
||||
@@ -344,7 +373,6 @@ impl GameDownloadAgent {
|
||||
}
|
||||
chunk_completions.push(async move {
|
||||
for i in 0..RETRY_COUNT {
|
||||
let base_path = self.dropdata.base_path.clone();
|
||||
match download_game_chunk(
|
||||
&self.metadata.id,
|
||||
&local_version_id,
|
||||
@@ -503,6 +531,7 @@ impl GameDownloadAgent {
|
||||
&self.metadata(),
|
||||
self.dropdata.base_path.display().to_string(),
|
||||
Some(app_handle),
|
||||
self.configuration.clone(),
|
||||
);
|
||||
|
||||
self.dropdata.write();
|
||||
@@ -573,6 +602,7 @@ impl Downloadable for GameDownloadAgent {
|
||||
async fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
match on_game_complete(
|
||||
&self.metadata(),
|
||||
self.configuration.clone(),
|
||||
self.dropdata.base_path.to_string_lossy().to_string(),
|
||||
app_handle,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fs::{Permissions, set_permissions};
|
||||
use std::io::SeekFrom;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -37,7 +37,7 @@ pub async fn download_game_chunk(
|
||||
key: &[u8; 16],
|
||||
chunk_data: &ChunkData,
|
||||
file_list: &HashMap<String, String>,
|
||||
base_path: PathBuf,
|
||||
base_path: &Path,
|
||||
control_flag: &DownloadThreadControl,
|
||||
// How much we're downloading
|
||||
download_progress: &ProgressHandle,
|
||||
@@ -95,7 +95,7 @@ pub async fn download_game_chunk(
|
||||
|
||||
let stream = response
|
||||
.bytes_stream()
|
||||
.map(|v| v.map_err(|err| std::io::Error::other(err)));
|
||||
.map(|v| v.map_err(std::io::Error::other));
|
||||
let mut stream_reader = StreamReader::new(stream);
|
||||
//let mut stream_reader = response;
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use database::platform::Platform;
|
||||
use database::{models::data::UserConfiguration, platform::Platform};
|
||||
use log::error;
|
||||
use native_model::{Decode, Encode};
|
||||
use utils::lock;
|
||||
|
||||
pub type DropData = v1::DropData;
|
||||
@@ -17,38 +16,73 @@ pub static DROPDATA_PATH: &str = ".dropdata";
|
||||
pub mod v1 {
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
|
||||
|
||||
use database::platform::Platform;
|
||||
use native_model::native_model;
|
||||
use database::{models::data::UserConfiguration, platform::Platform};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
pub struct DropData {
|
||||
pub game_id: String,
|
||||
pub game_version: String,
|
||||
pub target_platform: Platform,
|
||||
#[serde(default)]
|
||||
pub configuration: UserConfiguration,
|
||||
pub contexts: Mutex<HashMap<String, bool>>,
|
||||
pub base_path: PathBuf,
|
||||
pub previously_installed_version: Option<String>,
|
||||
}
|
||||
|
||||
impl DropData {
|
||||
pub fn new(game_id: String, game_version: String, target_platform: Platform, base_path: PathBuf) -> Self {
|
||||
pub fn new(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
target_platform: Platform,
|
||||
base_path: PathBuf,
|
||||
configuration: UserConfiguration,
|
||||
previously_installed_version: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base_path,
|
||||
game_id,
|
||||
game_version,
|
||||
target_platform,
|
||||
contexts: Mutex::new(HashMap::new()),
|
||||
configuration,
|
||||
previously_installed_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DropData {
|
||||
pub fn generate(game_id: String, game_version: String, target_platform: Platform, base_path: PathBuf) -> Self {
|
||||
pub fn generate(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
target_platform: Platform,
|
||||
base_path: PathBuf,
|
||||
configuration: UserConfiguration,
|
||||
) -> Self {
|
||||
match DropData::read(&base_path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => DropData::new(game_id, game_version, target_platform, base_path),
|
||||
Ok(v) => {
|
||||
if v.game_id != game_id || v.game_version != game_version {
|
||||
return DropData::new(
|
||||
game_id,
|
||||
game_version,
|
||||
target_platform,
|
||||
base_path,
|
||||
configuration,
|
||||
Some(v.game_version),
|
||||
);
|
||||
}
|
||||
v
|
||||
}
|
||||
Err(_) => DropData::new(
|
||||
game_id,
|
||||
game_version,
|
||||
target_platform,
|
||||
base_path,
|
||||
configuration,
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
pub fn read(base_path: &Path) -> Result<Self, io::Error> {
|
||||
@@ -57,7 +91,7 @@ impl DropData {
|
||||
let mut s = Vec::new();
|
||||
file.read_to_end(&mut s)?;
|
||||
|
||||
native_model::rmp_serde_1_3::RmpSerde::decode(s).map_err(|e| {
|
||||
pot::from_slice(&s).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Failed to decode drop data: {e}"),
|
||||
@@ -65,7 +99,7 @@ impl DropData {
|
||||
})
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
||||
let manifest_raw = match pot::to_vec(&self) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
@@ -2,12 +2,14 @@ use bitcode::{Decode, Encode};
|
||||
use database::{
|
||||
ApplicationTransientStatus, Database, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||
borrow_db_checked, borrow_db_mut_checked,
|
||||
models::data::{InstalledGameType, UserConfiguration},
|
||||
};
|
||||
use log::{debug, error, warn};
|
||||
use remote::{
|
||||
auth::generate_authorization_header, error::RemoteAccessError, requests::generate_url,
|
||||
utils::DROP_CLIENT_ASYNC,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::remove_dir_all;
|
||||
use std::thread::spawn;
|
||||
@@ -76,8 +78,9 @@ pub fn set_partially_installed(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: Option<&AppHandle>,
|
||||
configuration: UserConfiguration,
|
||||
) {
|
||||
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
|
||||
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle, configuration);
|
||||
}
|
||||
|
||||
pub fn set_partially_installed_db(
|
||||
@@ -85,13 +88,16 @@ pub fn set_partially_installed_db(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: Option<&AppHandle>,
|
||||
configuration: UserConfiguration,
|
||||
) {
|
||||
db_lock.applications.transient_statuses.remove(meta);
|
||||
db_lock.applications.game_statuses.insert(
|
||||
meta.id.clone(),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name: meta.version.clone(),
|
||||
GameDownloadStatus::Installed {
|
||||
install_type: InstalledGameType::PartiallyInstalled { configuration },
|
||||
version_id: meta.version.clone(),
|
||||
install_dir,
|
||||
update_available: false,
|
||||
},
|
||||
);
|
||||
db_lock
|
||||
@@ -135,16 +141,10 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
|
||||
if let Some((_, install_dir)) = match previous_state {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_type: _,
|
||||
version_id: version_name,
|
||||
install_dir,
|
||||
update_available: _,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
} {
|
||||
@@ -197,6 +197,7 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
|
||||
pub async fn on_game_complete(
|
||||
meta: &DownloadableMetadata,
|
||||
configuration: UserConfiguration,
|
||||
install_dir: String,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
@@ -211,7 +212,12 @@ pub async fn on_game_complete(
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let game_version: GameVersion = response.json().await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
||||
}
|
||||
|
||||
let mut game_version: GameVersion = response.json().await?;
|
||||
game_version.user_configuration = configuration;
|
||||
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
@@ -230,16 +236,15 @@ pub async fn on_game_complete(
|
||||
.iter()
|
||||
.find(|v| v.platform == meta.target_platform);
|
||||
|
||||
let status = if setup_configuration.is_none() {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: meta.version.clone(),
|
||||
install_dir,
|
||||
}
|
||||
} else {
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: meta.version.clone(),
|
||||
install_dir,
|
||||
}
|
||||
let status = GameDownloadStatus::Installed {
|
||||
version_id: meta.version.clone(),
|
||||
install_dir,
|
||||
install_type: if setup_configuration.is_none() {
|
||||
InstalledGameType::Installed
|
||||
} else {
|
||||
InstalledGameType::SetupRequired
|
||||
},
|
||||
update_available: false,
|
||||
};
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
@@ -247,10 +252,7 @@ pub async fn on_game_complete(
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.remove(meta);
|
||||
db_handle.applications.transient_statuses.remove(meta);
|
||||
drop(db_handle);
|
||||
app_emit!(
|
||||
app_handle,
|
||||
@@ -273,8 +275,10 @@ pub fn push_game_update(
|
||||
version: Option<GameVersion>,
|
||||
status: GameStatusWithTransient,
|
||||
) {
|
||||
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
|
||||
&status.0
|
||||
if let Some(GameDownloadStatus::Installed {
|
||||
install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired,
|
||||
..
|
||||
}) = &status.0
|
||||
&& version.is_none()
|
||||
{
|
||||
panic!("pushed game for installed game that doesn't have version information");
|
||||
@@ -289,11 +293,4 @@ pub fn push_game_update(
|
||||
version,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FrontendGameOptions {
|
||||
pub launch_string: String,
|
||||
pub override_proton_path: Option<String>,
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,22 @@ pub fn scan_install_dirs() {
|
||||
if !drop_data_file.exists() {
|
||||
continue;
|
||||
}
|
||||
let Ok(drop_data) = DropData::read(&game.path()) else {
|
||||
warn!(
|
||||
".dropdata exists for {}, but couldn't read it. is it corrupted?",
|
||||
game.file_name().display()
|
||||
);
|
||||
continue;
|
||||
let drop_data = match DropData::read(&game.path()) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
".dropdata exists for {}, but couldn't read it. is it corrupted? {:?}",
|
||||
game.file_name().display(),
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if db_lock.applications.game_statuses.contains_key(&drop_data.game_id) {
|
||||
if db_lock
|
||||
.applications
|
||||
.game_statuses
|
||||
.contains_key(&drop_data.game_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -41,6 +49,7 @@ pub fn scan_install_dirs() {
|
||||
&metadata,
|
||||
drop_data.base_path.to_str().unwrap().to_string(),
|
||||
None,
|
||||
drop_data.configuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,53 @@ impl ProcessHandler for NativeGameLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UMULauncher;
|
||||
impl ProcessHandler for UMULauncher {
|
||||
pub struct UMUNativeLauncher;
|
||||
impl ProcessHandler for UMUNativeLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
_database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
let umu_id_override = game_version
|
||||
.launches
|
||||
.iter()
|
||||
.find(|v| v.platform == meta.target_platform)
|
||||
.and_then(|v| v.umu_id_override.as_ref())
|
||||
.map_or("", |v| v);
|
||||
|
||||
let game_id = if umu_id_override.is_empty() {
|
||||
&game_version.version_id
|
||||
} else {
|
||||
umu_id_override
|
||||
};
|
||||
|
||||
let pfx_dir = DATA_ROOT_DIR.join("pfx");
|
||||
let pfx_dir = pfx_dir.join(meta.id.clone());
|
||||
create_dir_all(&pfx_dir)?;
|
||||
|
||||
Ok(format!(
|
||||
"GAMEID={game_id} UMU_NO_PROTON=1 WINEPREFIX={} {umu:?} {launch}",
|
||||
pfx_dir.to_string_lossy(),
|
||||
umu = UMU_LAUNCHER_EXECUTABLE
|
||||
.as_ref()
|
||||
.expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"),
|
||||
launch = launch_command,
|
||||
))
|
||||
}
|
||||
|
||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
||||
let Some(compat_info) = &*COMPAT_INFO else {
|
||||
return false;
|
||||
};
|
||||
compat_info.umu_installed
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UMUCompatLauncher;
|
||||
impl ProcessHandler for UMUCompatLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
@@ -52,39 +97,29 @@ impl ProcessHandler for UMULauncher {
|
||||
let pfx_dir = pfx_dir.join(meta.id.clone());
|
||||
create_dir_all(&pfx_dir)?;
|
||||
|
||||
let no_proton = match meta.target_platform {
|
||||
Platform::Linux => Some("UMU_NO_PROTON=1"),
|
||||
_ => None,
|
||||
};
|
||||
let proton_path = game_version
|
||||
.user_configuration
|
||||
.override_proton_path
|
||||
.as_ref()
|
||||
.or(database.applications.default_proton_path.as_ref())
|
||||
.ok_or(ProcessError::NoCompat)?;
|
||||
|
||||
let proton_env = if no_proton.is_none() {
|
||||
let proton_path = game_version
|
||||
.user_configuration
|
||||
.override_proton_path
|
||||
.as_ref()
|
||||
.or(database.applications.default_proton_path.as_ref())
|
||||
.ok_or(ProcessError::NoCompat)?;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let proton_valid = crate::compat::read_proton_path(PathBuf::from(proton_path))
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let proton_valid = false;
|
||||
if !proton_valid {
|
||||
return Err(ProcessError::NoCompat);
|
||||
}
|
||||
Some(format!("PROTONPATH={}", proton_path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
let proton_valid = crate::compat::read_proton_path(PathBuf::from(proton_path))
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let proton_valid = false;
|
||||
if !proton_valid {
|
||||
return Err(ProcessError::NoCompat);
|
||||
}
|
||||
let proton_env = format!("PROTONPATH={}", proton_path);
|
||||
|
||||
Ok(format!(
|
||||
"GAMEID={game_id} {} WINEPREFIX={} {} {umu:?} {launch}",
|
||||
proton_env.unwrap_or(String::new()),
|
||||
"GAMEID={game_id} {} WINEPREFIX={} {umu:?} {launch}",
|
||||
proton_env,
|
||||
pfx_dir.to_string_lossy(),
|
||||
no_proton.unwrap_or(""),
|
||||
umu = UMU_LAUNCHER_EXECUTABLE
|
||||
.as_ref()
|
||||
.expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"),
|
||||
@@ -110,7 +145,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
||||
current_dir: &str,
|
||||
database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
let umu_launcher = UMULauncher {};
|
||||
let umu_launcher = UMUCompatLauncher {};
|
||||
let umu_string = umu_launcher.create_launch_process(
|
||||
meta,
|
||||
launch_command,
|
||||
|
||||
@@ -11,7 +11,8 @@ use std::{
|
||||
|
||||
use database::{
|
||||
ApplicationTransientStatus, Database, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||
borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR, platform::Platform,
|
||||
borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR, models::data::InstalledGameType,
|
||||
platform::Platform,
|
||||
};
|
||||
use dynfmt::Format;
|
||||
use dynfmt::SimpleCurlyFormat;
|
||||
@@ -26,7 +27,9 @@ use crate::{
|
||||
error::ProcessError,
|
||||
format::DropFormatArgs,
|
||||
parser::{LaunchParameters, ParsedCommand},
|
||||
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
|
||||
process_handlers::{
|
||||
AsahiMuvmLauncher, NativeGameLauncher, UMUCompatLauncher, UMUNativeLauncher,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct RunningProcess {
|
||||
@@ -75,7 +78,7 @@ impl ProcessManager<'_> {
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Linux),
|
||||
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
&UMUNativeLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::macOS, Platform::macOS),
|
||||
@@ -87,7 +90,7 @@ impl ProcessManager<'_> {
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Windows),
|
||||
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
&UMUCompatLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
],
|
||||
app_handle,
|
||||
@@ -145,21 +148,15 @@ impl ProcessManager<'_> {
|
||||
.unwrap_or_else(|| panic!("Could not get installed version of {}", &game_id));
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
|
||||
if let Some(GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
let current_state = db_handle.applications.game_statuses.get_mut(&game_id);
|
||||
if let Some(GameDownloadStatus::Installed {
|
||||
install_type,
|
||||
..
|
||||
}) = current_state
|
||||
&& let Ok(exit_code) = result
|
||||
&& exit_code.success()
|
||||
{
|
||||
db_handle.applications.game_statuses.insert(
|
||||
game_id.clone(),
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: version_name.to_string(),
|
||||
install_dir: install_dir.to_string(),
|
||||
},
|
||||
);
|
||||
*install_type = InstalledGameType::Installed;
|
||||
}
|
||||
|
||||
let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO);
|
||||
@@ -268,12 +265,10 @@ impl ProcessManager<'_> {
|
||||
|
||||
let (version_name, install_dir) = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
version_id: version_name,
|
||||
install_dir,
|
||||
install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired,
|
||||
..
|
||||
} => (version_name, install_dir),
|
||||
_ => return Err(ProcessError::NotInstalled),
|
||||
};
|
||||
@@ -323,8 +318,8 @@ impl ProcessManager<'_> {
|
||||
|
||||
let (target_command, emulator) = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
install_type: InstalledGameType::Installed,
|
||||
..
|
||||
} => {
|
||||
let (_, launch_config) = game_version
|
||||
.launches
|
||||
@@ -338,9 +333,9 @@ impl ProcessManager<'_> {
|
||||
launch_config.emulator.as_ref(),
|
||||
)
|
||||
}
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
GameDownloadStatus::Installed {
|
||||
install_type: InstalledGameType::SetupRequired,
|
||||
..
|
||||
} => {
|
||||
let setup_config = game_version
|
||||
.setups
|
||||
@@ -375,12 +370,12 @@ impl ProcessManager<'_> {
|
||||
|
||||
let emulator_install_dir = match emulator_game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
install_type: InstalledGameType::Installed,
|
||||
..
|
||||
} => Ok(install_dir),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
GameDownloadStatus::Installed {
|
||||
install_type: InstalledGameType::SetupRequired,
|
||||
..
|
||||
} => todo!(),
|
||||
_ => Err(err.clone()),
|
||||
}?;
|
||||
@@ -407,8 +402,6 @@ impl ProcessManager<'_> {
|
||||
*v = v.replace("{rom}", &target_command.command);
|
||||
});
|
||||
|
||||
|
||||
|
||||
process_handler.create_launch_process(
|
||||
emulator_metadata,
|
||||
exe_command.reconstruct(),
|
||||
@@ -417,8 +410,6 @@ impl ProcessManager<'_> {
|
||||
&db_lock,
|
||||
)?
|
||||
} else {
|
||||
|
||||
|
||||
process_handler.create_launch_process(
|
||||
&meta,
|
||||
target_command.reconstruct(),
|
||||
@@ -474,9 +465,10 @@ impl ProcessManager<'_> {
|
||||
.map(|e| e.split("=").map(|v| v.to_string()).collect::<Vec<String>>())
|
||||
{
|
||||
if let Some(key) = parts.first()
|
||||
&& let Some(value) = parts.get(1) {
|
||||
command.env(key, value);
|
||||
}
|
||||
&& let Some(value) = parts.get(1)
|
||||
{
|
||||
command.env(key, value);
|
||||
}
|
||||
}
|
||||
command
|
||||
};
|
||||
|
||||
@@ -49,28 +49,21 @@ impl Middleware for AutoOfflineMiddleware {
|
||||
match res {
|
||||
Ok(res) => {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let lock = DROP_APP_HANDLE.try_lock();
|
||||
if let Ok(lock) = lock {
|
||||
if let Some(app_handle) = &*lock {
|
||||
let state = app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
|
||||
let state_lock = state.try_lock();
|
||||
if let Ok(mut state_lock) = state_lock {
|
||||
if state_lock.status == AppStatus::Offline {
|
||||
state_lock.status = AppStatus::SignedIn;
|
||||
app_handle
|
||||
.emit("update_state", &*state_lock)
|
||||
.expect("failed to emit state update");
|
||||
}
|
||||
} else {
|
||||
warn!("failed to lock app state - {}", url.as_str());
|
||||
let lock = DROP_APP_HANDLE.lock().await;
|
||||
if let Some(app_handle) = &*lock {
|
||||
let state = app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
|
||||
let state_lock = state.try_lock();
|
||||
if let Ok(mut state_lock) = state_lock {
|
||||
if state_lock.status == AppStatus::Offline {
|
||||
state_lock.status = AppStatus::SignedIn;
|
||||
app_handle
|
||||
.emit("update_state", &*state_lock)
|
||||
.expect("failed to emit state update");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
warn!(
|
||||
"failed to lock app handle for offline/online middleware - {}",
|
||||
url.as_str()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!("failed to lock app state - {}", url.as_str());
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::nonpoison::Mutex;
|
||||
|
||||
use client::app_state::AppState;
|
||||
use database::{GameDownloadStatus, borrow_db_checked};
|
||||
use database::{GameDownloadStatus, borrow_db_checked, models::data::InstalledGameType};
|
||||
use games::collections::collection::Collections;
|
||||
use remote::{
|
||||
cache::{cache_object, get_cached_object},
|
||||
@@ -57,7 +57,7 @@ pub async fn fetch_collections_offline(
|
||||
.game_statuses
|
||||
.get(&v.game_id)
|
||||
.unwrap_or(&GameDownloadStatus::Remote {}),
|
||||
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
|
||||
GameDownloadStatus::Installed { install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired, .. }
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
+35
-23
@@ -1,7 +1,9 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use database::{
|
||||
DownloadType, DownloadableMetadata, GameDownloadStatus, borrow_db_checked, platform::Platform,
|
||||
DownloadType, DownloadableMetadata, GameDownloadStatus, borrow_db_checked,
|
||||
models::data::{InstalledGameType, UserConfiguration},
|
||||
platform::Platform,
|
||||
};
|
||||
use download_manager::{
|
||||
DOWNLOAD_MANAGER, downloadable::Downloadable, error::ApplicationDownloadError,
|
||||
@@ -14,20 +16,8 @@ pub async fn download_game(
|
||||
version_id: String,
|
||||
target_platform: Platform,
|
||||
install_dir: usize,
|
||||
enable_updates: bool,
|
||||
) -> Result<(), ApplicationDownloadError> {
|
||||
{
|
||||
let db = borrow_db_checked();
|
||||
let status = db
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&game_id)
|
||||
.unwrap_or(&GameDownloadStatus::Remote {});
|
||||
|
||||
if matches!(status, GameDownloadStatus::Installed { .. }) {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let sender = { DOWNLOAD_MANAGER.get_sender().clone() };
|
||||
|
||||
let meta = DownloadableMetadata {
|
||||
@@ -37,11 +27,32 @@ pub async fn download_game(
|
||||
download_type: DownloadType::Game,
|
||||
};
|
||||
|
||||
let game_download_agent = GameDownloadAgent::new_from_index(
|
||||
{
|
||||
let db = borrow_db_checked();
|
||||
let status = db.applications.transient_statuses.get(&meta);
|
||||
|
||||
if status.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let configuration = UserConfiguration {
|
||||
enable_updates,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let base_dir = {
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
db_lock.applications.install_dirs[install_dir].clone()
|
||||
};
|
||||
|
||||
let game_download_agent = GameDownloadAgent::new(
|
||||
meta,
|
||||
install_dir,
|
||||
base_dir,
|
||||
sender,
|
||||
DOWNLOAD_MANAGER.clone_depot_manager(),
|
||||
configuration,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -58,7 +69,7 @@ pub async fn download_game(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn resume_download(game_id: String) -> Result<(), ApplicationDownloadError> {
|
||||
let (meta, install_dir) = {
|
||||
let (meta, (install_dir, configuration)) = {
|
||||
let db_lock = borrow_db_checked();
|
||||
let status = db_lock
|
||||
.applications
|
||||
@@ -75,12 +86,12 @@ pub async fn resume_download(game_id: String) -> Result<(), ApplicationDownloadE
|
||||
.clone();
|
||||
|
||||
let install_dir = match status {
|
||||
GameDownloadStatus::Remote {} => Err(ApplicationDownloadError::InvalidCommand),
|
||||
GameDownloadStatus::SetupRequired { .. } => {
|
||||
Err(ApplicationDownloadError::InvalidCommand)
|
||||
}
|
||||
GameDownloadStatus::Installed { .. } => Err(ApplicationDownloadError::InvalidCommand),
|
||||
GameDownloadStatus::PartiallyInstalled { install_dir, .. } => Ok(install_dir),
|
||||
GameDownloadStatus::Installed {
|
||||
install_type: InstalledGameType::PartiallyInstalled { configuration },
|
||||
install_dir,
|
||||
..
|
||||
} => Ok((install_dir, configuration)),
|
||||
_ => Err(ApplicationDownloadError::InvalidCommand),
|
||||
}?;
|
||||
(meta, install_dir)
|
||||
};
|
||||
@@ -98,6 +109,7 @@ pub async fn resume_download(game_id: String) -> Result<(), ApplicationDownloadE
|
||||
install_dir.to_path_buf(),
|
||||
sender,
|
||||
DOWNLOAD_MANAGER.clone_depot_manager(),
|
||||
configuration,
|
||||
)
|
||||
.await?,
|
||||
) as Box<dyn Downloadable + Send + Sync>);
|
||||
|
||||
+26
-26
@@ -3,12 +3,12 @@ use std::sync::nonpoison::Mutex;
|
||||
use bitcode::{Decode, Encode};
|
||||
use database::{
|
||||
DownloadableMetadata, GameDownloadStatus, borrow_db_checked, borrow_db_mut_checked,
|
||||
platform::Platform,
|
||||
models::data::{InstalledGameType, UserConfiguration}, platform::Platform,
|
||||
};
|
||||
use games::{
|
||||
collections::collection::Collection,
|
||||
downloads::error::LibraryError,
|
||||
library::{FetchGameStruct, FrontendGameOptions, Game, get_current_meta, uninstall_game_logic},
|
||||
library::{FetchGameStruct, Game, get_current_meta, uninstall_game_logic},
|
||||
state::{GameStatusManager, GameStatusWithTransient},
|
||||
};
|
||||
use log::warn;
|
||||
@@ -168,25 +168,20 @@ pub async fn fetch_library_logic_offline(
|
||||
.game_statuses
|
||||
.get(game.id())
|
||||
.unwrap_or(&GameDownloadStatus::Remote {}),
|
||||
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
|
||||
GameDownloadStatus::Installed {
|
||||
install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired,
|
||||
..
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
response.library.retain(retain_filter);
|
||||
response.other.retain(retain_filter);
|
||||
response.missing.retain(retain_filter);
|
||||
response.collections.iter_mut().for_each(|k| {
|
||||
k.entries.retain(|object| {
|
||||
matches!(
|
||||
&db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(object.game.id())
|
||||
.unwrap_or(&GameDownloadStatus::Remote {}),
|
||||
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
|
||||
)
|
||||
})
|
||||
});
|
||||
response
|
||||
.collections
|
||||
.iter_mut()
|
||||
.for_each(|k| k.entries.retain(|object| retain_filter(&object.game)));
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
@@ -272,11 +267,11 @@ struct VersionDownloadOptionRequiredContent {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VersionDownloadOption {
|
||||
game_id: String,
|
||||
version_id: String,
|
||||
pub game_id: String,
|
||||
pub version_id: String,
|
||||
display_name: Option<String>,
|
||||
version_path: String,
|
||||
platform: Platform,
|
||||
pub platform: Platform,
|
||||
size: GameSize,
|
||||
required_content: Vec<VersionDownloadOptionRequiredContent>,
|
||||
}
|
||||
@@ -293,7 +288,16 @@ pub async fn fetch_game_version_options_logic(
|
||||
) -> Result<Vec<VersionDownloadOption>, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
|
||||
let response = generate_url(&["/api/v1/client/game", &game_id, "versions"], &[])?;
|
||||
let previous_id = borrow_db_checked()
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(&game_id)
|
||||
.map(|v| v.version.clone());
|
||||
|
||||
let response = generate_url(
|
||||
&["/api/v1/client/game", &game_id, "versions"],
|
||||
&[("previous", &previous_id.unwrap_or(String::new()))],
|
||||
)?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
@@ -310,7 +314,7 @@ pub async fn fetch_game_version_options_logic(
|
||||
|
||||
let state_lock = state.lock();
|
||||
let process_manager_lock = PROCESS_MANAGER.lock();
|
||||
let data = data
|
||||
let data: Vec<VersionDownloadOption> = data
|
||||
.into_iter()
|
||||
.filter(|v| process_manager_lock.valid_platform(&v.platform))
|
||||
.collect();
|
||||
@@ -387,7 +391,7 @@ pub async fn fetch_game_version_options(
|
||||
#[tauri::command]
|
||||
pub fn update_game_configuration(
|
||||
game_id: String,
|
||||
options: FrontendGameOptions,
|
||||
options: UserConfiguration,
|
||||
) -> Result<(), LibraryError> {
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let installed_version = handle
|
||||
@@ -406,11 +410,7 @@ pub fn update_game_configuration(
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
// Add more options in here
|
||||
existing_configuration.user_configuration.launch_template = options.launch_string;
|
||||
existing_configuration.user_configuration.override_proton_path = options.override_proton_path;
|
||||
|
||||
// Add no more options past here
|
||||
existing_configuration.user_configuration = options;
|
||||
|
||||
handle
|
||||
.applications
|
||||
|
||||
+10
-17
@@ -57,7 +57,9 @@ mod downloads;
|
||||
mod games;
|
||||
mod process;
|
||||
mod remote;
|
||||
mod scheduler;
|
||||
mod settings;
|
||||
mod updates;
|
||||
|
||||
use client::*;
|
||||
use download_manager::*;
|
||||
@@ -67,6 +69,8 @@ use process::*;
|
||||
use remote::*;
|
||||
use settings::*;
|
||||
|
||||
use crate::scheduler::scheduler_task;
|
||||
|
||||
async fn setup(handle: AppHandle) -> AppState {
|
||||
let logfile = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(
|
||||
@@ -101,6 +105,9 @@ async fn setup(handle: AppHandle) -> AppState {
|
||||
ProcessManagerWrapper::init(handle.clone());
|
||||
DownloadManagerWrapper::init(handle.clone());
|
||||
|
||||
debug!("checking if database is set up");
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let umu_state = UmuState::NotNeeded;
|
||||
|
||||
@@ -110,9 +117,6 @@ async fn setup(handle: AppHandle) -> AppState {
|
||||
false => UmuState::NotInstalled,
|
||||
};
|
||||
|
||||
debug!("checking if database is set up");
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
|
||||
scan_install_dirs();
|
||||
|
||||
if !is_set_up {
|
||||
@@ -136,20 +140,7 @@ async fn setup(handle: AppHandle) -> AppState {
|
||||
for (game_id, status) in statuses {
|
||||
match status {
|
||||
GameDownloadStatus::Remote {} => {}
|
||||
GameDownloadStatus::PartiallyInstalled { .. } => {}
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
} => {
|
||||
let install_dir_path = Path::new(&install_dir);
|
||||
if !install_dir_path.exists() {
|
||||
missing_games.push(game_id);
|
||||
}
|
||||
}
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
} => {
|
||||
GameDownloadStatus::Installed { install_dir, .. } => {
|
||||
let install_dir_path = Path::new(&install_dir);
|
||||
if !install_dir_path.exists() {
|
||||
missing_games.push(game_id);
|
||||
@@ -416,6 +407,8 @@ pub fn run() {
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
|
||||
tokio::spawn(async move { scheduler_task().await });
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::{time::Duration};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::warn;
|
||||
use tokio::time;
|
||||
|
||||
use crate::updates::GameUpdater;
|
||||
|
||||
#[async_trait]
|
||||
pub trait ScheduleTask {
|
||||
/// Returns how many minutes between calls
|
||||
fn timeframe(&mut self) -> usize;
|
||||
async fn call(&mut self) -> Result<(), anyhow::Error>;
|
||||
}
|
||||
|
||||
struct TaskData {
|
||||
task: Box<dyn ScheduleTask + Send + Sync>,
|
||||
updates_since_call: usize,
|
||||
}
|
||||
|
||||
pub async fn scheduler_task() -> ! {
|
||||
let mut interval = time::interval(Duration::from_mins(1));
|
||||
interval.tick().await;
|
||||
|
||||
let mut tasks = vec![TaskData {
|
||||
task: Box::new(GameUpdater::new()),
|
||||
updates_since_call: usize::MAX - 1,
|
||||
}];
|
||||
|
||||
loop {
|
||||
for task in &mut tasks {
|
||||
task.updates_since_call += 1;
|
||||
if task.task.timeframe() <= task.updates_since_call {
|
||||
let result = task.task.call().await;
|
||||
if let Err(err) = result {
|
||||
warn!("background task returned error: {err:?}");
|
||||
}
|
||||
task.updates_since_call = 0;
|
||||
}
|
||||
}
|
||||
interval.tick().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use std::sync::nonpoison::Mutex;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use client::{app_state::AppState, app_status::AppStatus};
|
||||
use database::{
|
||||
GameDownloadStatus, GameVersion, borrow_db_checked, borrow_db_mut_checked,
|
||||
};
|
||||
use log::warn;
|
||||
use process::PROCESS_MANAGER;
|
||||
use remote::utils::DROP_APP_HANDLE;
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::{
|
||||
games::{VersionDownloadOption, fetch_game_version_options},
|
||||
scheduler::ScheduleTask,
|
||||
};
|
||||
|
||||
pub struct GameUpdater {
|
||||
no_internet: bool,
|
||||
}
|
||||
|
||||
impl GameUpdater {
|
||||
pub fn new() -> Self {
|
||||
GameUpdater { no_internet: false }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This implementation is kinda inefficient because we can't hold the locks across await boundaries,
|
||||
which means we constantly lock and unlock certain objects. It doesn't matter though, because this
|
||||
doesn't have to be fast.
|
||||
*/
|
||||
#[async_trait]
|
||||
impl ScheduleTask for GameUpdater {
|
||||
fn timeframe(&mut self) -> usize {
|
||||
if self.no_internet { 5 } else { 30 }
|
||||
}
|
||||
|
||||
async fn call(&mut self) -> Result<(), anyhow::Error> {
|
||||
let app_handle = DROP_APP_HANDLE.lock().await;
|
||||
let app_handle = app_handle
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("game update task ran before setup"))?;
|
||||
let state = app_handle.state::<Mutex<AppState>>();
|
||||
{
|
||||
let state_lock = state.lock();
|
||||
if state_lock.status == AppStatus::Offline {
|
||||
self.no_internet = true;
|
||||
return Ok(());
|
||||
};
|
||||
};
|
||||
|
||||
self.no_internet = false;
|
||||
|
||||
let to_check: Vec<GameVersion> = {
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
|
||||
|
||||
db_lock
|
||||
.applications
|
||||
.game_statuses
|
||||
.values()
|
||||
.map(|v| match v {
|
||||
GameDownloadStatus::Installed { version_id, .. } => Some(version_id),
|
||||
_ => None,
|
||||
})
|
||||
.map(|v| {
|
||||
v.and_then(|version_id| db_lock.applications.game_versions.get(version_id))
|
||||
})
|
||||
.filter(|v| {
|
||||
v.map(|v| v.user_configuration.enable_updates)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|v| v.cloned().unwrap())
|
||||
.collect()
|
||||
};
|
||||
|
||||
for version in to_check {
|
||||
let version_options =
|
||||
match fetch_game_version_options(version.game_id.clone(), state.clone()).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"failed to check for update for game id {}: {:?}",
|
||||
version.game_id, err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let process_manager_lock = PROCESS_MANAGER.lock();
|
||||
let valid_options: Vec<VersionDownloadOption> = version_options
|
||||
.into_iter()
|
||||
.filter(|v| process_manager_lock.valid_platform(&v.platform))
|
||||
.collect();
|
||||
|
||||
let latest = match valid_options.first() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
warn!("found no versions for game id: {}", version.game_id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
let game_status = db_lock
|
||||
.applications
|
||||
.game_statuses
|
||||
.get_mut(&version.game_id)
|
||||
.ok_or(anyhow::anyhow!(""))?;
|
||||
|
||||
if let GameDownloadStatus::Installed {
|
||||
update_available, ..
|
||||
} = game_status {
|
||||
*update_available = latest.version_id != version.version_id;
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user