mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-06-22 04:11:37 +10:00
Async downloader, better Proton support (#183)
* feat: async downloader + other fixes * feat: windows command parsing + use library path for install path * feat: better proton support * feat: style fixes and store button now uses in-app * feat: emulator rename + umu emulator fix * feat: bring process creation inline with docs * fix: clippy
This commit is contained in:
+1
-1
Submodule libs/drop-base updated: 04125e89be...dad3487be6
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { StarIcon } from "@heroicons/vue/24/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const props = defineProps<{
|
||||
path?: string;
|
||||
}>();
|
||||
|
||||
const model = defineModel<string | undefined>({ required: true });
|
||||
|
||||
const isDefault = computed(() => props.path == model.value);
|
||||
|
||||
async function setDefault() {
|
||||
if (!props.path) return;
|
||||
await invoke("set_default", { path: props.path });
|
||||
model.value = props.path;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="['p-0.5 rounded-full', isDefault ? 'bg-blue-500' : 'bg-zinc-800']"
|
||||
@click="setDefault"
|
||||
:disabled="!props.path"
|
||||
>
|
||||
<StarIcon
|
||||
:class="['size-[0.7rem]', isDefault ? 'text-zinc-100' : 'text-zinc-100']"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
@@ -11,7 +11,7 @@
|
||||
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline-1 -outline-offset-1 outline-zinc-800 placeholder:text-zinc-400 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
placeholder="{}"
|
||||
aria-describedby="launch-description"
|
||||
v-model="model!!.launchString"
|
||||
v-model="model.launchString"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400" id="launch-description">
|
||||
@@ -21,11 +21,203 @@
|
||||
>Leaving it blank will cause the game not to start.</span
|
||||
>
|
||||
</p>
|
||||
|
||||
<Listbox
|
||||
v-if="props.protonEnabled"
|
||||
as="div"
|
||||
v-model="model.overrideProtonPath"
|
||||
class="mt-6"
|
||||
>
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-white"
|
||||
>Proton override</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="grid w-full cursor-default grid-cols-1 rounded-md bg-white/5 py-1.5 pr-2 pl-3 text-left text-white outline-1 -outline-offset-1 outline-white/10 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-500 sm:text-sm/6"
|
||||
>
|
||||
<span
|
||||
v-if="currentProtonPath"
|
||||
class="col-start-1 row-start-1 truncate pr-6"
|
||||
>{{ currentProtonPath.name }} ({{ currentProtonPath.path }})</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="col-start-1 row-start-1 truncate pr-6 italic text-zinc-400"
|
||||
>No override configured</span
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="col-start-1 row-start-1 size-5 self-center justify-self-end text-zinc-400 sm:size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class=""
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-800 py-1 text-base outline-1 -outline-offset-1 outline-white/10 sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
:value="undefined"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active
|
||||
? 'bg-blue-500 text-white outline-hidden'
|
||||
: 'text-white',
|
||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate italic',
|
||||
]"
|
||||
>Use global default</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-400',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
<h1 class="text-white text-sm font-semibold bg-zinc-900 py-2 px-2">
|
||||
Auto-discovered
|
||||
</h1>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-if="protonPaths.autodiscovered.length > 0"
|
||||
v-for="proton in protonPaths.autodiscovered"
|
||||
:key="proton.path"
|
||||
:value="proton.path"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active
|
||||
? 'bg-blue-500 text-white outline-hidden'
|
||||
: 'text-white',
|
||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ proton.name }} ({{ proton.path }})</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-400',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
<li v-else class="italic text-zinc-400 py-2 pr-9 pl-3"
|
||||
>No auto-discovered layers.</li
|
||||
>
|
||||
<h1 class="text-white text-sm font-semibold bg-zinc-900 py-2 px-2">
|
||||
Manually added
|
||||
</h1>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-if="protonPaths.custom.length > 0"
|
||||
v-for="proton in protonPaths.custom"
|
||||
:key="proton.path"
|
||||
:value="proton.path"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active
|
||||
? 'bg-blue-500 text-white outline-hidden'
|
||||
: 'text-white',
|
||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ proton.name }} ({{ proton.path }})</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-400',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
<li v-else class="italic text-zinc-400 py-2 pr-9 pl-3"
|
||||
>No manually added layers.</li
|
||||
>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400" id="launch-description">
|
||||
Override the Proton layer used to launch this game. You can add or
|
||||
remove your custom Proton layer paths in
|
||||
<PageWidget to="/settings/compat">
|
||||
<WrenchIcon class="size-3" />
|
||||
Settings </PageWidget
|
||||
>.
|
||||
</p>
|
||||
</Listbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FrontendGameConfiguration } from "~/composables/game";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { FrontendGameConfiguration, ProtonPath } from "~/composables/game";
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
||||
import { CheckIcon } from "@heroicons/vue/20/solid";
|
||||
import { WrenchIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const model = defineModel<FrontendGameConfiguration>();
|
||||
const model = defineModel<FrontendGameConfiguration>({ required: true });
|
||||
|
||||
const props = defineProps<{
|
||||
protonEnabled: boolean;
|
||||
}>();
|
||||
|
||||
const protonPaths = await invoke<{
|
||||
autodiscovered: ProtonPath[];
|
||||
custom: ProtonPath[];
|
||||
default?: string;
|
||||
}>("fetch_proton_paths");
|
||||
const currentProtonPath = computed(
|
||||
() =>
|
||||
protonPaths.autodiscovered.find(
|
||||
(v) => v.path == model.value.overrideProtonPath,
|
||||
) ??
|
||||
protonPaths.custom.find((v) => v.path == model.value.overrideProtonPath),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<component
|
||||
v-model="configuration"
|
||||
:is="tabs[currentTabIndex]?.page"
|
||||
:proton-enabled="protonEnabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,14 +83,24 @@ import Launch from "./GameOptions/Launch.vue";
|
||||
import type { FrontendGameConfiguration } from "~/composables/game";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const appState = useAppState();
|
||||
|
||||
const open = defineModel<boolean>();
|
||||
const props = defineProps<{ gameId: string }>();
|
||||
const game = await useGame(props.gameId);
|
||||
|
||||
const configuration: Ref<FrontendGameConfiguration> = ref({
|
||||
launchString: game.version!!.launchCommandTemplate,
|
||||
launchString: game.version!.userConfiguration.launchTemplate,
|
||||
overrideProtonPath: game.version!.userConfiguration.overrideProtonPath,
|
||||
});
|
||||
|
||||
const hasWindows = !!(
|
||||
game.version!.setups.find((v) => v.platform === "Windows") ??
|
||||
game.version!.launches.find((v) => v.platform === "Windows")
|
||||
);
|
||||
|
||||
const protonEnabled = !!(appState.value!.umuState !== "NotNeeded" && hasWindows);
|
||||
|
||||
const tabs: Array<{ name: string; icon: Component; page: Component }> = [
|
||||
{
|
||||
name: "Launch",
|
||||
@@ -108,12 +119,14 @@ const saveLoading = ref(false);
|
||||
const saveError = ref<undefined | string>();
|
||||
async function save() {
|
||||
saveLoading.value = true;
|
||||
saveError.value = undefined;
|
||||
try {
|
||||
await invoke("update_game_configuration", {
|
||||
gameId: game.game.id,
|
||||
options: configuration.value,
|
||||
});
|
||||
open.value = false;
|
||||
saveError.value = undefined;
|
||||
} catch (e) {
|
||||
saveError.value = (e as unknown as string).toString();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
/>
|
||||
<div class="inline-flex items-center">
|
||||
<ol class="inline-flex gap-3">
|
||||
<HeaderProtonSupportWidget />
|
||||
<HeaderQueueWidget :object="currentQueueObject" />
|
||||
<li v-for="(item, itemIdx) in quickActions">
|
||||
<HeaderWidget
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<NuxtLink to="/settings/compat">
|
||||
<HeaderWidget
|
||||
v-if="appState && appState.umuState !== 'NotNeeded'"
|
||||
:problem="notInstalled"
|
||||
>
|
||||
<img src="/proton-logo.png" class="relative z-50 size-5 brightness-[30%]" />
|
||||
</HeaderWidget>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const appState = useAppState();
|
||||
|
||||
const notInstalled = appState.value?.umuState === "NotInstalled";
|
||||
</script>
|
||||
@@ -18,9 +18,9 @@ const props = defineProps<{ object?: QueueState["queue"][0] }>();
|
||||
]"
|
||||
/>
|
||||
<div
|
||||
v-if="props.object?.progress"
|
||||
v-if="props.object?.dl_progress"
|
||||
class="transition-all absolute left-0 top-0 bottom-0 bg-blue-600 z-10"
|
||||
:style="{ width: `${props.object.progress * 99 + 1}%` }"
|
||||
:style="{ width: `${props.object.dl_progress * 99 + 1}%` }"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
<template>
|
||||
<div class="transition inline-flex items-center cursor-pointer rounded-sm px-4 py-1.5 bg-zinc-900 text-zinc-600 hover:bg-zinc-800 hover:text-zinc-300 relative">
|
||||
<slot />
|
||||
<div v-if="props.notifications !== undefined"
|
||||
class="text-zinc-900 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] text-xs bg-blue-300 rounded-full w-3.5 h-3.5 text-center">
|
||||
{{ props.notifications }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'transition inline-flex items-center cursor-pointer rounded-sm px-4 py-1.5 text-zinc-600 hover:text-zinc-300 relative',
|
||||
props.notifications !== undefined
|
||||
? 'bg-blue-400'
|
||||
: props.problem !== undefined && props.problem
|
||||
? 'bg-red-400'
|
||||
: 'bg-zinc-900 hover:bg-zinc-800',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
<div
|
||||
v-if="props.notifications !== undefined"
|
||||
class="text-zinc-900 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] text-xs bg-blue-400 rounded-full w-3.5 h-3.5 text-center"
|
||||
>
|
||||
{{ props.notifications }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="props.problem !== undefined && props.problem"
|
||||
class="text-zinc-100 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] text-sm bg-red-400 rounded-full w-5 h-5 text-center"
|
||||
>
|
||||
!
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
notifications?: number
|
||||
notifications?: number;
|
||||
problem?: boolean;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5 h-full">
|
||||
<TransitionGroup
|
||||
name="list"
|
||||
tag="ul"
|
||||
class="flex flex-col gap-y-1.5 h-full"
|
||||
>
|
||||
<Disclosure
|
||||
as="div"
|
||||
v-for="(nav, navIndex) in filteredNavigation"
|
||||
@@ -46,7 +50,10 @@
|
||||
<span class="ml-6 relative flex size-4">
|
||||
<MinusIcon class="absolute inset-0 size-4" aria-hidden="true" />
|
||||
<MinusIcon
|
||||
:class="[ !open ? 'rotate-90' : 'rotate-0', 'transition-all absolute inset-0 size-4']"
|
||||
:class="[
|
||||
!open ? 'rotate-90' : 'rotate-0',
|
||||
'transition-all absolute inset-0 size-4',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
@@ -196,6 +203,7 @@ type FetchLibraryResponse = {
|
||||
library: Game[];
|
||||
collections: Collection[];
|
||||
other: Game[];
|
||||
missing: Game[];
|
||||
};
|
||||
|
||||
async function calculateGamesLogic(clearAll = false, forceRefresh = false) {
|
||||
@@ -215,6 +223,7 @@ async function calculateGamesLogic(clearAll = false, forceRefresh = false) {
|
||||
.flat()
|
||||
.map((e) => e.game),
|
||||
...library.other,
|
||||
...library.missing,
|
||||
].filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
for (const game of allGames) {
|
||||
@@ -237,11 +246,20 @@ async function calculateGamesLogic(clearAll = false, forceRefresh = false) {
|
||||
entries: library.other.map((v) => ({ gameId: v.id, game: v })),
|
||||
} satisfies Collection;
|
||||
|
||||
const missingCollection = {
|
||||
id: "missing",
|
||||
name: "Delisted",
|
||||
isDefault: false,
|
||||
isTools: true,
|
||||
entries: library.missing.map((v) => ({ gameId: v.id, game: v })),
|
||||
};
|
||||
|
||||
loading.value = false;
|
||||
collections.value = [
|
||||
libraryCollection,
|
||||
...library.collections,
|
||||
...(library.other.length > 0 ? [otherCollection] : []),
|
||||
...(library.missing.length > 0 ? [missingCollection] : []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -259,24 +277,25 @@ await new Promise<void>((r) => {
|
||||
|
||||
const navigation = computed(() =>
|
||||
collections.value.map((collection) => {
|
||||
const items = collection.entries.map(({ game }) => {
|
||||
const status = games[game.id].status;
|
||||
const items = collection.entries
|
||||
.map(({ game }) => {
|
||||
const status = games[game.id].status;
|
||||
|
||||
const isInstalled = computed(
|
||||
() => status.value.type != GameStatusEnum.Remote,
|
||||
);
|
||||
const isInstalled = computed(
|
||||
() => status.value.type != GameStatusEnum.Remote,
|
||||
);
|
||||
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
icon: game.mIconObjectId,
|
||||
isInstalled,
|
||||
id: game.id,
|
||||
type: game.type,
|
||||
};
|
||||
return item;
|
||||
});
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
icon: game.mIconObjectId,
|
||||
isInstalled,
|
||||
id: game.id,
|
||||
type: game.type,
|
||||
};
|
||||
return item;
|
||||
});
|
||||
|
||||
return {
|
||||
id: collection.id,
|
||||
|
||||
@@ -10,7 +10,7 @@ const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
|
||||
type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } };
|
||||
export type SerializedGameStatus = [
|
||||
{ type: GameStatusEnum },
|
||||
OptionGameStatus | null
|
||||
OptionGameStatus | null,
|
||||
];
|
||||
|
||||
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||
@@ -70,6 +70,7 @@ export const useGame = async (gameId: string) => {
|
||||
|
||||
export type FrontendGameConfiguration = {
|
||||
launchString: string;
|
||||
overrideProtonPath?: string;
|
||||
};
|
||||
|
||||
export type LaunchResult =
|
||||
@@ -81,13 +82,24 @@ export type VersionOption = {
|
||||
displayName?: string;
|
||||
versionPath: string;
|
||||
platform: string;
|
||||
size: number;
|
||||
size: {
|
||||
installSize: number;
|
||||
downloadSize: number;
|
||||
};
|
||||
requiredContent: Array<{
|
||||
gameId: string;
|
||||
versionId: string;
|
||||
name: string;
|
||||
iconObjectId: string;
|
||||
shortDescription: string;
|
||||
size: number;
|
||||
size: {
|
||||
installSize: number;
|
||||
downloadSize: number;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ProtonPath = {
|
||||
path: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -14,6 +14,7 @@
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||
"koa": "^2.16.1",
|
||||
|
||||
@@ -35,15 +35,18 @@
|
||||
@resume="() => resumeDownload()"
|
||||
:status="status"
|
||||
/>
|
||||
<a
|
||||
:href="remoteUrl"
|
||||
target="_blank"
|
||||
type="button"
|
||||
<NuxtLink
|
||||
class="transition-transform duration-300 hover:scale-105 active:scale-95 inline-flex items-center rounded-md bg-zinc-800/50 px-6 font-semibold text-white shadow-xl backdrop-blur-sm hover:bg-zinc-800/80 uppercase font-display"
|
||||
:to="{
|
||||
path: '/store',
|
||||
query: {
|
||||
gameId: game.id,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
|
||||
Store
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +174,11 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div v-if="versionOptions && versionOptions.length > 0 && currentVersionOption">
|
||||
<div
|
||||
v-if="
|
||||
versionOptions && versionOptions.length > 0 && currentVersionOption
|
||||
"
|
||||
>
|
||||
<Listbox as="div" v-model="installVersionIndex">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Version</ListboxLabel
|
||||
@@ -188,7 +195,7 @@
|
||||
on
|
||||
{{ currentVersionOption.platform }} ({{
|
||||
formatKilobytes(
|
||||
currentVersionOption.size / 1024
|
||||
currentVersionOption.size.installSize / 1024,
|
||||
)
|
||||
}}B)</span
|
||||
>
|
||||
@@ -233,7 +240,8 @@
|
||||
>{{ version.displayName || version.versionPath }} on
|
||||
{{ version.platform }} ({{
|
||||
formatKilobytes(
|
||||
versionOptions[installVersionIndex].size / 1024
|
||||
versionOptions[installVersionIndex].size
|
||||
.installSize / 1024,
|
||||
)
|
||||
}}B)</span
|
||||
>
|
||||
@@ -314,8 +322,7 @@
|
||||
</div>
|
||||
<ul role="list" class="mt-2 divide-y divide-white/5">
|
||||
<li
|
||||
v-for="content in currentVersionOption
|
||||
.requiredContent"
|
||||
v-for="content in currentVersionOption.requiredContent"
|
||||
:key="content.versionId"
|
||||
:class="[
|
||||
!installDepsDisabled[content.versionId]
|
||||
@@ -353,7 +360,7 @@
|
||||
<p
|
||||
class="inline-flex items-center gap-x-1 text-xs/5 text-gray-400"
|
||||
>
|
||||
{{ formatKilobytes(content.size / 1024) }}B
|
||||
{{ formatKilobytes(content.size.installSize / 1024) }}B
|
||||
<ServerIcon class="size-3" />
|
||||
</p>
|
||||
</div>
|
||||
@@ -566,10 +573,6 @@ const id = route.params.id.toString();
|
||||
const { game: rawGame, status } = await useGame(id);
|
||||
const game = ref(rawGame);
|
||||
|
||||
const remoteUrl: string = await invoke("gen_drop_url", {
|
||||
path: `/store/${game.value.id}`,
|
||||
});
|
||||
|
||||
const bannerUrl = await useObject(game.value.mBannerObjectId);
|
||||
|
||||
// Get all available images
|
||||
@@ -577,7 +580,7 @@ const mediaUrls = await Promise.all(
|
||||
game.value.mImageCarouselObjectIds.map(async (v) => {
|
||||
const src = await useObject(v);
|
||||
return src;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const htmlDescription = micromark(game.value.mDescription);
|
||||
@@ -611,7 +614,9 @@ const installVersionIndex = ref(0);
|
||||
const installDir = ref(0);
|
||||
const installDepsDisabled = ref<{ [key: string]: boolean }>({});
|
||||
|
||||
const currentVersionOption = computed(() => versionOptions.value?.[installVersionIndex.value]);
|
||||
const currentVersionOption = computed(
|
||||
() => versionOptions.value?.[installVersionIndex.value],
|
||||
);
|
||||
async function install() {
|
||||
try {
|
||||
if (!versionOptions.value) throw new Error("Versions have not been loaded");
|
||||
@@ -661,7 +666,7 @@ async function launch() {
|
||||
try {
|
||||
const fetchedLaunchOptions = await invoke<Array<{ name: string }>>(
|
||||
"get_launch_options",
|
||||
{ id: game.value.id }
|
||||
{ id: game.value.id },
|
||||
);
|
||||
if (fetchedLaunchOptions.length == 1) {
|
||||
await launchIndex(0);
|
||||
@@ -676,7 +681,7 @@ async function launch() {
|
||||
description: `Drop failed to launch "${game.value.mName}": ${e}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
(e, c) => c(),
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
@@ -707,7 +712,7 @@ async function launchIndex(index: number) {
|
||||
description: `Drop failed to launch "${game.value.mName}": ${e}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
(e, c) => c(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -731,7 +736,7 @@ async function kill() {
|
||||
description: `Drop failed to stop "${game.value.mName}": ${e}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
(e, c) => c(),
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
+54
-29
@@ -9,18 +9,25 @@
|
||||
<nav class="flex flex-col" aria-label="Sidebar">
|
||||
<ul role="list" class="-mx-2 space-y-1">
|
||||
<li v-for="(item, itemIdx) in navigation" :key="item.prefix">
|
||||
<NuxtLink :href="item.route" :class="[
|
||||
itemIdx === currentNavigation
|
||||
? 'bg-zinc-800/50 text-zinc-100'
|
||||
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
|
||||
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
|
||||
]">
|
||||
<component :is="item.icon" :class="[
|
||||
<NuxtLink
|
||||
:href="item.route"
|
||||
:class="[
|
||||
itemIdx === currentNavigation
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 group-hover:text-zinc-200',
|
||||
'transition h-6 w-6 shrink-0',
|
||||
]" aria-hidden="true" />
|
||||
? 'bg-zinc-800/50 text-zinc-100'
|
||||
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
|
||||
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
:class="[
|
||||
itemIdx === currentNavigation
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 group-hover:text-zinc-200',
|
||||
'transition h-6 w-6 shrink-0',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
@@ -43,7 +50,7 @@ import {
|
||||
} from "@heroicons/vue/16/solid";
|
||||
import type { Component } from "vue";
|
||||
import type { NavigationItem } from "~/types";
|
||||
import { platform } from '@tauri-apps/plugin-os';
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { UserIcon } from "@heroicons/vue/20/solid";
|
||||
|
||||
@@ -57,25 +64,28 @@ const systemData = await invoke<{
|
||||
const isDebugMode = ref(systemData.logLevel.toLowerCase() === "debug");
|
||||
const debugRevealed = ref(false);
|
||||
|
||||
const appState = useAppState();
|
||||
|
||||
// Track shift key state and debug reveal
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Shift') {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Shift") {
|
||||
isDebugMode.value = true;
|
||||
debugRevealed.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Shift') {
|
||||
isDebugMode.value = debugRevealed.value || systemData.logLevel.toLowerCase() === "debug";
|
||||
|
||||
window.addEventListener("keyup", (e) => {
|
||||
if (e.key === "Shift") {
|
||||
isDebugMode.value =
|
||||
debugRevealed.value || systemData.logLevel.toLowerCase() === "debug";
|
||||
}
|
||||
});
|
||||
|
||||
// Reset debug reveal when leaving the settings page
|
||||
const router = useRouter();
|
||||
router.beforeEach((to) => {
|
||||
if (!to.path.startsWith('/settings')) {
|
||||
if (!to.path.startsWith("/settings")) {
|
||||
debugRevealed.value = false;
|
||||
isDebugMode.value = systemData.logLevel.toLowerCase() === "debug";
|
||||
}
|
||||
@@ -91,7 +101,7 @@ const navigation = computed(() => [
|
||||
icon: HomeIcon,
|
||||
},
|
||||
{
|
||||
label: "Interface",
|
||||
label: "Interface",
|
||||
route: "/settings/interface",
|
||||
prefix: "/settings/interface",
|
||||
icon: RectangleGroupIcon,
|
||||
@@ -102,27 +112,42 @@ const navigation = computed(() => [
|
||||
prefix: "/settings/downloads",
|
||||
icon: ArrowDownTrayIcon,
|
||||
},
|
||||
...(appState.value!.umuState !== "NotNeeded"
|
||||
? [
|
||||
{
|
||||
label: "Proton",
|
||||
route: "/settings/compat",
|
||||
prefix: "/settings/compat",
|
||||
icon: h("img", { src: "/proton-logo.png" }),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "Account",
|
||||
route: "/settings/account",
|
||||
prefix: "/settings/account",
|
||||
icon: UserIcon
|
||||
icon: UserIcon,
|
||||
},
|
||||
...(isDebugMode.value ? [{
|
||||
label: "Debug Info",
|
||||
route: "/settings/debug",
|
||||
prefix: "/settings/debug",
|
||||
icon: BugAntIcon,
|
||||
}] : []),
|
||||
...(isDebugMode.value
|
||||
? [
|
||||
{
|
||||
label: "Debug Info",
|
||||
route: "/settings/debug",
|
||||
prefix: "/settings/debug",
|
||||
icon: BugAntIcon,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
|
||||
const currentPlatform = platform();
|
||||
|
||||
// Use .value to unwrap the computed ref
|
||||
const {currentNavigation} = useCurrentNavigationIndex(navigation.value);
|
||||
const { currentNavigation } = useCurrentNavigationIndex(navigation.value);
|
||||
|
||||
// Watch for navigation changes and update currentPageIndex
|
||||
watch(navigation, (newNav) => {
|
||||
currentNavigation.value = useCurrentNavigationIndex(newNav).currentNavigation.value;
|
||||
currentNavigation.value =
|
||||
useCurrentNavigationIndex(newNav).currentNavigation.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div class="border-b border-zinc-700 py-5">
|
||||
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
|
||||
Proton Compatibility Layer
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="appState!.umuState === 'Installed'"
|
||||
class="rounded-md bg-green-500/10 p-4 outline outline-green-500/20"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<CheckCircleIcon class="size-5 text-green-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-green-200">
|
||||
UMU Launcher installed
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-green-200/85">
|
||||
<p>
|
||||
The necessary component to use the Proton Compatibility Layer is
|
||||
installed, and detected.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="rounded-md bg-red-500/15 p-4 outline outline-red-500/25">
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<XCircleIcon class="size-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-200">
|
||||
UMU Launcher not installed
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-200/80">
|
||||
<p>
|
||||
You will be unable to install or run games designed for Windows
|
||||
until you install UMU Launcher and restart Drop.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!paths.data.value?.default" class="mt-4 rounded-md bg-yellow-500/15 p-4 outline outline-yellow-500/25">
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<ExclamationTriangleIcon class="size-5 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-200">
|
||||
No default Proton layer
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-yellow-200/80">
|
||||
<p>
|
||||
You won't be able to launch any Windows games without overriding their Proton layer in game settings. Please select a default layer below using the stars.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="paths.data.value"
|
||||
class="mt-4 text-zinc-100 gap-x-2 inline-flex p-4 w-full items-center justify-center font-bold"
|
||||
>
|
||||
<DefaultProtonButton v-model="paths.data.value.default" />
|
||||
= Default Proton Layer
|
||||
</div>
|
||||
|
||||
<!-- autodiscovered table -->
|
||||
<div class="mt-2">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-base font-semibold text-white">
|
||||
Auto-discovered Proton Layers
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-gray-300">
|
||||
All auto-discovered Proton Layers from common paths on your system.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<table class="relative min-w-full divide-y divide-white/15">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-white sm:pl-0"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-white"
|
||||
>
|
||||
Path
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/10">
|
||||
<tr
|
||||
v-for="path in paths.data.value?.autodiscovered"
|
||||
:key="path.path"
|
||||
>
|
||||
<td
|
||||
class="py-4 pr-3 pl-4 text-sm font-medium inline-flex items-center gap-x-2 whitespace-nowrap text-white sm:pl-0"
|
||||
>
|
||||
<DefaultProtonButton
|
||||
:path="path.path"
|
||||
v-model="paths.data.value!.default"
|
||||
/>
|
||||
{{ path.name }}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
|
||||
{{ path.path }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- custom table -->
|
||||
<div class="mt-8">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-base font-semibold text-white">Manual Proton Layers</h1>
|
||||
<p class="mt-2 text-sm text-gray-300">
|
||||
Add or remove custom Proton compatible layers for your games. We
|
||||
recommend
|
||||
<a
|
||||
href="https://github.com/DavidoTek/ProtonUp-Qt"
|
||||
target="_blank"
|
||||
class="text-blue-400 hover:text-blue-500"
|
||||
>ProtonUp-Qt</a
|
||||
>
|
||||
to download and manage your proton layers.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-gray-300">
|
||||
Note: deleting a custom Proton layer will
|
||||
<span class="font-bold">not</span> clear it from manually selected
|
||||
Proton layers.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<button
|
||||
@click="pickLayerModal = true"
|
||||
type="button"
|
||||
class="block rounded-md bg-blue-500 px-3 py-2 text-center text-sm font-semibold text-white shadow-xs hover:bg-blue-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
|
||||
>
|
||||
Add layer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<table class="relative min-w-full divide-y divide-white/15">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-white sm:pl-0"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-white"
|
||||
>
|
||||
Path
|
||||
</th>
|
||||
<th scope="col" class="py-3.5 pr-4 pl-3 sm:pr-0">
|
||||
<span class="sr-only">Delete</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/10">
|
||||
<tr
|
||||
v-for="(path, pathIdx) in paths.data.value?.custom"
|
||||
:key="path.path"
|
||||
>
|
||||
<td
|
||||
class="py-4 pr-3 pl-4 text-sm font-medium inline-flex items-center gap-x-2 whitespace-nowrap text-white sm:pl-0"
|
||||
>
|
||||
<DefaultProtonButton
|
||||
:path="path.path"
|
||||
v-model="paths.data.value!.default"
|
||||
/>
|
||||
{{ path.name }}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
|
||||
{{ path.path }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 pr-4 pl-3 text-right text-sm font-medium whitespace-nowrap sm:pr-0"
|
||||
>
|
||||
<button
|
||||
@click="() => deleteCustom(pathIdx)"
|
||||
class="text-red-400 hover:text-red-300"
|
||||
>
|
||||
Delete<span class="sr-only"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalTemplate v-model="pickLayerModal">
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<h3 class="text-base font-semibold text-zinc-100">
|
||||
Select your Proton layer
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Select the path to your Proton layer. It should have at least two
|
||||
files, one named "proton" and one named "compatibilitytool.vdf",
|
||||
for Drop to recognise it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-zinc-950 ring-2 ring-zinc-800 rounded-lg text-sm">
|
||||
<span v-if="path" class="text-zinc-100">{{ path }}</span>
|
||||
<span v-else class="italic text-zinc-400">No path selected.</span>
|
||||
</div>
|
||||
|
||||
<LoadingButton :loading="false" @click="pickLayer"
|
||||
>Select path</LoadingButton
|
||||
>
|
||||
|
||||
<div
|
||||
v-if="pickError"
|
||||
class="rounded-md bg-red-500/15 p-4 outline outline-red-500/25"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<XCircleIcon class="size-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-200">
|
||||
{{ pickError }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<LoadingButton
|
||||
@click="() => add()"
|
||||
:loading="false"
|
||||
:disabled="!path"
|
||||
type="submit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
>
|
||||
Add
|
||||
</LoadingButton>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="cancel"
|
||||
ref="cancelButtonRef"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon, XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/vue/24/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
const appState = useAppState();
|
||||
|
||||
const paths = useAsyncData<{
|
||||
autodiscovered: ProtonPath[];
|
||||
custom: ProtonPath[];
|
||||
default?: string;
|
||||
}>("proton_paths", async () => await invoke("fetch_proton_paths"));
|
||||
|
||||
const pickLayerModal = ref(false);
|
||||
const pickError = ref<string | null>(null);
|
||||
|
||||
const path = ref<string | null>(null);
|
||||
|
||||
async function pickLayer() {
|
||||
const file = await open({
|
||||
multiple: false,
|
||||
directory: true,
|
||||
canCreateDirectories: true,
|
||||
});
|
||||
path.value = file;
|
||||
pickError.value = null;
|
||||
}
|
||||
|
||||
async function add() {
|
||||
if (!path.value) return;
|
||||
pickError.value = null;
|
||||
try {
|
||||
await invoke("add_proton_layer", { path: path.value });
|
||||
path.value = null;
|
||||
pickLayerModal.value = false;
|
||||
paths.refresh();
|
||||
} catch (e) {
|
||||
pickError.value = (e as string).toString();
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
pickLayerModal.value = false;
|
||||
path.value = null;
|
||||
}
|
||||
|
||||
async function deleteCustom(index: number) {
|
||||
if (!paths.data.value) return;
|
||||
await invoke("remove_proton_layer", { index });
|
||||
const deleted = paths.data.value.custom.splice(index);
|
||||
if (paths.data.value.default == deleted[0].path) {
|
||||
paths.data.value.default = undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,11 +2,15 @@
|
||||
<iframe :src="convertedStoreUrl" class="grow w-full h-full" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BuildingStorefrontIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
|
||||
const convertedStoreUrl = convertFileSrc("store", "server");
|
||||
const route = useRoute();
|
||||
|
||||
const gameId = route.query.gameId?.toString();
|
||||
|
||||
// This is necessary because convertFileSrc encodes the URI
|
||||
const convertedStoreUrl = convertFileSrc(`dummyvalue`, "server").replace(
|
||||
"dummyvalue",
|
||||
gameId ? `store/${gameId}` : "store",
|
||||
);
|
||||
</script>
|
||||
|
||||
Generated
+53
@@ -20,6 +20,9 @@ importers:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.9.1
|
||||
version: 2.9.1
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
'@tauri-apps/plugin-os':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
@@ -565,36 +568,42 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-minify/binding-linux-arm64-musl@0.96.0':
|
||||
resolution: {integrity: sha512-rNqoFWOWaxwMmUY5fspd/h5HfvgUlA3sv9CUdA2MpnHFiyoJNovR7WU8tGh+Yn0qOAs0SNH0a05gIthHig14IA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-minify/binding-linux-riscv64-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-3paajIuzGnukHwSI3YBjYVqbd72pZd8NJxaayaNFR0AByIm8rmIT5RqFXbq8j2uhtpmNdZRXiu0em1zOmIScWA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-minify/binding-linux-s390x-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-9ESrpkB2XG0lQ89JlsxlZa86iQCOs+jkDZLl6O+u5wb7ynUy21bpJJ1joauCOSYIOUlSy3+LbtJLiqi7oSQt5Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-minify/binding-linux-x64-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-UMM1jkns+p+WwwmdjC5giI3SfR2BCTga18x3C0cAu6vDVf4W37uTZeTtSIGmwatTBbgiq++Te24/DE0oCdm1iQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-minify/binding-linux-x64-musl@0.96.0':
|
||||
resolution: {integrity: sha512-8b1naiC7MdP7xeMi7cQ5tb9W1rZAP9Qz/jBRqp1Y5EOZ1yhSGnf1QWuZ/0pCc+XiB9vEHXEY3Aki/H+86m2eOg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-minify/binding-wasm32-wasi@0.96.0':
|
||||
resolution: {integrity: sha512-bjGDjkGzo3GWU9Vg2qiFUrfoo5QxojPNV/2RHTlbIB5FWkkV4ExVjsfyqihFiAuj0NXIZqd2SAiEq9htVd3RFw==}
|
||||
@@ -654,36 +663,42 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.96.0':
|
||||
resolution: {integrity: sha512-fjDPbZjkqaDSTBe0FM8nZ9zBw4B/NF/I0gH7CfvNDwIj9smISaNFypYeomkvubORpnbX9ORhvhYwg3TxQ60OGA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-59KAHd/6/LmjkdSAuJn0piKmwSavMasWNUKuYLX/UnqI5KkGIp14+LBwwaBG6KzOtIq1NrRCnmlL4XSEaNkzTg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-VtupojtgahY8XmLwpVpM3C1WQEgMD1JxpB8lzUtdSLwosWaaz1EAl+VXWNuxTTZusNuLBtmR+F0qql22ISi/9g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-8XSY9aUYY+5I4I1mhSEWmYqdUrJi3J5cCAInvEVHyTnDAPkhb+tnLGVZD696TpW+lFOLrTFF2V5GMWJVafqIUA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.96.0':
|
||||
resolution: {integrity: sha512-IIVNtqhA0uxKkD8Y6aZinKO/sOD5O62VlduE54FnUU2rzZEszrZQLL8nMGVZhTdPaKW5M1aeLmjcdnOs6er1Jg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-wasm32-wasi@0.96.0':
|
||||
resolution: {integrity: sha512-TJ/sNPbVD4u6kUwm7sDKa5iRDEB8vd7ZIMjYqFrrAo9US1RGYOSvt6Ie9sDRekUL9fZhNsykvSrpmIj6dg/C2w==}
|
||||
@@ -746,36 +761,42 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-arm64-musl@0.96.0':
|
||||
resolution: {integrity: sha512-EiG/L3wEkPgTm4p906ufptyblBgtiQWTubGg/JEw82f8uLRroayr5zhbUqx40EgH037a3SfJthIyLZi7XPRFJw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-transform/binding-linux-riscv64-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-r01CY6OxKGtVeYnvH4mGmtkQMlLkXdPWWNXwo5o7fE2s/fgZPMpqh8bAuXEhuMXipZRJrjxTk1+ZQ4KCHpMn3Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-s390x-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-4djg2vYLGbVeS8YiA2K4RPPpZE4fxTGCX5g/bOMbCYyirDbmBAIop4eOAj8vOA9i1CcWbDtmp+PVJ1dSw7f3IQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-x64-gnu@0.96.0':
|
||||
resolution: {integrity: sha512-f6pcWVz57Y8jXa2OS7cz3aRNuks34Q3j61+3nQ4xTE8H1KbalcEvHNmM92OEddaJ8QLs9YcE0kUC6eDTbY34+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-x64-musl@0.96.0':
|
||||
resolution: {integrity: sha512-NSiRtFvR7Pbhv3mWyPMkTK38czIjcnK0+K5STo3CuzZRVbX1TM17zGdHzKBUHZu7v6IQ6/XsQ3ELa1BlEHPGWQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-transform/binding-wasm32-wasi@0.96.0':
|
||||
resolution: {integrity: sha512-A91ARLiuZHGN4hBds9s7bW3czUuLuHLsV+cz44iF9j8e1zX9m2hNGXf/acQRbg/zcFUXmjz5nmk8EkZyob876w==}
|
||||
@@ -823,36 +844,42 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-wasm@2.5.1':
|
||||
resolution: {integrity: sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw==}
|
||||
@@ -1010,56 +1037,67 @@ packages:
|
||||
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
|
||||
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.3':
|
||||
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
|
||||
@@ -1118,6 +1156,9 @@ packages:
|
||||
'@tauri-apps/api@2.9.1':
|
||||
resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==}
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.6.0':
|
||||
resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
|
||||
|
||||
'@tauri-apps/plugin-os@2.3.2':
|
||||
resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==}
|
||||
|
||||
@@ -3223,48 +3264,56 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: glibc
|
||||
|
||||
sass-embedded-linux-arm@1.93.3:
|
||||
resolution: {integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: glibc
|
||||
|
||||
sass-embedded-linux-musl-arm64@1.93.3:
|
||||
resolution: {integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: musl
|
||||
|
||||
sass-embedded-linux-musl-arm@1.93.3:
|
||||
resolution: {integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: musl
|
||||
|
||||
sass-embedded-linux-musl-riscv64@1.93.3:
|
||||
resolution: {integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: musl
|
||||
|
||||
sass-embedded-linux-musl-x64@1.93.3:
|
||||
resolution: {integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: musl
|
||||
|
||||
sass-embedded-linux-riscv64@1.93.3:
|
||||
resolution: {integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: glibc
|
||||
|
||||
sass-embedded-linux-x64@1.93.3:
|
||||
resolution: {integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: glibc
|
||||
|
||||
sass-embedded-unknown-all@1.93.3:
|
||||
resolution: {integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==}
|
||||
@@ -5051,6 +5100,10 @@ snapshots:
|
||||
|
||||
'@tauri-apps/api@2.9.1': {}
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.6.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.9.1
|
||||
|
||||
'@tauri-apps/plugin-os@2.3.2':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.9.1
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -7,6 +7,7 @@ export default {
|
||||
"./plugins/**/*.{js,ts}",
|
||||
"./app.vue",
|
||||
"./error.vue",
|
||||
"../libs/drop-base/**/*.{js,vue,ts}"
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
+9
-1
@@ -20,8 +20,11 @@ export type User = {
|
||||
profilePictureObjectId: string;
|
||||
};
|
||||
|
||||
type UmuState = "Installed" | "NotInstalled" | "NotNeeded";
|
||||
|
||||
export type AppState = {
|
||||
status: AppStatus;
|
||||
umuState: UmuState;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
@@ -47,7 +50,12 @@ export type Collection = {
|
||||
};
|
||||
|
||||
export type GameVersion = {
|
||||
launchCommandTemplate: string;
|
||||
userConfiguration: {
|
||||
launchTemplate: string;
|
||||
overrideProtonPath: string;
|
||||
};
|
||||
setups: Array<{ platform: string }>;
|
||||
launches: Array<{ platform: string }>;
|
||||
};
|
||||
|
||||
export enum AppStatus {
|
||||
|
||||
Generated
+26
@@ -3148,6 +3148,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyvalues-parser"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b1d591f0c9482810347586bd2ae842bbd1fc4e0849283c611244930ff44e90f"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "known-folders"
|
||||
version = "1.4.0"
|
||||
@@ -4328,6 +4337,16 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.8.0"
|
||||
@@ -4688,6 +4707,7 @@ dependencies = [
|
||||
"database",
|
||||
"dynfmt",
|
||||
"games",
|
||||
"keyvalues-parser",
|
||||
"log",
|
||||
"page_size",
|
||||
"serde",
|
||||
@@ -7245,6 +7265,12 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
|
||||
@@ -2,9 +2,17 @@ use serde::Serialize;
|
||||
|
||||
use crate::{app_status::AppStatus, user::User};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub enum UmuState {
|
||||
NotNeeded,
|
||||
NotInstalled,
|
||||
Installed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppState {
|
||||
pub status: AppStatus,
|
||||
pub user: Option<User>
|
||||
}
|
||||
pub user: Option<User>,
|
||||
pub umu_state: UmuState,
|
||||
}
|
||||
|
||||
@@ -184,6 +184,8 @@ fn handle_invalid_database(
|
||||
e
|
||||
)
|
||||
});
|
||||
fs::remove_dir_all(cache_dir.clone())?;
|
||||
fs::create_dir_all(cache_dir.clone())?;
|
||||
|
||||
let db = Database::new(games_base_dir, Some(new_path), cache_dir);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ pub mod data {
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum DatabaseVersionEnum {
|
||||
V1 { database: v1::Database },
|
||||
V0_4_0 { database: v1::Database },
|
||||
}
|
||||
|
||||
pub struct DatabaseVersionSerializable(pub(crate) Database);
|
||||
@@ -47,7 +47,7 @@ pub mod data {
|
||||
S: serde::Serializer,
|
||||
{
|
||||
// Always serialize to latest version
|
||||
DatabaseVersionEnum::V1 {
|
||||
DatabaseVersionEnum::V0_4_0 {
|
||||
database: self.0.clone(),
|
||||
}
|
||||
.serialize(serializer)
|
||||
@@ -60,7 +60,7 @@ pub mod data {
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(match DatabaseVersionEnum::deserialize(deserializer)? {
|
||||
DatabaseVersionEnum::V1 { database } => DatabaseVersionSerializable(database),
|
||||
DatabaseVersionEnum::V0_4_0 { database } => DatabaseVersionSerializable(database),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,18 @@ pub mod data {
|
||||
|
||||
use super::{Deserialize, Serialize};
|
||||
|
||||
fn default_template() -> String {
|
||||
"{}".to_owned()
|
||||
fn default_template() -> UserConfiguration {
|
||||
UserConfiguration {
|
||||
launch_template: "{}".to_owned(),
|
||||
override_proton_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserConfiguration {
|
||||
pub launch_template: String,
|
||||
pub override_proton_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -92,7 +102,7 @@ pub mod data {
|
||||
pub delta: bool,
|
||||
|
||||
#[serde(default = "default_template")]
|
||||
pub launch_template: String,
|
||||
pub user_configuration: UserConfiguration,
|
||||
|
||||
pub launches: Vec<LaunchConfiguration>,
|
||||
pub setups: Vec<SetupConfiguration>,
|
||||
@@ -108,7 +118,7 @@ pub mod data {
|
||||
pub platform: Platform,
|
||||
pub umu_id_override: Option<String>,
|
||||
|
||||
pub executor: Option<LaunchConfigurationExecutor>,
|
||||
pub emulator: Option<LaunchConfigurationEmulator>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -116,7 +126,7 @@ pub mod data {
|
||||
/**
|
||||
* This is intended to be used to look up the actual launch configuration that we store elsewhere
|
||||
*/
|
||||
pub struct LaunchConfigurationExecutor {
|
||||
pub struct LaunchConfigurationEmulator {
|
||||
pub launch_id: String,
|
||||
pub game_id: String,
|
||||
pub version_id: String,
|
||||
@@ -227,6 +237,9 @@ pub mod data {
|
||||
pub game_versions: HashMap<String, GameVersion>,
|
||||
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
||||
|
||||
pub additional_proton_paths: Vec<String>,
|
||||
pub default_proton_path: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
|
||||
}
|
||||
@@ -258,6 +271,8 @@ pub mod data {
|
||||
game_versions: HashMap::new(),
|
||||
installed_game_version: HashMap::new(),
|
||||
transient_statuses: HashMap::new(),
|
||||
additional_proton_paths: Vec::new(),
|
||||
default_proton_path: None,
|
||||
},
|
||||
prev_database,
|
||||
base_url: String::new(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::RwLock,
|
||||
time::{Duration, Instant},
|
||||
time::{Duration, Instant}, usize,
|
||||
};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
@@ -48,6 +48,12 @@ struct ServersideDepot {
|
||||
|
||||
const SPEEDTEST_TIMEOUT: Duration = Duration::from_secs(4);
|
||||
|
||||
impl Default for DepotManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DepotManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -77,11 +83,7 @@ impl DepotManager {
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed().as_millis() as usize;
|
||||
let speed = if elapsed == 0 {
|
||||
usize::MAX
|
||||
} else {
|
||||
(total_length / elapsed) * 1000
|
||||
};
|
||||
let speed = total_length.checked_div(elapsed).unwrap_or(usize::MAX);
|
||||
depot.latest_speed.replace(speed);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
download_manager_frontend::DownloadStatus,
|
||||
error::ApplicationDownloadError,
|
||||
frontend_updates::{
|
||||
DiskStatsUpdateEvent, DownloadStatsUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData,
|
||||
DownloadStatsUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -80,3 +80,9 @@ impl From<io::Error> for ApplicationDownloadError {
|
||||
ApplicationDownloadError::IoError(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RemoteAccessError> for ApplicationDownloadError {
|
||||
fn from(value: RemoteAccessError) -> Self {
|
||||
ApplicationDownloadError::Communication(value)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
Arc, LazyLock, Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Instant,
|
||||
@@ -10,7 +10,7 @@ use atomic_instant_full::AtomicInstant;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use utils::{lock, send};
|
||||
|
||||
use crate::{download_manager_frontend::DownloadManagerSignal, util::progress_object};
|
||||
use crate::download_manager_frontend::DownloadManagerSignal;
|
||||
|
||||
use super::rolling_progress_updates::RollingProgressWindow;
|
||||
|
||||
@@ -27,8 +27,6 @@ pub struct ProgressObject {
|
||||
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
|
||||
start: Arc<Mutex<Instant>>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
//last_update: Arc<RwLock<Instant>>,
|
||||
last_update_time: Arc<AtomicInstant>,
|
||||
bytes_last_update: Arc<AtomicUsize>,
|
||||
rolling: RollingProgressWindow<1000>,
|
||||
}
|
||||
@@ -39,6 +37,8 @@ pub struct ProgressHandle {
|
||||
progress_object: Arc<ProgressObject>,
|
||||
}
|
||||
|
||||
static LAST_UPDATE_TIME: LazyLock<AtomicInstant> = LazyLock::new(AtomicInstant::now);
|
||||
|
||||
impl ProgressHandle {
|
||||
pub fn new(progress: Arc<AtomicUsize>, progress_object: Arc<ProgressObject>) -> Self {
|
||||
Self {
|
||||
@@ -79,7 +79,6 @@ impl ProgressObject {
|
||||
start: Arc::new(Mutex::new(Instant::now())),
|
||||
sender,
|
||||
|
||||
last_update_time: Arc::new(AtomicInstant::now()),
|
||||
bytes_last_update: Arc::new(AtomicUsize::new(0)),
|
||||
rolling: RollingProgressWindow::new(),
|
||||
progress_type,
|
||||
@@ -125,7 +124,7 @@ impl ProgressObject {
|
||||
}
|
||||
|
||||
pub fn spawn_update(progress: &Arc<ProgressObject>) {
|
||||
let last_update_time = progress.last_update_time.load(Ordering::SeqCst);
|
||||
let last_update_time = LAST_UPDATE_TIME.load(Ordering::SeqCst);
|
||||
let time_since_last_update = Instant::now()
|
||||
.duration_since(last_update_time)
|
||||
.as_millis_f64();
|
||||
@@ -136,9 +135,7 @@ pub fn spawn_update(progress: &Arc<ProgressObject>) {
|
||||
}
|
||||
|
||||
pub async fn calculate_update(progress: Arc<ProgressObject>, time_since_last_update: f64) {
|
||||
progress
|
||||
.last_update_time
|
||||
.swap(Instant::now(), Ordering::SeqCst);
|
||||
LAST_UPDATE_TIME.swap(Instant::now(), Ordering::SeqCst);
|
||||
|
||||
let current_bytes_downloaded = progress.sum();
|
||||
let max = progress.get_max();
|
||||
|
||||
@@ -4,6 +4,12 @@ pub struct SyncSemaphore {
|
||||
inner: Arc<AtomicUsize>
|
||||
}
|
||||
|
||||
impl Default for SyncSemaphore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncSemaphore {
|
||||
pub fn new() -> Self {
|
||||
Self { inner: Arc::new(AtomicUsize::new(0)) }
|
||||
|
||||
@@ -11,15 +11,17 @@ use download_manager::util::download_thread_control_flag::{
|
||||
};
|
||||
use download_manager::util::progress_object::{ProgressHandle, ProgressObject, ProgressType};
|
||||
use droplet_rs::manifest::{ChunkData, Manifest};
|
||||
use futures_util::StreamExt;
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use log::{debug, error, info, warn};
|
||||
use remote::auth::generate_authorization_header;
|
||||
use remote::cache::get_cached_object;
|
||||
use remote::error::RemoteAccessError;
|
||||
use remote::requests::generate_url;
|
||||
use remote::utils::DROP_CLIENT_ASYNC;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
@@ -28,7 +30,7 @@ use tokio::sync::mpsc::Sender;
|
||||
use utils::{app_emit, lock, send};
|
||||
|
||||
use crate::downloads::utils::get_disk_available;
|
||||
use crate::library::{on_game_complete, push_game_update, set_partially_installed};
|
||||
use crate::library::{Game, on_game_complete, push_game_update, set_partially_installed};
|
||||
use crate::state::GameStatusManager;
|
||||
|
||||
use super::download_logic::download_game_chunk;
|
||||
@@ -87,9 +89,11 @@ impl GameDownloadAgent {
|
||||
// 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 base_dir_path = Path::new(&base_dir);
|
||||
info!("base dir {}", base_dir_path.display());
|
||||
let data_base_dir_path = base_dir_path.join(metadata.id.clone());
|
||||
let data_base_dir_path = base_dir_path.join(game_name);
|
||||
info!("data dir path {}", data_base_dir_path.display());
|
||||
|
||||
let stored_manifest = DropData::generate(
|
||||
@@ -166,10 +170,7 @@ impl GameDownloadAgent {
|
||||
|
||||
info!("beginning download for {}...", self.metadata().id);
|
||||
|
||||
let res = self
|
||||
.run()
|
||||
.await
|
||||
.map_err(ApplicationDownloadError::Communication);
|
||||
let res = self.run().await;
|
||||
|
||||
debug!(
|
||||
"{} took {}ms to download",
|
||||
@@ -244,17 +245,16 @@ impl GameDownloadAgent {
|
||||
|
||||
self.download_progress
|
||||
.set_max(dl_info.download_size.try_into().unwrap());
|
||||
self.download_progress
|
||||
.set_size(total_chunks);
|
||||
self.download_progress.set_size(total_chunks);
|
||||
self.download_progress.reset();
|
||||
|
||||
self.disk_progress.set_max(dl_info.install_size.try_into().unwrap());
|
||||
self.disk_progress
|
||||
.set_size(total_chunks);
|
||||
.set_max(dl_info.install_size.try_into().unwrap());
|
||||
self.disk_progress.set_size(total_chunks);
|
||||
self.disk_progress.reset();
|
||||
}
|
||||
|
||||
async fn run(&self) -> Result<bool, RemoteAccessError> {
|
||||
async fn run(&self) -> Result<bool, ApplicationDownloadError> {
|
||||
self.depot_manager.sync_depots().await?;
|
||||
info!("synced depots");
|
||||
self.setup_progress();
|
||||
@@ -278,124 +278,119 @@ impl GameDownloadAgent {
|
||||
completed_chunks.clone()
|
||||
};
|
||||
let chunk_len = manifests_chunks.iter().map(|v| v.1.len()).sum::<usize>();
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
let mut max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
if max_download_threads <= 0 {
|
||||
max_download_threads = 1;
|
||||
}
|
||||
|
||||
let (sender, recv) = crossbeam_channel::bounded(16);
|
||||
|
||||
// SAFETY: I pinky-promise
|
||||
// (the scope keeps these in scope)
|
||||
let unsafe_self: &'static GameDownloadAgent = unsafe { mem::transmute(self) };
|
||||
let file_list: &'static HashMap<String, String> = unsafe { mem::transmute(&file_list) };
|
||||
let file_list = &file_list;
|
||||
|
||||
let local_completed_chunks = completed_chunks.clone();
|
||||
|
||||
let download_join_handle = tauri::async_runtime::spawn_blocking(move || {
|
||||
let thread_pool = rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(max_download_threads)
|
||||
.build()
|
||||
.unwrap();
|
||||
thread_pool.scope(move |s| {
|
||||
let mut index = 0;
|
||||
for (version_id, chunks, key) in manifests_chunks.into_iter() {
|
||||
let version_id = &version_id;
|
||||
for (chunk_id, chunk_data) in chunks.into_iter() {
|
||||
let local_sender = sender.clone();
|
||||
let download_progress_handle = ProgressHandle::new(
|
||||
unsafe_self.download_progress.get(index),
|
||||
unsafe_self.download_progress.clone(),
|
||||
);
|
||||
let disk_progress_handle = ProgressHandle::new(
|
||||
unsafe_self.disk_progress.get(index),
|
||||
unsafe_self.disk_progress.clone(),
|
||||
);
|
||||
index += 1;
|
||||
|
||||
let chunk_length = chunk_data.files.iter().map(|v| v.length).sum();
|
||||
|
||||
if *local_completed_chunks.get(&chunk_id).unwrap_or(&false) {
|
||||
download_progress_handle.skip(chunk_length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let sender = unsafe_self.sender.clone();
|
||||
let (depot, permit) = match unsafe_self
|
||||
.depot_manager
|
||||
.next_depot(&unsafe_self.metadata.id, &unsafe_self.metadata.version)
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
send!(
|
||||
sender,
|
||||
DownloadManagerSignal::Error(
|
||||
ApplicationDownloadError::Communication(err)
|
||||
)
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let local_version_id = version_id.clone();
|
||||
s.spawn(move |_| {
|
||||
for i in 0..RETRY_COUNT {
|
||||
let base_path = unsafe_self.dropdata.base_path.clone();
|
||||
match download_game_chunk(
|
||||
&unsafe_self.metadata.id,
|
||||
&local_version_id,
|
||||
&chunk_id,
|
||||
&depot,
|
||||
&key,
|
||||
&chunk_data,
|
||||
file_list,
|
||||
base_path,
|
||||
&unsafe_self.control_flag,
|
||||
&download_progress_handle,
|
||||
&disk_progress_handle,
|
||||
) {
|
||||
Ok(true) => {
|
||||
local_sender.send(chunk_id.clone()).unwrap();
|
||||
drop(permit); // Take ownership
|
||||
return;
|
||||
}
|
||||
Ok(false) => return,
|
||||
Err(e) => {
|
||||
warn!("got error for chunk id {}: {e:?}", chunk_id);
|
||||
|
||||
let retry = true; /*matches!(
|
||||
&e,
|
||||
ApplicationDownloadError::Communication(_)
|
||||
| ApplicationDownloadError::Checksum
|
||||
| ApplicationDownloadError::Lock
|
||||
| ApplicationDownloadError::IoError(_)
|
||||
);*/
|
||||
|
||||
if i == RETRY_COUNT - 1 || !retry {
|
||||
warn!("retry logic failed, not re-attempting.");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
send!(sender, DownloadManagerSignal::Error(e));
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
drop(sender);
|
||||
});
|
||||
});
|
||||
let mut chunk_completions = FuturesUnordered::new();
|
||||
|
||||
let mut outputs = Vec::new();
|
||||
while let Ok(chunk_id) = recv.recv() {
|
||||
outputs.push(chunk_id);
|
||||
|
||||
let mut handle_output =
|
||||
|value: Result<Option<String>, ApplicationDownloadError>| match value {
|
||||
Ok(value) => {
|
||||
if let Some(chunk_id) = value {
|
||||
outputs.push(chunk_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let mut index = 0;
|
||||
for (version_id, chunks, key) in manifests_chunks.into_iter() {
|
||||
let version_id = &version_id;
|
||||
for (chunk_id, chunk_data) in chunks.into_iter() {
|
||||
let download_progress_handle = ProgressHandle::new(
|
||||
self.download_progress.get(index),
|
||||
self.download_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();
|
||||
|
||||
if *local_completed_chunks.get(&chunk_id).unwrap_or(&false) {
|
||||
download_progress_handle.skip(chunk_length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (depot, permit) = match self
|
||||
.depot_manager
|
||||
.next_depot(&self.metadata.id, &self.metadata.version)
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
let local_version_id = version_id.clone();
|
||||
while chunk_completions.len() >= max_download_threads {
|
||||
handle_output(
|
||||
chunk_completions
|
||||
.next()
|
||||
.await
|
||||
.expect("max download threads is zero?"),
|
||||
)?;
|
||||
}
|
||||
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,
|
||||
&chunk_id,
|
||||
&depot,
|
||||
&key,
|
||||
&chunk_data,
|
||||
file_list,
|
||||
base_path,
|
||||
&self.control_flag,
|
||||
&download_progress_handle,
|
||||
&disk_progress_handle,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
drop(permit);
|
||||
return Ok(Some(chunk_id.clone()));
|
||||
}
|
||||
Ok(false) => return Ok(None),
|
||||
Err(e) => {
|
||||
warn!("got error for chunk id {}: {e:?}", chunk_id);
|
||||
|
||||
let retry = true; /*matches!(
|
||||
&e,
|
||||
ApplicationDownloadError::Communication(_)
|
||||
| ApplicationDownloadError::Checksum
|
||||
| ApplicationDownloadError::Lock
|
||||
| ApplicationDownloadError::IoError(_)
|
||||
);*/
|
||||
|
||||
if i == RETRY_COUNT - 1 || !retry {
|
||||
warn!("retry logic failed, not re-attempting.");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
download_join_handle
|
||||
.await
|
||||
.expect("failed to complete download");
|
||||
while let Some(value) = chunk_completions.next().await {
|
||||
handle_output(value)?
|
||||
}
|
||||
|
||||
for completed_chunk in outputs {
|
||||
completed_chunks.insert(completed_chunk, true);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
use std::io::{Read, Seek as _, SeekFrom, Write as _};
|
||||
use std::io::SeekFrom;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
@@ -14,19 +14,22 @@ use download_manager::util::download_thread_control_flag::{
|
||||
};
|
||||
use download_manager::util::progress_object::ProgressHandle;
|
||||
use droplet_rs::manifest::ChunkData;
|
||||
use futures_util::StreamExt as _;
|
||||
use log::{debug, info};
|
||||
use remote::auth::generate_authorization_header;
|
||||
use remote::error::{DropServerError, RemoteAccessError};
|
||||
use remote::utils::DROP_CLIENT_SYNC;
|
||||
use remote::utils::DROP_CLIENT_ASYNC;
|
||||
use sha2::Digest;
|
||||
use tauri::Url;
|
||||
use tokio::io::{AsyncReadExt as _, AsyncSeekExt as _, AsyncWriteExt as _};
|
||||
use tokio_util::io::StreamReader;
|
||||
|
||||
const READ_BUF_LEN: usize = 1024 * 1024;
|
||||
|
||||
type Aes128Ctr64LE = ctr::Ctr64LE<aes::Aes128>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn download_game_chunk(
|
||||
pub async fn download_game_chunk(
|
||||
game_id: &str,
|
||||
version_id: &str,
|
||||
chunk_id: &str,
|
||||
@@ -57,15 +60,16 @@ pub fn download_game_chunk(
|
||||
.join(&format!("content/{}/{}/{}", game_id, version_id, chunk_id))
|
||||
.map_err(|v| ApplicationDownloadError::DownloadError(v.into()))?;
|
||||
|
||||
let response = DROP_CLIENT_SYNC
|
||||
let response = DROP_CLIENT_ASYNC
|
||||
.get(url)
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
info!("chunk request got status code: {}", response.status());
|
||||
let raw_res = response.text().map_err(|e| {
|
||||
let raw_res = response.text().await.map_err(|e| {
|
||||
ApplicationDownloadError::Communication(RemoteAccessError::FetchErrorLegacy(e.into()))
|
||||
})?;
|
||||
info!("{raw_res}");
|
||||
@@ -89,11 +93,11 @@ pub fn download_game_chunk(
|
||||
|
||||
debug!("took {}ms to start downloading", timestep);
|
||||
|
||||
/*let stream = response
|
||||
let stream = response
|
||||
.bytes_stream()
|
||||
.map(|v| v.map_err(|err| std::io::Error::other(err)));
|
||||
let mut stream_reader = StreamReader::new(stream);*/
|
||||
let mut stream_reader = response;
|
||||
let mut stream_reader = StreamReader::new(stream);
|
||||
//let mut stream_reader = response;
|
||||
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
let mut cipher = Aes128Ctr64LE::new(key.into(), &chunk_data.iv.into());
|
||||
@@ -108,13 +112,14 @@ pub fn download_game_chunk(
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut file_handle = if should_write {
|
||||
let mut file_handle = std::fs::OpenOptions::new()
|
||||
let mut file_handle = tokio::fs::OpenOptions::new()
|
||||
.truncate(false)
|
||||
.write(true)
|
||||
.append(false)
|
||||
.create(true)
|
||||
.open(&path)?;
|
||||
file_handle.seek(SeekFrom::Start(file.start.try_into().unwrap()))?;
|
||||
.open(&path)
|
||||
.await?;
|
||||
file_handle.seek(SeekFrom::Start(file.start.try_into().unwrap())).await?;
|
||||
Some(file_handle)
|
||||
} else {
|
||||
None
|
||||
@@ -122,14 +127,14 @@ pub fn download_game_chunk(
|
||||
|
||||
let mut remaining = file.length;
|
||||
while remaining > 0 {
|
||||
let amount = stream_reader.read(&mut read_buf[0..remaining.min(READ_BUF_LEN)])?;
|
||||
let amount = stream_reader.read(&mut read_buf[0..remaining.min(READ_BUF_LEN)]).await?;
|
||||
download_progress.add(amount);
|
||||
remaining -= amount;
|
||||
|
||||
cipher.apply_keystream(&mut read_buf[0..amount]);
|
||||
//hasher.update(&read_buf[0..amount]);
|
||||
hasher.update(&read_buf[0..amount]);
|
||||
if let Some(file_handle) = &mut file_handle {
|
||||
file_handle.write_all(&read_buf[0..amount])?;
|
||||
file_handle.write_all(&read_buf[0..amount]).await?;
|
||||
disk_progress.add(amount);
|
||||
}
|
||||
}
|
||||
@@ -155,7 +160,7 @@ pub fn download_game_chunk(
|
||||
|
||||
let digest = hex::encode(hasher.finalize());
|
||||
if digest != chunk_data.checksum {
|
||||
//return Err(ApplicationDownloadError::Checksum);
|
||||
return Err(ApplicationDownloadError::Checksum);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
|
||||
@@ -49,6 +49,7 @@ pub struct Game {
|
||||
pub m_cover_object_id: String,
|
||||
pub m_image_library_object_ids: Vec<String>,
|
||||
pub m_image_carousel_object_ids: Vec<String>,
|
||||
pub library_path: String,
|
||||
}
|
||||
impl Game {
|
||||
pub fn id(&self) -> &String {
|
||||
@@ -246,6 +247,10 @@ pub async fn on_game_complete(
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.remove(meta);
|
||||
drop(db_handle);
|
||||
app_emit!(
|
||||
app_handle,
|
||||
@@ -289,11 +294,6 @@ pub fn push_game_update(
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FrontendGameOptions {
|
||||
launch_string: String,
|
||||
}
|
||||
|
||||
impl FrontendGameOptions {
|
||||
pub fn launch_string(&self) -> &String {
|
||||
&self.launch_string
|
||||
}
|
||||
pub launch_string: String,
|
||||
pub override_proton_path: Option<String>,
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ pub fn scan_install_dirs() {
|
||||
if !drop_data_file.exists() {
|
||||
continue;
|
||||
}
|
||||
let game_id = game.file_name().display().to_string();
|
||||
let Ok(drop_data) = DropData::read(&game.path()) else {
|
||||
warn!(
|
||||
".dropdata exists for {}, but couldn't read it. is it corrupted?",
|
||||
@@ -27,7 +26,7 @@ pub fn scan_install_dirs() {
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if db_lock.applications.game_statuses.contains_key(&game_id) {
|
||||
if db_lock.applications.game_statuses.contains_key(&drop_data.game_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ client = { path = "../client", version = "0.1.0" }
|
||||
database = { path = "../database", version = "0.1.0" }
|
||||
dynfmt = { version = "0.1.5", features = ["curly"] }
|
||||
games = { path = "../games", version = "0.1.0" }
|
||||
keyvalues-parser = "0.2.3"
|
||||
log = "0.4.28"
|
||||
page_size = "0.6.0"
|
||||
serde = "1.0.228"
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
// Linux-only file
|
||||
|
||||
use std::{
|
||||
fs::{DirEntry, read_dir, read_to_string},
|
||||
io,
|
||||
path::PathBuf,
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use database::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use log::warn;
|
||||
use serde::Serialize;
|
||||
|
||||
static SEARCH_PATHS: LazyLock<Vec<String>> = LazyLock::new(|| {
|
||||
let mut paths = vec!["/usr/share/steam/compatibilitytools.d/".to_owned()];
|
||||
|
||||
if let Some(home_dir) = std::env::home_dir() {
|
||||
paths.push(
|
||||
home_dir
|
||||
.join(".steam/root/compatibilitytools.d/")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
paths
|
||||
});
|
||||
|
||||
pub fn read_proton_path(proton_path: PathBuf) -> Result<Option<ProtonPath>, io::Error> {
|
||||
let read_dir = read_dir(&proton_path)?
|
||||
.flatten()
|
||||
.collect::<Vec<DirEntry>>();
|
||||
let has_proton_path = read_dir
|
||||
.iter()
|
||||
.find(|v| v.file_name().to_string_lossy() == "proton")
|
||||
.is_some();
|
||||
if !has_proton_path {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let compat_vdf = read_dir
|
||||
.iter()
|
||||
.find(|v| v.file_name().to_string_lossy() == "compatibilitytool.vdf");
|
||||
|
||||
let compat_vdf = match compat_vdf {
|
||||
Some(v) => v,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let compat_vdf = read_to_string(compat_vdf.path())?;
|
||||
let compat_vdf = keyvalues_parser::parse(&compat_vdf)
|
||||
.inspect_err(|err| warn!("failed to parse vdf: {:?}", err))
|
||||
.map_err(|err| io::Error::other(err.to_string()))?;
|
||||
|
||||
// Function was made with a lot of trial and error
|
||||
// Not intended to be readable
|
||||
let get_display_name = || -> Option<String> {
|
||||
let compat_tools = compat_vdf.value.unwrap_obj();
|
||||
let compat_tools = compat_tools.values().next()?.iter().next()?;
|
||||
let compat_tools = compat_tools.get_obj().unwrap();
|
||||
let compat_tools = compat_tools.values().next()?.iter().next()?.get_obj()?;
|
||||
let display_name = compat_tools.get("display_name")?.iter().next()?.get_str()?;
|
||||
Some(display_name.to_string())
|
||||
};
|
||||
|
||||
if let Some(display_name) = get_display_name() {
|
||||
return Ok(Some(ProtonPath {
|
||||
path: proton_path.to_string_lossy().to_string(),
|
||||
name: display_name,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn discover_proton_paths() -> Result<Vec<ProtonPath>, io::Error> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for search_path in &*SEARCH_PATHS {
|
||||
if let Ok(potential_dirs) = read_dir(search_path) {
|
||||
for proton_path in potential_dirs {
|
||||
if let Some(proton) = read_proton_path(proton_path?.path())? {
|
||||
results.push(proton);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProtonPath {
|
||||
pub path: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProtonPaths {
|
||||
pub autodiscovered: Vec<ProtonPath>,
|
||||
pub custom: Vec<ProtonPath>,
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_proton_paths() -> Result<ProtonPaths, String> {
|
||||
let autodiscovered = discover_proton_paths().map_err(|v| v.to_string())?;
|
||||
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
let custom = db_lock
|
||||
.applications
|
||||
.additional_proton_paths
|
||||
.iter()
|
||||
.flat_map(|v| read_proton_path(PathBuf::from(v)))
|
||||
.flatten()
|
||||
.collect::<Vec<ProtonPath>>();
|
||||
|
||||
let default = db_lock.applications.default_proton_path.clone();
|
||||
|
||||
Ok(ProtonPaths {
|
||||
autodiscovered,
|
||||
custom,
|
||||
default,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_proton_layer(path: String) -> Result<(), String> {
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
let proton_layer = read_proton_path(path)
|
||||
.map_err(|err| err.to_string())?
|
||||
.ok_or("Unable to detect Proton at selected path.".to_owned())?;
|
||||
|
||||
let mut db = borrow_db_mut_checked();
|
||||
db.applications
|
||||
.additional_proton_paths
|
||||
.push(proton_layer.path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_proton_layer(index: usize) {
|
||||
let mut db = borrow_db_mut_checked();
|
||||
let deleted = db.applications.additional_proton_paths.try_remove(index);
|
||||
if let Some(deleted) = deleted
|
||||
&& let Some(default_path) = &db.applications.default_proton_path
|
||||
&& *default_path == deleted {
|
||||
db.applications.default_proton_path = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_default(path: String) -> Result<(), String> {
|
||||
let proton_paths = fetch_proton_paths().await?;
|
||||
|
||||
let valid = proton_paths
|
||||
.autodiscovered
|
||||
.iter()
|
||||
.find(|v| v.path == path)
|
||||
.or(proton_paths.custom.iter().find(|v| v.path == path))
|
||||
.is_some();
|
||||
|
||||
if !valid {
|
||||
return Err("Invalid default Proton path.".to_string());
|
||||
}
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
db_lock.applications.default_proton_path = Some(path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::{fmt::Display, io::{self, Error}, sync::Arc};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{self, Error},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
@@ -15,6 +19,7 @@ pub enum ProcessError {
|
||||
OpenerError(Arc<tauri_plugin_opener::Error>),
|
||||
InvalidArguments(String),
|
||||
FailedLaunch(String),
|
||||
NoCompat,
|
||||
}
|
||||
|
||||
impl Display for ProcessError {
|
||||
@@ -38,6 +43,7 @@ impl Display for ProcessError {
|
||||
"Missing a required dependency to launch this game: {} {}",
|
||||
game_id, version_id
|
||||
),
|
||||
ProcessError::NoCompat => "No Proton compatibility layer could be found for this tool. Add an override or set your global default in settings.",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
@@ -47,4 +53,4 @@ impl From<io::Error> for ProcessError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
ProcessError::IOError(Arc::new(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ impl DropFormatArgs {
|
||||
map.insert("abs_exe", absolute_executable_name);
|
||||
|
||||
if let Some(original) = original {
|
||||
map.insert("executor", original);
|
||||
map.insert("rom", original);
|
||||
}
|
||||
|
||||
Self { positional, map }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![feature(nonpoison_mutex)]
|
||||
#![feature(sync_nonpoison)]
|
||||
#![feature(extend_one)]
|
||||
#![feature(vec_try_remove)]
|
||||
|
||||
use std::{
|
||||
ops::Deref,
|
||||
@@ -13,11 +14,13 @@ use crate::process_manager::ProcessManager;
|
||||
|
||||
pub static PROCESS_MANAGER: ProcessManagerWrapper = ProcessManagerWrapper::new();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod compat;
|
||||
pub mod error;
|
||||
pub mod format;
|
||||
mod parser;
|
||||
pub mod process_handlers;
|
||||
pub mod process_manager;
|
||||
mod parser;
|
||||
|
||||
pub struct ProcessManagerWrapper(OnceLock<Mutex<ProcessManager<'static>>>);
|
||||
impl ProcessManagerWrapper {
|
||||
|
||||
@@ -43,8 +43,8 @@ impl ParsedCommand {
|
||||
v.extend(self.env);
|
||||
v.extend_one(self.command);
|
||||
v.extend(self.args);
|
||||
v.join(" ")
|
||||
shell_words::join(v)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LaunchParameters(pub String, pub PathBuf);
|
||||
pub struct LaunchParameters(pub ParsedCommand, pub PathBuf);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fs::create_dir_all;
|
||||
use std::{fs::create_dir_all, path::PathBuf};
|
||||
|
||||
use client::compat::{COMPAT_INFO, UMU_LAUNCHER_EXECUTABLE};
|
||||
use database::{
|
||||
@@ -15,6 +15,7 @@ impl ProcessHandler for NativeGameLauncher {
|
||||
launch_command: String,
|
||||
_game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
_database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
Ok(format!("\"{}\"", launch_command))
|
||||
}
|
||||
@@ -32,33 +33,55 @@ impl ProcessHandler for UMULauncher {
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
let launch_config = game_version
|
||||
let umu_id_override = game_version
|
||||
.launches
|
||||
.iter()
|
||||
.find(|v| v.platform == meta.target_platform)
|
||||
.ok_or(ProcessError::NotInstalled)?;
|
||||
.and_then(|v| v.umu_id_override.as_ref())
|
||||
.map_or("", |v| v);
|
||||
|
||||
let game_id = match &launch_config.umu_id_override {
|
||||
Some(game_override) => {
|
||||
if game_override.is_empty() {
|
||||
game_version.version_id.clone()
|
||||
} else {
|
||||
game_override.clone()
|
||||
}
|
||||
}
|
||||
None => game_version.version_id.clone(),
|
||||
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)?;
|
||||
|
||||
let no_proton = match meta.target_platform {
|
||||
Platform::Linux => Some("UMU_NO_PROTON=1"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
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)?;
|
||||
|
||||
let proton_valid = crate::compat::read_proton_path(PathBuf::from(proton_path))
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
if !proton_valid {
|
||||
return Err(ProcessError::NoCompat);
|
||||
}
|
||||
Some(format!("PROTONPATH={}", proton_path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(format!(
|
||||
"GAMEID={game_id} WINEPREFIX={} {} {umu:?} {launch}",
|
||||
"GAMEID={game_id} {} WINEPREFIX={} {} {umu:?} {launch}",
|
||||
proton_env.unwrap_or(String::new()),
|
||||
pfx_dir.to_string_lossy(),
|
||||
match meta.target_platform {
|
||||
Platform::Linux => "UMU_NO_PROTON=1",
|
||||
_ => "",
|
||||
},
|
||||
no_proton.unwrap_or(""),
|
||||
umu = UMU_LAUNCHER_EXECUTABLE
|
||||
.as_ref()
|
||||
.expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"),
|
||||
@@ -82,6 +105,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
let umu_launcher = UMULauncher {};
|
||||
let umu_string = umu_launcher.create_launch_process(
|
||||
@@ -89,6 +113,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
||||
launch_command,
|
||||
game_version,
|
||||
current_dir,
|
||||
database,
|
||||
)?;
|
||||
let mut args_cmd = umu_string
|
||||
.split("umu-run")
|
||||
|
||||
@@ -321,7 +321,7 @@ impl ProcessManager<'_> {
|
||||
|
||||
let process_handler = self.fetch_process_handler(&db_lock, &target_platform)?;
|
||||
|
||||
let (target_command, executor) = match game_status {
|
||||
let (target_command, emulator) = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
@@ -335,7 +335,7 @@ impl ProcessManager<'_> {
|
||||
.ok_or(ProcessError::NotInstalled)?;
|
||||
(
|
||||
launch_config.command.clone(),
|
||||
launch_config.executor.as_ref(),
|
||||
launch_config.emulator.as_ref(),
|
||||
)
|
||||
}
|
||||
GameDownloadStatus::SetupRequired {
|
||||
@@ -353,27 +353,27 @@ impl ProcessManager<'_> {
|
||||
_ => unreachable!("Game registered as 'Partially Installed'"),
|
||||
};
|
||||
|
||||
let target_command = ParsedCommand::parse(target_command)?;
|
||||
let mut target_command = ParsedCommand::parse(target_command)?;
|
||||
|
||||
let launch_parameters = if let Some(executor) = executor {
|
||||
let target_launch_string = if let Some(emulator) = emulator {
|
||||
let err = ProcessError::RequiredDependency(
|
||||
executor.game_id.clone(),
|
||||
executor.version_id.clone(),
|
||||
emulator.game_id.clone(),
|
||||
emulator.version_id.clone(),
|
||||
);
|
||||
|
||||
let executor_metadata = db_lock
|
||||
let emulator_metadata = db_lock
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(&executor.game_id)
|
||||
.get(&emulator.game_id)
|
||||
.ok_or(err.clone())?;
|
||||
|
||||
let executor_game_status = db_lock
|
||||
let emulator_game_status = db_lock
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&executor.game_id)
|
||||
.get(&emulator.game_id)
|
||||
.ok_or(err.clone())?;
|
||||
|
||||
let executor_install_dir = match executor_game_status {
|
||||
let emulator_install_dir = match emulator_game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
@@ -385,86 +385,101 @@ impl ProcessManager<'_> {
|
||||
_ => Err(err.clone()),
|
||||
}?;
|
||||
|
||||
let executor_game_version = db_lock
|
||||
let emulator_game_version = db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&executor.version_id)
|
||||
.get(&emulator.version_id)
|
||||
.ok_or(err.clone())?;
|
||||
|
||||
let executor_launch_config = executor_game_version
|
||||
let emulator_launch_config = emulator_game_version
|
||||
.launches
|
||||
.iter()
|
||||
.find(|v| v.launch_id == executor.launch_id)
|
||||
.find(|v| v.launch_id == emulator.launch_id)
|
||||
.ok_or(err)?;
|
||||
|
||||
println!("{}", executor_launch_config.command);
|
||||
let mut exe_command = ParsedCommand::parse(executor_launch_config.command.clone())?;
|
||||
println!("{:?}", exe_command);
|
||||
exe_command.env.extend(target_command.env);
|
||||
exe_command.make_absolute(executor_install_dir.into());
|
||||
let mut exe_command = ParsedCommand::parse(emulator_launch_config.command.clone())?;
|
||||
exe_command.env.extend(target_command.env.clone());
|
||||
exe_command.make_absolute(emulator_install_dir.into());
|
||||
|
||||
target_command.make_absolute(PathBuf::from(install_dir.clone()));
|
||||
|
||||
exe_command.args.iter_mut().for_each(|v| {
|
||||
*v = v.replace("{executor}", &target_command.command);
|
||||
*v = v.replace("{rom}", &target_command.command);
|
||||
});
|
||||
|
||||
let executor_launch_string = process_handler.create_launch_process(
|
||||
executor_metadata,
|
||||
exe_command.reconstruct(),
|
||||
executor_game_version,
|
||||
install_dir,
|
||||
)?;
|
||||
|
||||
|
||||
LaunchParameters(executor_launch_string, install_dir.into())
|
||||
process_handler.create_launch_process(
|
||||
emulator_metadata,
|
||||
exe_command.reconstruct(),
|
||||
emulator_game_version,
|
||||
install_dir,
|
||||
&db_lock,
|
||||
)?
|
||||
} else {
|
||||
let target_launch_string = process_handler.create_launch_process(
|
||||
|
||||
|
||||
process_handler.create_launch_process(
|
||||
&meta,
|
||||
target_command.reconstruct(),
|
||||
game_version,
|
||||
install_dir,
|
||||
)?;
|
||||
|
||||
let mut parsed_launch = ParsedCommand::parse(target_launch_string.clone())?;
|
||||
let executable_name = parsed_launch.command.clone();
|
||||
parsed_launch.make_absolute(install_dir.into());
|
||||
|
||||
let format_args = DropFormatArgs::new(
|
||||
target_launch_string,
|
||||
install_dir,
|
||||
&executable_name,
|
||||
parsed_launch.command,
|
||||
None,
|
||||
);
|
||||
|
||||
let target_launch_string = SimpleCurlyFormat
|
||||
.format(&game_version.launch_template, &format_args)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
let target_launch_string = SimpleCurlyFormat
|
||||
.format(&target_launch_string, format_args)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
LaunchParameters(target_launch_string, install_dir.into())
|
||||
&db_lock,
|
||||
)?
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut command = Command::new("cmd");
|
||||
#[cfg(target_os = "windows")]
|
||||
command.raw_arg(format!("/C \"{}\"", &launch_parameters.0));
|
||||
let mut parsed_launch = ParsedCommand::parse(target_launch_string.clone())?;
|
||||
let executable_name = parsed_launch.command.clone();
|
||||
parsed_launch.make_absolute(install_dir.into());
|
||||
|
||||
let format_args = DropFormatArgs::new(
|
||||
target_launch_string,
|
||||
install_dir,
|
||||
&executable_name,
|
||||
parsed_launch.command,
|
||||
None,
|
||||
);
|
||||
|
||||
let target_launch_string = SimpleCurlyFormat
|
||||
.format(
|
||||
&game_version.user_configuration.launch_template,
|
||||
&format_args,
|
||||
)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
let target_launch_string = SimpleCurlyFormat
|
||||
.format(&target_launch_string, format_args)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
let launch_parameters = LaunchParameters(
|
||||
ParsedCommand::parse(target_launch_string)?,
|
||||
install_dir.into(),
|
||||
);
|
||||
|
||||
info!(
|
||||
"launching (in {}): {}",
|
||||
"launching (in {}): {:?}",
|
||||
launch_parameters.1.to_string_lossy(),
|
||||
launch_parameters.0
|
||||
);
|
||||
|
||||
#[cfg(unix)]
|
||||
let mut command: Command = Command::new("sh");
|
||||
#[cfg(unix)]
|
||||
command.args(vec!["-c", &launch_parameters.0]);
|
||||
let mut command = {
|
||||
let mut command = Command::new(launch_parameters.0.command);
|
||||
command.args(launch_parameters.0.args);
|
||||
for parts in launch_parameters
|
||||
.0
|
||||
.env
|
||||
.into_iter()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
command
|
||||
};
|
||||
|
||||
command
|
||||
.stderr(error_file)
|
||||
@@ -474,8 +489,7 @@ impl ProcessManager<'_> {
|
||||
|
||||
let child = command.spawn()?;
|
||||
|
||||
let launch_process_handle =
|
||||
Arc::new(SharedChild::new(child)?);
|
||||
let launch_process_handle = Arc::new(SharedChild::new(child)?);
|
||||
|
||||
db_lock
|
||||
.applications
|
||||
@@ -518,6 +532,7 @@ pub trait ProcessHandler: Send + 'static {
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
database: &Database,
|
||||
) -> Result<String, ProcessError>;
|
||||
|
||||
fn valid_for_platform(&self, db: &Database, target: &Platform) -> bool;
|
||||
|
||||
@@ -4,10 +4,8 @@ use std::{
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use client::{app_status::AppStatus, user::User};
|
||||
use database::{DatabaseAuth, interface::borrow_db_checked};
|
||||
use droplet_rs::ssl::sign_nonce;
|
||||
use gethostname::gethostname;
|
||||
use jsonwebtoken::{Algorithm, EncodingKey, Header};
|
||||
use log::{error, warn};
|
||||
|
||||
@@ -35,6 +35,7 @@ pub enum RemoteAccessError {
|
||||
Cache(std::io::Error),
|
||||
CorruptedState,
|
||||
NoDepots,
|
||||
FailedDownload,
|
||||
}
|
||||
|
||||
impl Display for RemoteAccessError {
|
||||
@@ -104,8 +105,12 @@ impl Display for RemoteAccessError {
|
||||
f,
|
||||
"Drop encountered a corrupted internal state. Please report this to the developers, with details of reproduction."
|
||||
),
|
||||
RemoteAccessError::NoDepots => write!(f, "There are no download depots configured on the server. Contact your server admin."),
|
||||
}
|
||||
RemoteAccessError::NoDepots => write!(
|
||||
f,
|
||||
"There are no download depots configured on the server. Contact your server admin."
|
||||
),
|
||||
RemoteAccessError::FailedDownload => write!(f, "Failed to download."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,21 +44,33 @@ impl Middleware for AutoOfflineMiddleware {
|
||||
extensions: &mut Extensions,
|
||||
next: Next<'_>,
|
||||
) -> Result<Response> {
|
||||
let url = req.url().clone();
|
||||
let res = next.run(req, extensions).await;
|
||||
match res {
|
||||
Ok(res) => {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let lock = DROP_APP_HANDLE.lock().await;
|
||||
if let Some(app_handle) = &*lock {
|
||||
let state = app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
|
||||
let 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");
|
||||
}
|
||||
};
|
||||
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());
|
||||
}
|
||||
};
|
||||
} else {
|
||||
warn!(
|
||||
"failed to lock app handle for offline/online middleware - {}",
|
||||
url.as_str()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
|
||||
+16
-5
@@ -48,6 +48,7 @@ pub struct FetchLibraryResponse {
|
||||
library: Vec<Game>,
|
||||
collections: Vec<Collection>,
|
||||
other: Vec<Game>,
|
||||
missing: Vec<Game>,
|
||||
}
|
||||
|
||||
pub async fn fetch_library_logic(
|
||||
@@ -60,11 +61,11 @@ pub async fn fetch_library_logic(
|
||||
return Ok(library);
|
||||
}
|
||||
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let response = generate_url(&["/api/v1/client/user/library"], &[])?;
|
||||
let response = client
|
||||
let auth_header = generate_authorization_header();
|
||||
let response = DROP_CLIENT_ASYNC
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.header("Authorization", auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -111,6 +112,7 @@ pub async fn fetch_library_logic(
|
||||
|
||||
// Add games that are installed but no longer in library
|
||||
let mut other = Vec::new();
|
||||
let mut missing = Vec::new();
|
||||
for meta in installed_metas {
|
||||
if all_games.iter().any(|e| *e.id() == meta.id) {
|
||||
continue;
|
||||
@@ -132,13 +134,18 @@ pub async fn fetch_library_logic(
|
||||
continue;
|
||||
}
|
||||
};
|
||||
other.push(game);
|
||||
if game.game_type == "Game" {
|
||||
missing.push(game);
|
||||
} else {
|
||||
other.push(game);
|
||||
}
|
||||
}
|
||||
|
||||
let response = FetchLibraryResponse {
|
||||
library,
|
||||
collections,
|
||||
other,
|
||||
missing,
|
||||
};
|
||||
|
||||
cache_object("library", &response)?;
|
||||
@@ -167,6 +174,7 @@ pub async fn fetch_library_logic_offline(
|
||||
|
||||
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!(
|
||||
@@ -253,6 +261,7 @@ pub async fn fetch_game_logic(
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VersionDownloadOptionRequiredContent {
|
||||
game_id: String,
|
||||
version_id: String,
|
||||
name: String,
|
||||
icon_object_id: String,
|
||||
@@ -263,6 +272,7 @@ struct VersionDownloadOptionRequiredContent {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VersionDownloadOption {
|
||||
game_id: String,
|
||||
version_id: String,
|
||||
display_name: Option<String>,
|
||||
version_path: String,
|
||||
@@ -397,7 +407,8 @@ pub fn update_game_configuration(
|
||||
.clone();
|
||||
|
||||
// Add more options in here
|
||||
existing_configuration.launch_template = options.launch_string().clone();
|
||||
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
|
||||
|
||||
|
||||
+28
-4
@@ -12,7 +12,12 @@ use std::{
|
||||
sync::nonpoison::Mutex, time::SystemTime,
|
||||
};
|
||||
|
||||
use ::client::{app_state::AppState, app_status::AppStatus, autostart::sync_autostart_on_startup};
|
||||
use ::client::{
|
||||
app_state::{AppState, UmuState},
|
||||
app_status::AppStatus,
|
||||
autostart::sync_autostart_on_startup,
|
||||
compat::UMU_LAUNCHER_EXECUTABLE,
|
||||
};
|
||||
use ::download_manager::DownloadManagerWrapper;
|
||||
use ::games::scan::scan_install_dirs;
|
||||
use ::process::ProcessManagerWrapper;
|
||||
@@ -96,6 +101,15 @@ async fn setup(handle: AppHandle) -> AppState {
|
||||
ProcessManagerWrapper::init(handle.clone());
|
||||
DownloadManagerWrapper::init(handle.clone());
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let umu_state = UmuState::NotNeeded;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let umu_state = match UMU_LAUNCHER_EXECUTABLE.is_some() {
|
||||
true => UmuState::Installed,
|
||||
false => UmuState::NotInstalled,
|
||||
};
|
||||
|
||||
debug!("checking if database is set up");
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
|
||||
@@ -105,6 +119,7 @@ async fn setup(handle: AppHandle) -> AppState {
|
||||
return AppState {
|
||||
status: AppStatus::NotConfigured,
|
||||
user: None,
|
||||
umu_state,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,6 +181,7 @@ async fn setup(handle: AppHandle) -> AppState {
|
||||
AppState {
|
||||
status: app_status,
|
||||
user,
|
||||
umu_state,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +268,15 @@ pub fn run() {
|
||||
toggle_autostart,
|
||||
get_autostart_enabled,
|
||||
open_process_logs,
|
||||
get_launch_options
|
||||
get_launch_options,
|
||||
#[cfg(target_os = "linux")]
|
||||
::process::compat::fetch_proton_paths,
|
||||
#[cfg(target_os = "linux")]
|
||||
::process::compat::add_proton_layer,
|
||||
#[cfg(target_os = "linux")]
|
||||
::process::compat::remove_proton_layer,
|
||||
#[cfg(target_os = "linux")]
|
||||
::process::compat::set_default
|
||||
])
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
@@ -296,7 +320,7 @@ pub fn run() {
|
||||
|
||||
main_window
|
||||
.add_child(
|
||||
WebviewBuilder::new("frontned", WebviewUrl::App("main".into()))
|
||||
WebviewBuilder::new("frontend", WebviewUrl::App("main".into()))
|
||||
.auto_resize(),
|
||||
LogicalPosition::new(0., 0.),
|
||||
LogicalSize::new(width, height),
|
||||
@@ -355,7 +379,7 @@ pub fn run() {
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"open" => {
|
||||
app.webview_windows()
|
||||
.get("main")
|
||||
.get("frontend")
|
||||
.expect("Failed to get webview")
|
||||
.show()
|
||||
.expect("Failed to show window");
|
||||
|
||||
Reference in New Issue
Block a user