mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-01-30 19:15:17 +01:00
Compare commits
11 Commits
v0.1.0-bet
...
gamepads
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4228e1797b | ||
|
|
9af0d08875 | ||
|
|
dcb1564568 | ||
|
|
1f899ec349 | ||
|
|
6a8d0af87d | ||
|
|
21835858f1 | ||
|
|
a135b1321c | ||
|
|
ad92dbec08 | ||
|
|
85a08990c3 | ||
|
|
dd7f5675d8 | ||
|
|
9ea2aa4997 |
8
app.vue
8
app.vue
@@ -6,7 +6,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import "~/composables/queue";
|
||||
import "~/composables/downloads.js";
|
||||
import "~/plugins"
|
||||
import "~/gamepad"
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { AppStatus } from "~/types";
|
||||
@@ -20,10 +22,10 @@ import {
|
||||
const router = useRouter();
|
||||
|
||||
const state = useAppState();
|
||||
state.value = await invoke("fetch_state");
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
|
||||
router.beforeEach(async () => {
|
||||
state.value = await invoke("fetch_state");
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
});
|
||||
|
||||
setupHooks();
|
||||
|
||||
@@ -1,52 +1,75 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
@click="() => buttonActions[props.status.type]()"
|
||||
:class="[
|
||||
<div class="inline-flex divide-x divide-zinc-900">
|
||||
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex uppercase font-display items-center gap-x-2 rounded-md px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="buttonIcons[props.status.type]"
|
||||
class="-mr-0.5 size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ buttonNames[props.status.type] }}
|
||||
</button>
|
||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]">
|
||||
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
|
||||
{{ buttonNames[props.status.type] }}
|
||||
</button>
|
||||
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
|
||||
<div class="h-full">
|
||||
<MenuButton :class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm'
|
||||
]">
|
||||
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<MenuItems
|
||||
class="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
|
||||
<div class="py-1">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button @click="() => emit('uninstall')"
|
||||
:class="[active ? 'bg-zinc-800 text-zinc-100 outline-none' : 'text-zinc-400', 'w-full block px-4 py-2 text-sm inline-flex justify-between']">Uninstall
|
||||
<TrashIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ChevronDownIcon,
|
||||
PlayIcon,
|
||||
QueueListIcon,
|
||||
TrashIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
|
||||
import type { Component } from "vue";
|
||||
import { GameStatusEnum, type GameStatus } from "~/types.js";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||
|
||||
const props = defineProps<{ status: GameStatus }>();
|
||||
const emit = defineEmits<{
|
||||
(e: "install"): void;
|
||||
(e: "play"): void;
|
||||
(e: "launch"): void;
|
||||
(e: "queue"): void;
|
||||
(e: "uninstall"): void;
|
||||
}>();
|
||||
|
||||
const showDropdown = computed(() => props.status.type === GameStatusEnum.Installed || props.status.type === GameStatusEnum.SetupRequired);
|
||||
|
||||
const styles: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]:
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600",
|
||||
[GameStatusEnum.Queued]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.Downloading]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.SetupRequired]:
|
||||
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600",
|
||||
[GameStatusEnum.Installed]:
|
||||
"bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600",
|
||||
[GameStatusEnum.Updating]: "",
|
||||
[GameStatusEnum.Uninstalling]: "",
|
||||
[GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600",
|
||||
[GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.SetupRequired]: "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600",
|
||||
[GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600",
|
||||
[GameStatusEnum.Updating]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.Uninstalling]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.Running]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700"
|
||||
};
|
||||
|
||||
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||
@@ -57,6 +80,7 @@ const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Installed]: "Play",
|
||||
[GameStatusEnum.Updating]: "Updating",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||
[GameStatusEnum.Running]: "Running"
|
||||
};
|
||||
|
||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||
@@ -67,15 +91,17 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||
[GameStatusEnum.Installed]: PlayIcon,
|
||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||
[GameStatusEnum.Running]: PlayIcon
|
||||
};
|
||||
|
||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||
[GameStatusEnum.Remote]: () => emit("install"),
|
||||
[GameStatusEnum.Queued]: () => emit("queue"),
|
||||
[GameStatusEnum.Downloading]: () => emit("queue"),
|
||||
[GameStatusEnum.SetupRequired]: () => {},
|
||||
[GameStatusEnum.Installed]: () => emit("play"),
|
||||
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
||||
[GameStatusEnum.Installed]: () => emit("launch"),
|
||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||
[GameStatusEnum.Uninstalling]: () => {},
|
||||
[GameStatusEnum.Uninstalling]: () => { },
|
||||
[GameStatusEnum.Running]: () => { }
|
||||
};
|
||||
</script>
|
||||
|
||||
27
composables/downloads.ts
Normal file
27
composables/downloads.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
export type QueueState = {
|
||||
queue: Array<{ id: string; status: string; progress: number | null }>;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type StatsState = {
|
||||
speed: number; // Bytes per second
|
||||
time: number; // Seconds,
|
||||
};
|
||||
|
||||
export const useQueueState = () =>
|
||||
useState<QueueState>("queue", () => ({ queue: [], status: "Unknown" }));
|
||||
|
||||
export const useStatsState = () =>
|
||||
useState<StatsState>("stats", () => ({ speed: 0, time: 0 }));
|
||||
|
||||
listen("update_queue", (event) => {
|
||||
const queue = useQueueState();
|
||||
queue.value = event.payload as QueueState;
|
||||
});
|
||||
|
||||
listen("update_stats", (event) => {
|
||||
const stats = useStatsState();
|
||||
stats.value = event.payload as StatsState;
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
export type QueueState = {
|
||||
queue: Array<{ id: string; status: string, progress: number | null }>;
|
||||
};
|
||||
|
||||
export const useQueueState = () =>
|
||||
useState<QueueState>("queue", () => ({ queue: [] }));
|
||||
|
||||
listen("update_queue", (event) => {
|
||||
const queue = useQueueState();
|
||||
queue.value = event.payload as QueueState;
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { data } from "autoprefixer";
|
||||
import { AppStatus, type AppState } from "~/types";
|
||||
|
||||
export function setupHooks() {
|
||||
@@ -18,6 +19,20 @@ export function setupHooks() {
|
||||
router.push("/store");
|
||||
});
|
||||
|
||||
listen("download_error", (event) => {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "Drop encountered an error while downloading",
|
||||
description: `Drop encountered an error while downloading your game: "${(
|
||||
event.payload as unknown as string
|
||||
).toString()}"`,
|
||||
buttonText: "Close"
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
|
||||
67
gamepad.ts
Normal file
67
gamepad.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import gameControl, { XBoxButton, Button, type GCGamepad } from 'esm-gamecontroller.js';
|
||||
|
||||
let mainGamepad: Ref<number | undefined> = ref(undefined);
|
||||
|
||||
let buttonIndex = 0;
|
||||
const buttons = computed((): any[] => {
|
||||
let as = document.getElementsByTagName('a');
|
||||
let buttons = document.getElementsByTagName('button');
|
||||
return [].concat(Array.from(as)).concat(Array.from(buttons));
|
||||
})
|
||||
|
||||
const wrap = (num: number, min: number, max: number) => ((((num - min) % (max - min)) + (max - min)) % (max - min)) + min;
|
||||
|
||||
setInterval(() => {
|
||||
mainGamepad.value = navigator.getGamepads().filter(g => g !== null)[0]?.axes[1];
|
||||
console.log(navigator.getGamepads().filter(g => g !== null)[0]?.axes)
|
||||
}, 100);
|
||||
|
||||
watch(mainGamepad, (v) => {
|
||||
console.log(v)
|
||||
if (!v || v == 0) return;
|
||||
buttonIndex = wrap(buttonIndex + v > 0 ? 1 : -1, 0, buttons.value.length);
|
||||
|
||||
console.log(`Focusing ${buttonIndex}`)
|
||||
console.log(buttons.value[buttonIndex]);
|
||||
|
||||
buttons.value[buttonIndex].focus()
|
||||
})
|
||||
|
||||
|
||||
|
||||
/*
|
||||
setInterval(() => {
|
||||
console.log(gamepads[0]);
|
||||
console.log(gamepads[0].checkStatus())
|
||||
}, 1000);
|
||||
*/
|
||||
|
||||
/*
|
||||
window.ongamepadconnected = (e) => {
|
||||
console.log("ongamepadconnected", e);
|
||||
}
|
||||
|
||||
|
||||
function selectButton(index: number) {
|
||||
buttonIndex = (buttonIndex + index) % buttons.value.length;
|
||||
console.log(`Selecting button ${buttonIndex}`);
|
||||
buttons.value[buttonIndex].focus()
|
||||
}
|
||||
|
||||
function processGamepads() {
|
||||
while (true) {
|
||||
console.log("Processing gamepads");
|
||||
let gamepad = navigator.getGamepads()[0];
|
||||
if (!gamepad) continue;
|
||||
let direction = gamepad.axes[1];
|
||||
if (direction > 0.1) {
|
||||
selectButton(1)
|
||||
console.log("Selecting button 1")
|
||||
}
|
||||
else if (direction < -0.1) {
|
||||
selectButton(-1)
|
||||
console.log("Selecting button -1")
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -14,15 +14,19 @@
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@maulingmonkey/gamepad": "^0.0.5",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@tauri-apps/api": ">=2.0.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-shell": ">=2.0.0",
|
||||
"esm-gamecontroller.js": "^1.0.4",
|
||||
"gamepad-events": "^0.7.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"moment": "^2.30.1",
|
||||
"nuxt": "^3.13.0",
|
||||
"scss": "^0.2.4",
|
||||
"tauri-plugin-gamepad-api": "^0.0.5",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"vuedraggable": "^4.1.0"
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
<div class="h-full flex flex-row gap-x-4 items-stretch">
|
||||
<GameStatusButton
|
||||
@install="() => installFlow()"
|
||||
@play="() => play()"
|
||||
@launch="() => launch()"
|
||||
@queue="() => queue()"
|
||||
@uninstall="() => uninstall()"
|
||||
:status="status"
|
||||
/>
|
||||
<a
|
||||
@@ -389,7 +390,7 @@ async function install() {
|
||||
}
|
||||
}
|
||||
|
||||
async function play() {
|
||||
async function launch() {
|
||||
try {
|
||||
await invoke("launch_game", { gameId: game.value.id });
|
||||
} catch (e) {
|
||||
@@ -409,4 +410,8 @@ async function play() {
|
||||
async function queue() {
|
||||
router.push("/queue");
|
||||
}
|
||||
|
||||
async function uninstall() {
|
||||
await invoke("uninstall_game", {gameId: game.value.id});
|
||||
}
|
||||
</script>
|
||||
|
||||
125
pages/queue.vue
125
pages/queue.vue
@@ -1,18 +1,21 @@
|
||||
<template>
|
||||
<div class="bg-zinc-950 p-4 min-h-full">
|
||||
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
|
||||
<div class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900">
|
||||
<div class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2">
|
||||
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}</span>
|
||||
<span v-if="stats.time > 0" class="text-sm">{{ formatTime(stats.time) }} left</span>
|
||||
|
||||
</div>
|
||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
||||
<div v-for="bar in speedHistory" :style="{ height: `${bar / speedMax * 100}%` }" class="w-[8px] bg-blue-600/40" />
|
||||
</div>
|
||||
</div>
|
||||
<draggable v-model="queue.queue" @end="onEnd">
|
||||
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
|
||||
<li
|
||||
v-if="games[element.id]"
|
||||
:key="element.id"
|
||||
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"
|
||||
>
|
||||
<li v-if="games[element.id]" :key="element.id"
|
||||
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4">
|
||||
<div class="w-full flex items-center max-w-md gap-x-4 relative">
|
||||
<img
|
||||
class="size-24 flex-none bg-zinc-800 object-cover rounded"
|
||||
:src="games[element.id].cover"
|
||||
alt=""
|
||||
/>
|
||||
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="games[element.id].cover" alt="" />
|
||||
<div class="min-w-0 flex-auto">
|
||||
<p class="text-xl font-semibold text-zinc-100">
|
||||
<NuxtLink :href="`/library/${element.id}`" class="">
|
||||
@@ -30,31 +33,19 @@
|
||||
<p class="text-md text-zinc-500 uppercase font-display font-bold">
|
||||
{{ element.status }}
|
||||
</p>
|
||||
<div
|
||||
v-if="element.progress"
|
||||
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-2 bg-blue-600"
|
||||
:style="{ width: `${element.progress * 100}%` }"
|
||||
/>
|
||||
<div v-if="element.progress" class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden">
|
||||
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
<button @click="() => cancelGame(element.id)" class="group">
|
||||
<XMarkIcon
|
||||
class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<p v-else>Loading...</p>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
class="text-zinc-600 uppercase font-semibold font-display w-full text-center"
|
||||
v-if="queue.queue.length == 0"
|
||||
>
|
||||
<div class="text-zinc-600 uppercase font-semibold font-display w-full text-center" v-if="queue.queue.length == 0">
|
||||
No items in the queue
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,21 +54,67 @@
|
||||
<script setup lang="ts">
|
||||
import { XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import gameControl, { type GCGamepad } from "esm-gamecontroller.js";
|
||||
import type { Game, GameStatus } from "~/types";
|
||||
|
||||
const queue = useQueueState();
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
window.addEventListener('resize', (event) => {
|
||||
windowWidth.value = window.innerWidth;
|
||||
})
|
||||
|
||||
const current = computed(() => queue.value.queue.at(0));
|
||||
const rest = computed(() => queue.value.queue.slice(1));
|
||||
const queue = useQueueState();
|
||||
const stats = useStatsState();
|
||||
const speedHistory = useState<Array<number>>(() => []);
|
||||
const speedHistoryMax = computed(() => windowWidth.value / 8);
|
||||
const speedMax = computed(() => speedHistory.value.reduce((a, b) => a > b ? a : b) * 1.3);
|
||||
const previousGameId = ref<string | undefined>();
|
||||
|
||||
const games: Ref<{
|
||||
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
||||
}> = ref({});
|
||||
|
||||
|
||||
function resetHistoryGraph() {
|
||||
speedHistory.value = [];
|
||||
stats.value = { time: 0, speed: 0 };
|
||||
}
|
||||
function checkReset(v: QueueState) {
|
||||
const currentGame = v.queue.at(0);
|
||||
// If we're finished
|
||||
if (!currentGame && previousGameId.value) {
|
||||
previousGameId.value = undefined;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
}
|
||||
// If we don't have a game
|
||||
if (!currentGame) return;
|
||||
// If we started a new download
|
||||
if (currentGame && !previousGameId.value) {
|
||||
previousGameId.value = currentGame.id;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
}
|
||||
// If it's a different game now
|
||||
if (currentGame.id != previousGameId.value
|
||||
) {
|
||||
previousGameId.value = currentGame.id;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
}
|
||||
}
|
||||
watch(queue, (v) => {
|
||||
loadGamesForQueue(v);
|
||||
checkReset(v);
|
||||
});
|
||||
|
||||
watch(stats, (v) => {
|
||||
const newLength = speedHistory.value.push(v.speed);
|
||||
if (newLength > speedHistoryMax.value) {
|
||||
speedHistory.value.splice(0, 1);
|
||||
}
|
||||
checkReset(queue.value);
|
||||
})
|
||||
|
||||
function loadGamesForQueue(v: typeof queue.value) {
|
||||
for (const { id } of v.queue) {
|
||||
if (games.value[id]) return;
|
||||
@@ -101,4 +138,32 @@ async function onEnd(event: { oldIndex: number; newIndex: number }) {
|
||||
async function cancelGame(id: string) {
|
||||
await invoke("cancel_game", { gameId: id });
|
||||
}
|
||||
|
||||
function formatKilobytes(bytes: number): string {
|
||||
const units = ["KB", "MB", "GB", "TB", "PB"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
const scalar = 1000;
|
||||
|
||||
while (value >= scalar && unitIndex < units.length - 1) {
|
||||
value /= scalar;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}/s`;
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
if (seconds < 60) {
|
||||
return `${Math.round(seconds)}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ${Math.round(seconds % 60)}s`
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,25 +9,18 @@
|
||||
<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="[
|
||||
<NuxtLink :href="item.route" :class="[
|
||||
itemIdx === currentPageIndex
|
||||
? '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 === currentPageIndex
|
||||
? '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 === currentPageIndex
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 group-hover:text-zinc-200',
|
||||
'transition h-6 w-6 shrink-0',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
? '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,11 +36,13 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
CubeIcon,
|
||||
HomeIcon,
|
||||
RectangleGroupIcon,
|
||||
} from "@heroicons/vue/16/solid";
|
||||
import type { Component } from "vue";
|
||||
import type { NavigationItem } from "~/types";
|
||||
import { platform } from '@tauri-apps/plugin-os';
|
||||
|
||||
const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
{
|
||||
@@ -70,5 +65,12 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
},
|
||||
];
|
||||
|
||||
const currentPlatform = platform();
|
||||
switch (currentPlatform) {
|
||||
case "linux":
|
||||
navigation.splice(2, 0, { label: "Compatibility", route: "/settings/compatibility", prefix: "/settings/compatibility", icon: CubeIcon });
|
||||
break;
|
||||
}
|
||||
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
||||
</script>
|
||||
|
||||
3
pages/settings/compatibility.vue
Normal file
3
pages/settings/compatibility.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
1
plugins.ts
Normal file
1
plugins.ts
Normal file
@@ -0,0 +1 @@
|
||||
import 'tauri-plugin-gamepad-api'
|
||||
195
src-tauri/Cargo.lock
generated
195
src-tauri/Cargo.lock
generated
@@ -355,6 +355,12 @@ dependencies = [
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "7.0.0"
|
||||
@@ -965,6 +971,7 @@ dependencies = [
|
||||
name = "drop-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"chrono",
|
||||
"directories",
|
||||
"hex",
|
||||
@@ -984,6 +991,8 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-gamepad",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tokio",
|
||||
@@ -1061,6 +1070,26 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635"
|
||||
dependencies = [
|
||||
"enum-iterator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.91",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.10"
|
||||
@@ -1435,6 +1464,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
@@ -1457,6 +1496,40 @@ dependencies = [
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs"
|
||||
version = "0.11.0"
|
||||
source = "git+https://github.com/eugenehp/gilrs.git#cbcff6a4cd722a132bf2ca3c55fa9147775c9fe9"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"gilrs-core",
|
||||
"log",
|
||||
"serde",
|
||||
"uuid",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs-core"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/eugenehp/gilrs.git#cbcff6a4cd722a132bf2ca3c55fa9147775c9fe9"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"inotify",
|
||||
"io-kit-sys",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"serde",
|
||||
"uuid",
|
||||
"vec_map",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
@@ -2022,6 +2095,26 @@ dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
@@ -2031,6 +2124,16 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-kit-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"mach2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.10.1"
|
||||
@@ -2235,6 +2338,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
@@ -2306,6 +2419,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
@@ -2468,6 +2590,18 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
@@ -2853,6 +2987,17 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb6651f4be5e39563c4fe5cc8326349eb99a25d805a3493f791d5bfd0269e430"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.1"
|
||||
@@ -4107,6 +4252,15 @@ dependencies = [
|
||||
"syn 2.0.91",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
@@ -4388,6 +4542,39 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-gamepad"
|
||||
version = "0.0.5"
|
||||
source = "git+https://github.com/quexeky/tauri-plugin-gamepad.git#92cafc0aae66a3de01c252ae2ed599b329b0c686"
|
||||
dependencies = [
|
||||
"enum-iterator",
|
||||
"gilrs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.9",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-os"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dda2d571a9baf0664c1f2088db227e3072f9028602fafa885deade7547c3b738"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"log",
|
||||
"os_info",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serialize-to-javascript",
|
||||
"sys-locale",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.2.0"
|
||||
@@ -5021,6 +5208,12 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
@@ -5856,7 +6049,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix",
|
||||
"nix 0.27.1",
|
||||
"ordered-stream",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -41,12 +41,13 @@ http = "1.1.0"
|
||||
urlencoding = "2.1.3"
|
||||
md5 = "0.7.0"
|
||||
chrono = "0.4.38"
|
||||
tauri-plugin-os = "2"
|
||||
boxcar = "0.2.7"
|
||||
tauri-plugin-gamepad = {git = "https://github.com/quexeky/tauri-plugin-gamepad.git", version = "0.0.5"}
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.1.1"
|
||||
features = [
|
||||
"tray-icon"
|
||||
]
|
||||
features = ["tray-icon"]
|
||||
|
||||
|
||||
[dependencies.tokio]
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-close",
|
||||
"deep-link:default",
|
||||
"dialog:default"
|
||||
"dialog:default",
|
||||
"os:default",
|
||||
"gamepad:default"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
env,
|
||||
sync::Mutex,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
@@ -13,7 +12,7 @@ use url::Url;
|
||||
|
||||
use crate::{
|
||||
db::{DatabaseAuth, DatabaseImpls},
|
||||
remote::RemoteAccessError,
|
||||
remote::{DropServerError, RemoteAccessError},
|
||||
AppState, AppStatus, User, DB,
|
||||
};
|
||||
|
||||
@@ -78,7 +77,13 @@ pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
.send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
info!("Could not fetch user: {}", response.text().unwrap());
|
||||
let data = response.json::<DropServerError>()?;
|
||||
info!("Could not fetch user: {}", data.status_message);
|
||||
|
||||
if data.status_message == "Nonce expired" {
|
||||
return Err(RemoteAccessError::OutOfSync);
|
||||
}
|
||||
|
||||
return Err(RemoteAccessError::InvalidCodeError(0));
|
||||
}
|
||||
|
||||
@@ -147,7 +152,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||
app.emit("auth/finished", ()).unwrap();
|
||||
}
|
||||
|
||||
async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
||||
fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
||||
let base_url = {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
Url::parse(&db_lock.base_url.clone())?
|
||||
@@ -159,14 +164,17 @@ async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
||||
platform: env::consts::OS.to_string(),
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send().await?;
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(RemoteAccessError::InvalidRedirect);
|
||||
let data = response.json::<DropServerError>()?;
|
||||
info!("Could not start handshake: {}", data.status_message);
|
||||
|
||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||
}
|
||||
|
||||
let redir_url = response.text().await?;
|
||||
let redir_url = response.text()?;
|
||||
let complete_redir_url = base_url.join(&redir_url)?;
|
||||
|
||||
info!("opening web browser to continue authentication");
|
||||
@@ -176,8 +184,8 @@ async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn auth_initiate<'a>() -> Result<(), String> {
|
||||
let result = auth_initiate_wrapper().await;
|
||||
pub fn auth_initiate<'a>() -> Result<(), String> {
|
||||
let result = auth_initiate_wrapper();
|
||||
if result.is_err() {
|
||||
return Err(result.err().unwrap().to_string());
|
||||
}
|
||||
@@ -199,8 +207,10 @@ pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> Result<(), ()>
|
||||
|
||||
pub fn setup() -> Result<(AppStatus, Option<User>), ()> {
|
||||
let data = DB.borrow_data().unwrap();
|
||||
let auth = data.auth.clone();
|
||||
drop(data);
|
||||
|
||||
if data.auth.is_some() {
|
||||
if auth.is_some() {
|
||||
let user_result = fetch_user();
|
||||
if user_result.is_err() {
|
||||
let error = user_result.err().unwrap();
|
||||
@@ -215,7 +225,5 @@ pub fn setup() -> Result<(AppStatus, Option<User>), ()> {
|
||||
return Ok((AppStatus::SignedIn, Some(user_result.unwrap())));
|
||||
}
|
||||
|
||||
drop(data);
|
||||
|
||||
Ok((AppStatus::SignedOut, None))
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use log::info;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn quit(app: tauri::AppHandle) {
|
||||
cleanup_and_exit(&app);
|
||||
}
|
||||
|
||||
|
||||
pub fn cleanup_and_exit(app: &AppHandle, ) {
|
||||
pub fn cleanup_and_exit(app: &AppHandle) {
|
||||
info!("exiting drop application...");
|
||||
|
||||
app.exit(0);
|
||||
|
||||
@@ -21,6 +21,11 @@ pub struct DatabaseAuth {
|
||||
pub client_id: String,
|
||||
}
|
||||
|
||||
pub struct GameStatusData {
|
||||
version_name: String,
|
||||
install_dir: String,
|
||||
}
|
||||
|
||||
// Strings are version names for a particular game
|
||||
#[derive(Serialize, Clone, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -42,6 +47,7 @@ pub enum GameTransientStatus {
|
||||
Downloading { version_name: String },
|
||||
Uninstalling {},
|
||||
Updating { version_name: String },
|
||||
Running {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
|
||||
@@ -4,11 +4,9 @@ use crate::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||
use crate::downloads::progress_object::ProgressHandle;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::DB;
|
||||
use core::time;
|
||||
use log::{debug, error, info};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use serde::ser::{Error, SerializeMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io;
|
||||
@@ -32,24 +30,24 @@ pub struct GameDownloadAgent {
|
||||
pub version: String,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
contexts: Vec<DropDownloadContext>,
|
||||
completed_contexts: Mutex<Vec<usize>>,
|
||||
completed_contexts: VecDeque<usize>,
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub stored_manifest: StoredManifest,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GameDownloadError {
|
||||
Communication(RemoteAccessError),
|
||||
Checksum,
|
||||
Setup(SetupError),
|
||||
Lock,
|
||||
IoError(io::Error),
|
||||
IoError(io::ErrorKind),
|
||||
DownloadError,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SetupError {
|
||||
Context,
|
||||
}
|
||||
@@ -101,7 +99,7 @@ impl GameDownloadAgent {
|
||||
control_flag,
|
||||
manifest: Mutex::new(None),
|
||||
contexts: Vec::new(),
|
||||
completed_contexts: Mutex::new(Vec::new()),
|
||||
completed_contexts: VecDeque::new(),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
stored_manifest,
|
||||
@@ -219,12 +217,9 @@ impl GameDownloadAgent {
|
||||
let base_path = Path::new(&self.stored_manifest.base_path);
|
||||
create_dir_all(base_path).unwrap();
|
||||
|
||||
*self.completed_contexts.lock().unwrap() = self.stored_manifest.get_completed_contexts();
|
||||
|
||||
info!(
|
||||
"Completed contexts: {:?}",
|
||||
*self.completed_contexts.lock().unwrap()
|
||||
);
|
||||
self.completed_contexts.clear();
|
||||
self.completed_contexts
|
||||
.extend(self.stored_manifest.get_completed_contexts());
|
||||
|
||||
for (raw_path, chunk) in manifest {
|
||||
let path = base_path.join(Path::new(&raw_path));
|
||||
@@ -260,7 +255,7 @@ impl GameDownloadAgent {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<(), ()> {
|
||||
pub fn run(&mut self) -> Result<(), ()> {
|
||||
info!("downloading game: {}", self.id);
|
||||
const DOWNLOAD_MAX_THREADS: usize = 1;
|
||||
|
||||
@@ -269,56 +264,57 @@ impl GameDownloadAgent {
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let completed_indexes = Arc::new(Mutex::new(Vec::new()));
|
||||
let completed_indexes = Arc::new(boxcar::Vec::new());
|
||||
let completed_indexes_loop_arc = completed_indexes.clone();
|
||||
|
||||
pool.scope(move |scope| {
|
||||
let completed_lock = self.completed_contexts.lock().unwrap();
|
||||
|
||||
pool.scope(|scope| {
|
||||
for (index, context) in self.contexts.iter().enumerate() {
|
||||
let completed_indexes = completed_indexes_loop_arc.clone();
|
||||
|
||||
let progress = self.progress.get(index); // Clone arcs
|
||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||
// If we've done this one already, skip it
|
||||
if completed_lock.contains(&index) {
|
||||
if self.completed_contexts.contains(&index) {
|
||||
progress_handle.add(context.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let context = context.clone();
|
||||
let control_flag = self.control_flag.clone(); // Clone arcs
|
||||
let completed_indexes_ref = completed_indexes_loop_arc.clone();
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match download_game_chunk(context.clone(), control_flag, progress_handle) {
|
||||
Ok(res) => {
|
||||
if res {
|
||||
let mut lock = completed_indexes_ref.lock().unwrap();
|
||||
lock.push(index);
|
||||
completed_indexes.push(index);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("GameDownloadError: {}", e);
|
||||
self.sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
error!("{}", e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let newly_completed = completed_indexes.to_owned();
|
||||
|
||||
let completed_lock_len = {
|
||||
let mut completed_lock = self.completed_contexts.lock().unwrap();
|
||||
let newly_completed_lock = completed_indexes.lock().unwrap();
|
||||
for (item, item_ref) in newly_completed.iter() {
|
||||
self.completed_contexts.push_front(item);
|
||||
}
|
||||
|
||||
completed_lock.extend(newly_completed_lock.iter());
|
||||
|
||||
completed_lock.len()
|
||||
self.completed_contexts.len()
|
||||
};
|
||||
|
||||
// If we're not out of contexts, we're not done, so we don't fire completed
|
||||
if completed_lock_len != self.contexts.len() {
|
||||
info!("da for {} exited without completing", self.id.clone());
|
||||
self.stored_manifest
|
||||
.set_completed_contexts(&self.completed_contexts);
|
||||
.set_completed_contexts(&self.completed_contexts.clone().into());
|
||||
info!("Setting completed contexts");
|
||||
self.stored_manifest.write();
|
||||
info!("Wrote completed contexts");
|
||||
|
||||
@@ -6,14 +6,11 @@ use crate::DB;
|
||||
use log::warn;
|
||||
use md5::{Context, Digest};
|
||||
use reqwest::blocking::Response;
|
||||
use tauri::utils::acl::Permission;
|
||||
|
||||
use std::fs::{set_permissions, Permissions};
|
||||
use std::io::Read;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||
@@ -186,7 +183,9 @@ pub fn download_game_chunk(
|
||||
content_length.unwrap().try_into().unwrap(),
|
||||
);
|
||||
|
||||
let completed = pipeline.copy().map_err(GameDownloadError::IoError)?;
|
||||
let completed = pipeline
|
||||
.copy()
|
||||
.map_err(|e| GameDownloadError::IoError(e.kind()))?;
|
||||
if !completed {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
@@ -40,9 +40,14 @@ pub enum DownloadManagerSignal {
|
||||
/// Any error which occurs in the agent
|
||||
Error(GameDownloadError),
|
||||
/// Pushes UI update
|
||||
Update,
|
||||
UpdateUIQueue,
|
||||
UpdateUIStats(usize, usize), //kb/s and seconds
|
||||
/// Uninstall game
|
||||
/// Takes game ID
|
||||
Uninstall(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DownloadManagerStatus {
|
||||
Downloading,
|
||||
Paused,
|
||||
@@ -51,6 +56,15 @@ pub enum DownloadManagerStatus {
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl Serialize for DownloadManagerStatus {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!["{:?}", self])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub enum GameDownloadStatus {
|
||||
Queued,
|
||||
@@ -142,7 +156,7 @@ impl DownloadManager {
|
||||
let to_move = queue.remove(current_index).unwrap();
|
||||
queue.insert(new_index, to_move);
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Update)
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
pub fn cancel(&self, game_id: String) {
|
||||
@@ -174,7 +188,7 @@ impl DownloadManager {
|
||||
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Update)
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
pub fn pause_downloads(&self) {
|
||||
@@ -191,6 +205,11 @@ impl DownloadManager {
|
||||
.unwrap();
|
||||
self.terminator.join()
|
||||
}
|
||||
pub fn uninstall_game(&self, game_id: String) {
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Uninstall(game_id))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes in the locked value from .edit() and attempts to
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::remove_dir_all,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Arc, Mutex, RwLockWriteGuard,
|
||||
@@ -12,7 +13,10 @@ use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::{
|
||||
db::{Database, GameStatus, GameTransientStatus},
|
||||
library::{on_game_complete, GameUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData},
|
||||
library::{
|
||||
on_game_complete, push_game_update, QueueUpdateEvent,
|
||||
QueueUpdateEventQueueData, StatsUpdateEvent,
|
||||
},
|
||||
state::GameStatusManager,
|
||||
DB,
|
||||
};
|
||||
@@ -108,7 +112,7 @@ impl DownloadManagerBuilder {
|
||||
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
||||
}
|
||||
|
||||
fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &String) -> ()>(
|
||||
fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &String)>(
|
||||
&self,
|
||||
id: String,
|
||||
setter: F,
|
||||
@@ -120,18 +124,16 @@ impl DownloadManagerBuilder {
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id);
|
||||
|
||||
self.app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", id),
|
||||
GameUpdateEvent {
|
||||
game_id: id,
|
||||
status,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
push_game_update(&self.app_handle, id, status);
|
||||
}
|
||||
|
||||
fn push_manager_update(&self) {
|
||||
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
||||
let event_data = StatsUpdateEvent { speed: kbs, time };
|
||||
|
||||
self.app_handle.emit("update_stats", event_data).unwrap();
|
||||
}
|
||||
|
||||
fn push_ui_queue_update(&self) {
|
||||
let queue = self.download_queue.read();
|
||||
let queue_objs: Vec<QueueUpdateEventQueueData> = queue
|
||||
.iter()
|
||||
@@ -142,7 +144,14 @@ impl DownloadManagerBuilder {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let event_data = QueueUpdateEvent { queue: queue_objs };
|
||||
let status_handle = self.status.lock().unwrap();
|
||||
let status = status_handle.clone();
|
||||
drop(status_handle);
|
||||
|
||||
let event_data = QueueUpdateEvent {
|
||||
queue: queue_objs,
|
||||
status,
|
||||
};
|
||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
||||
}
|
||||
|
||||
@@ -159,9 +168,7 @@ impl DownloadManagerBuilder {
|
||||
drop(download_thread_lock);
|
||||
}
|
||||
|
||||
fn sync_download_agent(&self) {}
|
||||
|
||||
fn remove_and_cleanup_game(&mut self, game_id: &String) -> Arc<Mutex<GameDownloadAgent>> {
|
||||
fn remove_and_cleanup_front_game(&mut self, game_id: &String) -> Arc<Mutex<GameDownloadAgent>> {
|
||||
self.download_queue.pop_front();
|
||||
let download_agent = self.download_agent_registry.remove(game_id).unwrap();
|
||||
self.cleanup_current_download();
|
||||
@@ -206,40 +213,117 @@ impl DownloadManagerBuilder {
|
||||
DownloadManagerSignal::Cancel => {
|
||||
self.manage_cancel_signal();
|
||||
}
|
||||
DownloadManagerSignal::Update => {
|
||||
self.push_manager_update();
|
||||
DownloadManagerSignal::UpdateUIQueue => {
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
|
||||
self.push_ui_stats_update(kbs, time);
|
||||
}
|
||||
DownloadManagerSignal::Finish => {
|
||||
self.stop_and_wait_current_download();
|
||||
return Ok(());
|
||||
}
|
||||
DownloadManagerSignal::Remove(game_id) => {
|
||||
self.manage_remove_game(game_id);
|
||||
self.manage_remove_game_queue(game_id);
|
||||
}
|
||||
DownloadManagerSignal::Uninstall(game_id) => {
|
||||
self.uninstall_game(game_id);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn manage_remove_game(&mut self, game_id: String) {
|
||||
fn uninstall_game(&mut self, game_id: String) {
|
||||
// Removes the game if it's in the queue
|
||||
self.manage_remove_game_queue(game_id.clone());
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle
|
||||
.games
|
||||
.transient_statuses
|
||||
.entry(game_id.clone())
|
||||
.and_modify(|v| *v = GameTransientStatus::Uninstalling {});
|
||||
push_game_update(
|
||||
&self.app_handle,
|
||||
game_id.clone(),
|
||||
(None, Some(GameTransientStatus::Uninstalling {})),
|
||||
);
|
||||
|
||||
let previous_state = db_handle.games.statuses.get(&game_id).cloned();
|
||||
if previous_state.is_none() {
|
||||
info!("uninstall job doesn't have previous state, failing silently");
|
||||
return;
|
||||
}
|
||||
let previous_state = previous_state.unwrap();
|
||||
if let Some((version_name, install_dir)) = match previous_state {
|
||||
GameStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
} {
|
||||
db_handle
|
||||
.games
|
||||
.transient_statuses
|
||||
.entry(game_id.clone())
|
||||
.and_modify(|v| *v = GameTransientStatus::Uninstalling {});
|
||||
drop(db_handle);
|
||||
|
||||
let sender = self.sender.clone();
|
||||
let app_handle = self.app_handle.clone();
|
||||
spawn(move || match remove_dir_all(install_dir) {
|
||||
Err(e) => {
|
||||
sender
|
||||
.send(DownloadManagerSignal::Error(GameDownloadError::IoError(
|
||||
e.kind(),
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
Ok(_) => {
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle.games.transient_statuses.remove(&game_id);
|
||||
db_handle
|
||||
.games
|
||||
.statuses
|
||||
.entry(game_id.clone())
|
||||
.and_modify(|e| *e = GameStatus::Remote {});
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
|
||||
info!("uninstalled {}", game_id);
|
||||
|
||||
push_game_update(&app_handle, game_id, (Some(GameStatus::Remote {}), None));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn manage_remove_game_queue(&mut self, game_id: String) {
|
||||
if let Some(current_download) = &self.current_download_agent {
|
||||
if current_download.id == game_id {
|
||||
self.manage_cancel_signal();
|
||||
}
|
||||
}
|
||||
|
||||
let index = self.download_queue.get_by_id(game_id.clone()).unwrap();
|
||||
let mut queue_handle = self.download_queue.edit();
|
||||
queue_handle.remove(index);
|
||||
self.set_game_status(game_id, |db_handle, id| {
|
||||
db_handle.games.transient_statuses.remove(id);
|
||||
});
|
||||
drop(queue_handle);
|
||||
let index = self.download_queue.get_by_id(game_id.clone());
|
||||
if let Some(index) = index {
|
||||
let mut queue_handle = self.download_queue.edit();
|
||||
queue_handle.remove(index);
|
||||
self.set_game_status(game_id, |db_handle, id| {
|
||||
db_handle.games.transient_statuses.remove(id);
|
||||
});
|
||||
drop(queue_handle);
|
||||
}
|
||||
|
||||
if self.current_download_agent.is_none() {
|
||||
self.manage_go_signal();
|
||||
}
|
||||
|
||||
self.push_manager_update();
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
|
||||
fn manage_stop_signal(&mut self) {
|
||||
@@ -256,11 +340,16 @@ impl DownloadManagerBuilder {
|
||||
// When if let chains are stabilised, combine these two statements
|
||||
if interface.id == game_id {
|
||||
info!("Popping consumed data");
|
||||
let download_agent = self.remove_and_cleanup_game(&game_id);
|
||||
let download_agent = self.remove_and_cleanup_front_game(&game_id);
|
||||
let download_agent_lock = download_agent.lock().unwrap();
|
||||
|
||||
let version = download_agent_lock.version.clone();
|
||||
let install_dir = download_agent_lock.stored_manifest.base_path.clone().to_string_lossy().to_string();
|
||||
let install_dir = download_agent_lock
|
||||
.stored_manifest
|
||||
.base_path
|
||||
.clone()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
drop(download_agent_lock);
|
||||
|
||||
@@ -275,12 +364,30 @@ impl DownloadManagerBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
|
||||
fn manage_queue_signal(&mut self, id: String, version: String, target_download_dir: usize) {
|
||||
info!("Got signal Queue");
|
||||
|
||||
if let Some(index) = self.download_queue.get_by_id(id.clone()) {
|
||||
// Should always give us a value
|
||||
if let Some(download_agent) = self.download_agent_registry.get(&id) {
|
||||
let download_agent_handle = download_agent.lock().unwrap();
|
||||
if download_agent_handle.version == version {
|
||||
info!("game with same version already queued, skipping");
|
||||
return;
|
||||
}
|
||||
// If it's not the same, we want to cancel the current one, and then add the new one
|
||||
drop(download_agent_handle);
|
||||
|
||||
self.manage_remove_game_queue(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let download_agent = Arc::new(Mutex::new(GameDownloadAgent::new(
|
||||
id.clone(),
|
||||
version,
|
||||
@@ -309,7 +416,9 @@ impl DownloadManagerBuilder {
|
||||
GameTransientStatus::Downloading { version_name },
|
||||
);
|
||||
});
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn manage_go_signal(&mut self) {
|
||||
@@ -386,12 +495,19 @@ impl DownloadManagerBuilder {
|
||||
);
|
||||
});
|
||||
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
fn manage_error_signal(&mut self, error: GameDownloadError) {
|
||||
let current_status = self.current_download_agent.clone().unwrap();
|
||||
|
||||
self.remove_and_cleanup_game(¤t_status.id); // Remove all the locks and shit
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_game(¤t_status.id); // Remove all the locks and shit, and remove from queue
|
||||
|
||||
self.app_handle
|
||||
.emit("download_error", error.to_string())
|
||||
.unwrap();
|
||||
|
||||
let mut lock = current_status.status.lock().unwrap();
|
||||
*lock = GameDownloadStatus::Error;
|
||||
@@ -402,7 +518,9 @@ impl DownloadManagerBuilder {
|
||||
db_handle.games.transient_statuses.remove(id);
|
||||
});
|
||||
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
fn manage_cancel_signal(&mut self) {
|
||||
self.stop_and_wait_current_download();
|
||||
|
||||
@@ -7,4 +7,4 @@ mod download_thread_control_flag;
|
||||
mod manifest;
|
||||
mod progress_object;
|
||||
pub mod queue;
|
||||
mod stored_manifest;
|
||||
mod stored_manifest;
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::Sender,
|
||||
Arc, Mutex,
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::Instant,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use log::info;
|
||||
@@ -19,7 +19,9 @@ pub struct ProgressObject {
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
|
||||
points_towards_update: Arc<AtomicUsize>,
|
||||
points_to_push_update: Arc<Mutex<usize>>,
|
||||
points_to_push_update: Arc<AtomicUsize>,
|
||||
last_update: Arc<RwLock<Instant>>,
|
||||
amount_last_update: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
pub struct ProgressHandle {
|
||||
@@ -58,7 +60,9 @@ impl ProgressObject {
|
||||
sender,
|
||||
|
||||
points_towards_update: Arc::new(AtomicUsize::new(0)),
|
||||
points_to_push_update: Arc::new(Mutex::new(points_to_push_update)),
|
||||
points_to_push_update: Arc::new(AtomicUsize::new(points_to_push_update)),
|
||||
last_update: Arc::new(RwLock::new(Instant::now())),
|
||||
amount_last_update: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,16 +71,44 @@ impl ProgressObject {
|
||||
.points_towards_update
|
||||
.fetch_add(amount_added, Ordering::Relaxed);
|
||||
|
||||
let to_update_handle = self.points_to_push_update.lock().unwrap();
|
||||
let to_update = *to_update_handle;
|
||||
drop(to_update_handle);
|
||||
let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed);
|
||||
|
||||
if current_amount < to_update {
|
||||
return;
|
||||
if current_amount >= to_update {
|
||||
self.points_towards_update
|
||||
.fetch_sub(to_update, Ordering::Relaxed);
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let last_update = self.last_update.read().unwrap();
|
||||
let last_update_difference = Instant::now().duration_since(*last_update).as_millis();
|
||||
if last_update_difference > 1000 {
|
||||
// push update
|
||||
drop(last_update);
|
||||
let mut last_update = self.last_update.write().unwrap();
|
||||
*last_update = Instant::now();
|
||||
drop(last_update);
|
||||
|
||||
let current_amount = self.sum();
|
||||
let max = self.get_max();
|
||||
let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed);
|
||||
self.amount_last_update
|
||||
.store(current_amount, Ordering::Relaxed);
|
||||
|
||||
let amount_since_last_update = current_amount - amount_at_last_update;
|
||||
|
||||
let kilobytes_per_second = amount_since_last_update / (last_update_difference as usize).max(1);
|
||||
|
||||
let remaining = max - current_amount; // bytes
|
||||
let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1);
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIStats(
|
||||
kilobytes_per_second,
|
||||
time_remaining,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
self.points_towards_update
|
||||
.fetch_sub(to_update, Ordering::Relaxed);
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
}
|
||||
|
||||
pub fn set_time_now(&self) {
|
||||
@@ -95,7 +127,8 @@ impl ProgressObject {
|
||||
}
|
||||
pub fn set_max(&self, new_max: usize) {
|
||||
*self.max.lock().unwrap() = new_max;
|
||||
*self.points_to_push_update.lock().unwrap() = new_max / PROGRESS_UPDATES;
|
||||
self.points_to_push_update
|
||||
.store(new_max / PROGRESS_UPDATES, Ordering::Relaxed);
|
||||
info!("points to push update: {}", new_max / PROGRESS_UPDATES);
|
||||
}
|
||||
pub fn set_size(&self, length: usize) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::{
|
||||
default,
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use log::{error, info};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_binary::binary_stream::Endian;
|
||||
|
||||
@@ -44,15 +43,15 @@ impl StoredManifest {
|
||||
}
|
||||
};
|
||||
|
||||
let manifest = match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
|
||||
|
||||
|
||||
match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
StoredManifest::new(game_id, game_version, base_path)
|
||||
}
|
||||
};
|
||||
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
|
||||
@@ -73,8 +72,8 @@ impl StoredManifest {
|
||||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
pub fn set_completed_contexts(&self, completed_contexts: &Mutex<Vec<usize>>) {
|
||||
*self.completed_contexts.lock().unwrap() = completed_contexts.lock().unwrap().clone();
|
||||
pub fn set_completed_contexts(&self, completed_contexts: &Vec<usize>) {
|
||||
*self.completed_contexts.lock().unwrap() = completed_contexts.clone();
|
||||
}
|
||||
pub fn get_completed_contexts(&self) -> Vec<usize> {
|
||||
self.completed_contexts.lock().unwrap().clone()
|
||||
|
||||
@@ -3,42 +3,46 @@ mod db;
|
||||
mod downloads;
|
||||
mod library;
|
||||
|
||||
mod cleanup;
|
||||
mod process;
|
||||
mod remote;
|
||||
mod state;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod cleanup;
|
||||
|
||||
use crate::db::DatabaseImpls;
|
||||
use auth::{auth_initiate, generate_authorization_header, recieve_handshake, retry_connect};
|
||||
use cleanup::{cleanup_and_exit, quit};
|
||||
use db::{
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface,
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameStatus,
|
||||
DATA_ROOT_DIR,
|
||||
};
|
||||
use downloads::download_commands::*;
|
||||
use downloads::download_manager::DownloadManager;
|
||||
use downloads::download_manager_builder::DownloadManagerBuilder;
|
||||
use http::Response;
|
||||
use http::{header::*, response::Builder as ResponseBuilder};
|
||||
use library::{fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, Game};
|
||||
use log::{debug, info, LevelFilter};
|
||||
use library::{
|
||||
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game,
|
||||
};
|
||||
use log::{debug, info, warn, LevelFilter};
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::append::rolling_file::RollingFileAppender;
|
||||
use log4rs::config::{Appender, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use log4rs::Config;
|
||||
use process::compat::CompatibilityManager;
|
||||
use process::process_commands::launch_game;
|
||||
use process::process_manager::ProcessManager;
|
||||
use remote::{gen_drop_url, use_remote};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
use tauri::menu::{Menu, MenuItem, MenuItemBuilder, PredefinedMenuItem};
|
||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
@@ -65,7 +69,7 @@ pub struct User {
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppState {
|
||||
pub struct AppState<'a> {
|
||||
status: AppStatus,
|
||||
user: Option<User>,
|
||||
games: HashMap<String, Game>,
|
||||
@@ -73,18 +77,20 @@ pub struct AppState {
|
||||
#[serde(skip_serializing)]
|
||||
download_manager: Arc<DownloadManager>,
|
||||
#[serde(skip_serializing)]
|
||||
process_manager: Arc<Mutex<ProcessManager>>,
|
||||
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
||||
#[serde(skip_serializing)]
|
||||
compat_manager: Arc<Mutex<CompatibilityManager>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn fetch_state(state: tauri::State<'_, Mutex<AppState>>) -> Result<AppState, String> {
|
||||
fn fetch_state(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<String, String> {
|
||||
let guard = state.lock().unwrap();
|
||||
let cloned_state = guard.clone();
|
||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||
drop(guard);
|
||||
Ok(cloned_state)
|
||||
}
|
||||
|
||||
fn setup(handle: AppHandle) -> AppState {
|
||||
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
let logfile = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
|
||||
.append(false)
|
||||
@@ -110,8 +116,9 @@ fn setup(handle: AppHandle) -> AppState {
|
||||
log4rs::init_config(config).unwrap();
|
||||
|
||||
let games = HashMap::new();
|
||||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle));
|
||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new()));
|
||||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
||||
let compat_manager = Arc::new(Mutex::new(CompatibilityManager::new()));
|
||||
|
||||
debug!("Checking if database is set up");
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
@@ -122,27 +129,72 @@ fn setup(handle: AppHandle) -> AppState {
|
||||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_manager,
|
||||
};
|
||||
}
|
||||
|
||||
debug!("Database is set up");
|
||||
|
||||
let (app_status, user) = auth::setup().unwrap();
|
||||
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let mut missing_games = Vec::new();
|
||||
let statuses = db_handle.games.statuses.clone();
|
||||
drop(db_handle);
|
||||
for (game_id, status) in statuses.into_iter() {
|
||||
match status {
|
||||
db::GameStatus::Remote {} => {}
|
||||
db::GameStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
} => {
|
||||
let install_dir_path = Path::new(&install_dir);
|
||||
if !install_dir_path.exists() {
|
||||
missing_games.push(game_id);
|
||||
}
|
||||
}
|
||||
db::GameStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
} => {
|
||||
let install_dir_path = Path::new(&install_dir);
|
||||
if !install_dir_path.exists() {
|
||||
missing_games.push(game_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("detected games missing: {:?}", missing_games);
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
for game_id in missing_games {
|
||||
db_handle
|
||||
.games
|
||||
.statuses
|
||||
.entry(game_id.to_string())
|
||||
.and_modify(|v| *v = GameStatus::Remote {});
|
||||
}
|
||||
drop(db_handle);
|
||||
info!("finished setup!");
|
||||
|
||||
AppState {
|
||||
status: app_status,
|
||||
user,
|
||||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_manager,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let mut builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init());
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[allow(unused_variables)]
|
||||
@@ -152,7 +204,7 @@ pub fn run() {
|
||||
}));
|
||||
}
|
||||
|
||||
let mut app = builder
|
||||
let app = builder
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Core utils
|
||||
@@ -178,11 +230,13 @@ pub fn run() {
|
||||
pause_game_downloads,
|
||||
resume_game_downloads,
|
||||
cancel_game,
|
||||
uninstall_game,
|
||||
// Processes
|
||||
launch_game,
|
||||
])
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_gamepad::init())
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
let state = setup(handle);
|
||||
@@ -272,8 +326,16 @@ pub fn run() {
|
||||
let response = client
|
||||
.get(object_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.unwrap();
|
||||
.send();
|
||||
if response.is_err() {
|
||||
warn!(
|
||||
"failed to fetch object with error: {}",
|
||||
response.err().unwrap()
|
||||
);
|
||||
responder.respond(Response::builder().status(500).body(Vec::new()).unwrap());
|
||||
return;
|
||||
}
|
||||
let response = response.unwrap();
|
||||
|
||||
let resp_builder = ResponseBuilder::new().header(
|
||||
CONTENT_TYPE,
|
||||
@@ -284,22 +346,16 @@ pub fn run() {
|
||||
|
||||
responder.respond(resp);
|
||||
})
|
||||
.on_window_event(|window, event| match event {
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
window.hide().unwrap();
|
||||
api.prevent_close();
|
||||
}
|
||||
_ => (),
|
||||
.on_window_event(|window, event| if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
window.hide().unwrap();
|
||||
api.prevent_close();
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|app_handle, event| match event {
|
||||
RunEvent::ExitRequested { code, api, .. } => {
|
||||
if code.is_none() {
|
||||
api.prevent_exit();
|
||||
}
|
||||
app.run(|app_handle, event| if let RunEvent::ExitRequested { code, api, .. } = event {
|
||||
if code.is_none() {
|
||||
api.prevent_exit();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use urlencoding::encode;
|
||||
|
||||
use crate::db::DatabaseImpls;
|
||||
use crate::db::GameVersion;
|
||||
use crate::db::{GameStatus, GameTransientStatus};
|
||||
use crate::downloads::download_manager::GameDownloadStatus;
|
||||
use crate::db::GameStatus;
|
||||
use crate::downloads::download_manager::{DownloadManagerStatus, GameDownloadStatus};
|
||||
use crate::process::process_manager::Platform;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::state::{GameStatusManager, GameStatusWithTransient};
|
||||
@@ -37,7 +37,7 @@ pub struct Game {
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct GameUpdateEvent {
|
||||
pub game_id: String,
|
||||
pub status: (Option<GameStatus>, Option<GameTransientStatus>),
|
||||
pub status: GameStatusWithTransient,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
@@ -50,6 +50,13 @@ pub struct QueueUpdateEventQueueData {
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct QueueUpdateEvent {
|
||||
pub queue: Vec<QueueUpdateEventQueueData>,
|
||||
pub status: DownloadManagerStatus,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct StatsUpdateEvent {
|
||||
pub speed: usize,
|
||||
pub time: usize,
|
||||
}
|
||||
|
||||
// Game version with some fields missing and size information
|
||||
@@ -232,6 +239,30 @@ pub fn fetch_game_verion_options<'a>(
|
||||
fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn uninstall_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let state_lock = state.lock().unwrap();
|
||||
state_lock.download_manager.uninstall_game(game_id);
|
||||
drop(state_lock);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_game_update(app_handle: &AppHandle, id: String, status: GameStatusWithTransient) {
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", id),
|
||||
GameUpdateEvent {
|
||||
game_id: id,
|
||||
status,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn on_game_complete(
|
||||
game_id: String,
|
||||
version_name: String,
|
||||
|
||||
51
src-tauri/src/process/compat.rs
Normal file
51
src-tauri/src/process/compat.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::{
|
||||
fs::create_dir_all,
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use crate::db::DATA_ROOT_DIR;
|
||||
|
||||
pub struct CompatibilityManager {
|
||||
compat_tools_path: PathBuf,
|
||||
prefixes_path: PathBuf,
|
||||
created_paths: AtomicBool,
|
||||
}
|
||||
|
||||
/*
|
||||
This gets built into both the Windows & Linux client, but
|
||||
we only need it in the Linux client. Therefore, it should
|
||||
do nothing but take a little bit of memory if we're on
|
||||
Windows.
|
||||
*/
|
||||
impl CompatibilityManager {
|
||||
pub fn new() -> Self {
|
||||
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
|
||||
let compat_tools_path = root_dir_lock.join("compatibility_tools");
|
||||
let prefixes_path = root_dir_lock.join("prefixes");
|
||||
drop(root_dir_lock);
|
||||
|
||||
Self {
|
||||
compat_tools_path,
|
||||
prefixes_path,
|
||||
created_paths: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_paths_exist(&self) -> Result<(), String> {
|
||||
if self.created_paths.fetch_and(true, Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
if !self.compat_tools_path.exists() {
|
||||
create_dir_all(self.compat_tools_path.clone()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
if !self.prefixes_path.exists() {
|
||||
create_dir_all(self.prefixes_path.clone()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
self.created_paths.store(true, Ordering::Relaxed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod compat;
|
||||
pub mod process_commands;
|
||||
pub mod process_manager;
|
||||
pub mod process_commands;
|
||||
@@ -3,11 +3,14 @@ use std::sync::Mutex;
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn launch_game(game_id: String, state: tauri::State<'_, Mutex<AppState>>) -> Result<(), String> {
|
||||
pub fn launch_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
|
||||
process_manager_lock.launch_game(game_id)?;
|
||||
process_manager_lock.launch_process(game_id)?;
|
||||
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Stdout, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, Command},
|
||||
sync::LazyLock,
|
||||
process::{Child, Command, ExitStatus},
|
||||
sync::{Arc, Mutex},
|
||||
thread::spawn,
|
||||
};
|
||||
|
||||
use log::info;
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::{
|
||||
db::{GameStatus, DATA_ROOT_DIR},
|
||||
DB,
|
||||
db::{GameStatus, GameTransientStatus, DATA_ROOT_DIR},
|
||||
library::push_game_update,
|
||||
state::GameStatusManager,
|
||||
AppState, DB,
|
||||
};
|
||||
|
||||
pub struct ProcessManager {
|
||||
pub struct ProcessManager<'a> {
|
||||
current_platform: Platform,
|
||||
log_output_dir: PathBuf,
|
||||
processes: HashMap<String, Child>,
|
||||
processes: HashMap<String, Arc<Mutex<Child>>>,
|
||||
app_handle: AppHandle,
|
||||
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
|
||||
}
|
||||
|
||||
impl ProcessManager {
|
||||
pub fn new() -> Self {
|
||||
impl ProcessManager<'_> {
|
||||
pub fn new(app_handle: AppHandle) -> Self {
|
||||
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
|
||||
let log_output_dir = root_dir_lock.join("logs");
|
||||
drop(root_dir_lock);
|
||||
@@ -34,12 +39,28 @@ impl ProcessManager {
|
||||
Platform::Linux
|
||||
},
|
||||
|
||||
app_handle,
|
||||
processes: HashMap::new(),
|
||||
log_output_dir,
|
||||
game_launchers: HashMap::from([
|
||||
// Current platform to target platform
|
||||
(
|
||||
(Platform::Windows, Platform::Windows),
|
||||
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Linux),
|
||||
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Windows),
|
||||
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_command(&self, install_dir: &String, raw_command: String) -> (String, Vec<String>) {
|
||||
fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec<String>) {
|
||||
let command_components = raw_command.split(" ").collect::<Vec<&str>>();
|
||||
let root = command_components[0].to_string();
|
||||
|
||||
@@ -47,41 +68,90 @@ impl ProcessManager {
|
||||
let absolute_exe = install_dir.join(root);
|
||||
|
||||
let args = command_components[1..]
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect();
|
||||
(absolute_exe.to_str().unwrap().to_owned(), args)
|
||||
(absolute_exe, args)
|
||||
}
|
||||
|
||||
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
|
||||
if !self.processes.contains_key(&game_id) {
|
||||
warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result);
|
||||
return;
|
||||
}
|
||||
|
||||
info!("process for {} exited with {:?}", game_id, result);
|
||||
|
||||
self.processes.remove(&game_id);
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle.games.transient_statuses.remove(&game_id);
|
||||
|
||||
let current_state = db_handle.games.statuses.get(&game_id).cloned();
|
||||
if let Some(saved_state) = current_state {
|
||||
if let GameStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} = saved_state {
|
||||
if let Ok(exit_code) = result {
|
||||
if exit_code.success() {
|
||||
db_handle.games.statuses.insert(
|
||||
game_id.clone(),
|
||||
GameStatus::Installed {
|
||||
version_name: version_name.to_string(),
|
||||
install_dir: install_dir.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(db_handle);
|
||||
|
||||
let status = GameStatusManager::fetch_state(&game_id);
|
||||
|
||||
push_game_update(&self.app_handle, game_id.clone(), status);
|
||||
|
||||
// TODO better management
|
||||
}
|
||||
|
||||
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
|
||||
let current = &self.current_platform;
|
||||
let valid_platforms = PROCESS_COMPATABILITY_MATRIX
|
||||
.get(current)
|
||||
.ok_or("Incomplete platform compatability matrix.")?;
|
||||
|
||||
Ok(valid_platforms.contains(platform))
|
||||
Ok(self
|
||||
.game_launchers
|
||||
.contains_key(&(current.clone(), platform.clone())))
|
||||
}
|
||||
|
||||
pub fn launch_game(&mut self, game_id: String) -> Result<(), String> {
|
||||
pub fn launch_process(&mut self, game_id: String) -> Result<(), String> {
|
||||
if self.processes.contains_key(&game_id) {
|
||||
return Err("Game or setup is already running.".to_owned());
|
||||
}
|
||||
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let mut db_lock = DB.borrow_data_mut().unwrap();
|
||||
let game_status = db_lock
|
||||
.games
|
||||
.statuses
|
||||
.get(&game_id)
|
||||
.ok_or("Game not installed")?;
|
||||
|
||||
let GameStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} = game_status
|
||||
else {
|
||||
return Err("Game not installed.".to_owned());
|
||||
let status_metadata: Option<(&String, &String)> = match game_status {
|
||||
GameStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if status_metadata.is_none() {
|
||||
return Err("Game has not been downloaded.".to_owned());
|
||||
}
|
||||
|
||||
let (version_name, install_dir) = status_metadata.unwrap();
|
||||
|
||||
let game_version = db_lock
|
||||
.games
|
||||
.versions
|
||||
@@ -90,13 +160,30 @@ impl ProcessManager {
|
||||
.get(version_name)
|
||||
.ok_or("Invalid version name".to_owned())?;
|
||||
|
||||
let (command, args) =
|
||||
self.process_command(install_dir, game_version.launch_command.clone());
|
||||
let raw_command: String = match game_status {
|
||||
GameStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => game_version.launch_command.clone(),
|
||||
GameStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => game_version.setup_command.clone(),
|
||||
_ => panic!("unreachable code"),
|
||||
};
|
||||
|
||||
info!("launching process {} in {}", command, install_dir);
|
||||
let (command, args) = self.process_command(install_dir, raw_command);
|
||||
|
||||
let target_current_dir = command.parent().unwrap().to_str().unwrap();
|
||||
|
||||
info!(
|
||||
"launching process {} in {}",
|
||||
command.to_str().unwrap(),
|
||||
target_current_dir
|
||||
);
|
||||
|
||||
let current_time = chrono::offset::Local::now();
|
||||
let mut log_file = OpenOptions::new()
|
||||
let log_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.read(true)
|
||||
@@ -107,46 +194,132 @@ impl ProcessManager {
|
||||
)
|
||||
.map_err(|v| v.to_string())?;
|
||||
|
||||
let mut error_file = OpenOptions::new()
|
||||
let error_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.open(
|
||||
self.log_output_dir
|
||||
.join(format!("{}-{}-error.log", game_id, current_time.timestamp())),
|
||||
)
|
||||
.open(self.log_output_dir.join(format!(
|
||||
"{}-{}-error.log",
|
||||
game_id,
|
||||
current_time.timestamp()
|
||||
)))
|
||||
.map_err(|v| v.to_string())?;
|
||||
|
||||
info!("opened log file for {}", command);
|
||||
let current_platform = self.current_platform.clone();
|
||||
let target_platform = game_version.platform.clone();
|
||||
|
||||
let launch_process = Command::new(command)
|
||||
.current_dir(install_dir)
|
||||
.stdout(log_file)
|
||||
.stderr(error_file)
|
||||
.args(args)
|
||||
.spawn()
|
||||
.map_err(|v| v.to_string())?;
|
||||
let game_launcher = self
|
||||
.game_launchers
|
||||
.get(&(current_platform, target_platform))
|
||||
.ok_or("Invalid version for this platform.")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
self.processes.insert(game_id, launch_process);
|
||||
let launch_process = game_launcher.launch_process(
|
||||
&game_id,
|
||||
version_name,
|
||||
command.to_str().unwrap().to_owned(),
|
||||
args,
|
||||
&target_current_dir.to_string(),
|
||||
log_file,
|
||||
error_file,
|
||||
)?;
|
||||
|
||||
let launch_process_handle = Arc::new(Mutex::new(launch_process));
|
||||
|
||||
db_lock
|
||||
.games
|
||||
.transient_statuses
|
||||
.insert(game_id.clone(), GameTransientStatus::Running {});
|
||||
|
||||
push_game_update(
|
||||
&self.app_handle,
|
||||
game_id.clone(),
|
||||
(None, Some(GameTransientStatus::Running {})),
|
||||
);
|
||||
|
||||
let wait_thread_handle = launch_process_handle.clone();
|
||||
let wait_thread_apphandle = self.app_handle.clone();
|
||||
let wait_thread_game_id = game_id.clone();
|
||||
|
||||
spawn(move || {
|
||||
let mut child_handle = wait_thread_handle.lock().unwrap();
|
||||
let result: Result<ExitStatus, std::io::Error> = child_handle.wait();
|
||||
|
||||
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
|
||||
let app_state_handle = app_state.lock().unwrap();
|
||||
|
||||
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
|
||||
process_manager_handle.on_process_finish(wait_thread_game_id, result);
|
||||
|
||||
// As everything goes out of scope, they should get dropped
|
||||
// But just to explicit about it
|
||||
drop(process_manager_handle);
|
||||
drop(app_state_handle);
|
||||
drop(child_handle);
|
||||
});
|
||||
|
||||
self.processes.insert(game_id, launch_process_handle);
|
||||
|
||||
info!("finished spawning process");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone)]
|
||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum Platform {
|
||||
Windows,
|
||||
Linux,
|
||||
}
|
||||
|
||||
pub type ProcessCompatabilityMatrix = HashMap<Platform, Vec<Platform>>;
|
||||
pub static PROCESS_COMPATABILITY_MATRIX: LazyLock<ProcessCompatabilityMatrix> =
|
||||
LazyLock::new(|| {
|
||||
let mut matrix: ProcessCompatabilityMatrix = HashMap::new();
|
||||
pub trait ProcessHandler: Send + 'static {
|
||||
fn launch_process(
|
||||
&self,
|
||||
game_id: &String,
|
||||
version_name: &String,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, String>;
|
||||
}
|
||||
|
||||
matrix.insert(Platform::Windows, vec![Platform::Windows]);
|
||||
matrix.insert(Platform::Linux, vec![Platform::Linux]); // TODO: add Proton support
|
||||
struct NativeGameLauncher;
|
||||
impl ProcessHandler for NativeGameLauncher {
|
||||
fn launch_process(
|
||||
&self,
|
||||
game_id: &String,
|
||||
version_name: &String,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, String> {
|
||||
Command::new(command)
|
||||
.current_dir(current_dir)
|
||||
.stdout(log_file)
|
||||
.stderr(error_file)
|
||||
.args(args)
|
||||
.spawn()
|
||||
.map_err(|v| v.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
return matrix;
|
||||
});
|
||||
struct UMULauncher;
|
||||
impl ProcessHandler for UMULauncher {
|
||||
fn launch_process(
|
||||
&self,
|
||||
game_id: &String,
|
||||
version_name: &String,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, String> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Formatter},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
@@ -16,23 +17,33 @@ pub enum RemoteAccessError {
|
||||
ParsingError(ParseError),
|
||||
InvalidCodeError(u16),
|
||||
InvalidEndpoint,
|
||||
HandshakeFailed,
|
||||
HandshakeFailed(String),
|
||||
GameNotFound,
|
||||
InvalidResponse,
|
||||
InvalidRedirect,
|
||||
ManifestDownloadFailed(StatusCode, String),
|
||||
OutOfSync,
|
||||
}
|
||||
|
||||
impl Display for RemoteAccessError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RemoteAccessError::FetchError(error) => write!(f, "{}", error),
|
||||
RemoteAccessError::FetchError(error) => write!(
|
||||
f,
|
||||
"{}: {}",
|
||||
error,
|
||||
error
|
||||
.source()
|
||||
.map(|e| e.to_string())
|
||||
.or_else(|| Some("Unknown error".to_string()))
|
||||
.unwrap()
|
||||
),
|
||||
RemoteAccessError::ParsingError(parse_error) => {
|
||||
write!(f, "{}", parse_error)
|
||||
}
|
||||
RemoteAccessError::InvalidCodeError(error) => write!(f, "Invalid HTTP code {}", error),
|
||||
RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
|
||||
RemoteAccessError::HandshakeFailed => write!(f, "Failed to complete handshake"),
|
||||
RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message),
|
||||
RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),
|
||||
RemoteAccessError::InvalidResponse => write!(f, "Server returned an invalid response"),
|
||||
RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"),
|
||||
@@ -41,6 +52,7 @@ impl Display for RemoteAccessError {
|
||||
"Failed to download game manifest: {} {}",
|
||||
status, response
|
||||
),
|
||||
RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +75,15 @@ impl From<u16> for RemoteAccessError {
|
||||
|
||||
impl std::error::Error for RemoteAccessError {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DropServerError {
|
||||
pub status_code: usize,
|
||||
pub status_message: String,
|
||||
pub message: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DropHealthcheck {
|
||||
@@ -71,7 +92,7 @@ struct DropHealthcheck {
|
||||
|
||||
async fn use_remote_logic<'a>(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'a>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
info!("connecting to url {}", url);
|
||||
let base_url = Url::parse(&url)?;
|
||||
@@ -103,7 +124,7 @@ async fn use_remote_logic<'a>(
|
||||
#[tauri::command]
|
||||
pub async fn use_remote<'a>(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'a>>>,
|
||||
) -> Result<(), String> {
|
||||
let result = use_remote_logic(url, state).await;
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
db::{GameStatus, GameTransientStatus},
|
||||
DB,
|
||||
};
|
||||
|
||||
pub type GameStatusWithTransient = (
|
||||
Option<GameStatus>,
|
||||
Option<GameTransientStatus>,
|
||||
);
|
||||
pub type GameStatusWithTransient = (Option<GameStatus>, Option<GameTransientStatus>);
|
||||
pub struct GameStatusManager {}
|
||||
|
||||
impl GameStatusManager {
|
||||
@@ -26,6 +22,6 @@ impl GameStatusManager {
|
||||
return (offline_state, None);
|
||||
}
|
||||
|
||||
return (None, None);
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
],
|
||||
"externalBin": []
|
||||
}
|
||||
}
|
||||
|
||||
1
types.ts
1
types.ts
@@ -52,6 +52,7 @@ export enum GameStatusEnum {
|
||||
Updating = "Updating",
|
||||
Uninstalling = "Uninstalling",
|
||||
SetupRequired = "SetupRequired",
|
||||
Running = "Running"
|
||||
}
|
||||
|
||||
export type GameStatus = {
|
||||
|
||||
40
yarn.lock
40
yarn.lock
@@ -773,6 +773,11 @@
|
||||
semver "^7.3.5"
|
||||
tar "^6.1.11"
|
||||
|
||||
"@maulingmonkey/gamepad@^0.0.5":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@maulingmonkey/gamepad/-/gamepad-0.0.5.tgz#50220895cc310f66f0678e07fa1e17959332ac20"
|
||||
integrity sha512-dokZlauyTVAW2UEFC8T/L+1Oc7oK+vgakjfa4JZQPxkSoCTf7e3l0zV3E/Gys3Ps43Mmc8mnX+7d1ICTyD5pcg==
|
||||
|
||||
"@netlify/functions@^2.8.0":
|
||||
version "2.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-2.8.2.tgz#653395b901a74a6189e913a089f9cb90083ca6ce"
|
||||
@@ -1307,6 +1312,11 @@
|
||||
dependencies:
|
||||
"@tanstack/virtual-core" "3.10.8"
|
||||
|
||||
"@tauri-apps/api@2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.1.1.tgz#77d4ddb683d31072de4e6a47c8613d9db011652b"
|
||||
integrity sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==
|
||||
|
||||
"@tauri-apps/api@>=2.0.0", "@tauri-apps/api@^2.0.0":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.1.tgz#dc49d899fb873b96ee1d46a171384625ba5ad404"
|
||||
@@ -1392,6 +1402,13 @@
|
||||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@tauri-apps/plugin-os@~2":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.2.0.tgz#ef5511269f59c0ccc580a9d09600034cfaa9743b"
|
||||
integrity sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==
|
||||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@tauri-apps/plugin-shell@>=2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz#b6fc88ab070fd5f620e46405715779aa44eb8428"
|
||||
@@ -2769,6 +2786,11 @@ escape-string-regexp@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
|
||||
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
|
||||
|
||||
esm-gamecontroller.js@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/esm-gamecontroller.js/-/esm-gamecontroller.js-1.0.4.tgz#215998947c796ead90133b4cb2cef7852729d985"
|
||||
integrity sha512-787c6eP+DwFJqUsAudnb0zt7ipG+pGYjVT1vyIecsVpRW/GJw5YGV5FQT5asuMQ2k1JCnTY3MTVEUftKkyYoNw==
|
||||
|
||||
estree-walker@2.0.2, estree-walker@^2.0.1, estree-walker@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||
@@ -2945,6 +2967,11 @@ function-bind@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
gamepad-events@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/gamepad-events/-/gamepad-events-0.7.0.tgz#63566540dd930f7d5997335a401118b390399494"
|
||||
integrity sha512-930McsulrWFlGqzlC5plW4WQhqL+3PZ9PAaICqT7OtQLSQ5BzF+wJ32LPzjpZYaoCH7NzvEgtzadqZs8rzNkJw==
|
||||
|
||||
gauge@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
|
||||
@@ -3928,11 +3955,6 @@ mlly@^1.3.0, mlly@^1.4.2, mlly@^1.6.1, mlly@^1.7.1:
|
||||
pkg-types "^1.2.0"
|
||||
ufo "^1.5.4"
|
||||
|
||||
moment@^2.30.1:
|
||||
version "2.30.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
|
||||
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
|
||||
|
||||
mri@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
@@ -5580,6 +5602,14 @@ tar@^6.1.11, tar@^6.2.0:
|
||||
mkdirp "^1.0.3"
|
||||
yallist "^4.0.0"
|
||||
|
||||
tauri-plugin-gamepad-api@^0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/tauri-plugin-gamepad-api/-/tauri-plugin-gamepad-api-0.0.5.tgz#39f9137421b632956f1d617a239b08814ce2c19f"
|
||||
integrity sha512-WO0xwXmPr4PMe9HzljKUbTkGv7J6ur2Df/OuEPsxBeSA5QPffAJR3N32M4pM4yW72D9Vt19XplPnHHDVATVMSw==
|
||||
dependencies:
|
||||
"@tauri-apps/api" "2.1.1"
|
||||
tslib "^2.1.0"
|
||||
|
||||
terser@^5.17.4:
|
||||
version "5.34.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.34.1.tgz#af40386bdbe54af0d063e0670afd55c3105abeb6"
|
||||
|
||||
Reference in New Issue
Block a user