mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-20 03:31:23 +10:00
Compare commits
13 Commits
AdenMGB-sm
...
e8b9ec020d
| Author | SHA1 | Date | |
|---|---|---|---|
| e8b9ec020d | |||
| cc417d6d69 | |||
| f733fbba65 | |||
| 0d78375f54 | |||
| d6a9994f8b | |||
| dcb7455954 | |||
| 70cecdad19 | |||
| 463c5e6f3b | |||
| 83dc773b10 | |||
| ddde547c08 | |||
| be5500d29f | |||
| ea6fa551a2 | |||
| be4fc2d37a |
24
README.md
24
README.md
@ -1,29 +1,21 @@
|
|||||||
# Drop App
|
# Drop Desktop Client
|
||||||
|
|
||||||
Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
|
The Drop Desktop Client is the companion app for [Drop](https://github.com/Drop-OSS/drop). It is the official & intended way to download and play games on your Drop server.
|
||||||
|
|
||||||
## Running
|
## Internals
|
||||||
Before setting up the drop app, be sure that you have a server set up.
|
|
||||||
The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart)
|
|
||||||
|
|
||||||
## Current features
|
It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
|
||||||
Currently supported are the following features:
|
|
||||||
- Signin (with custom server)
|
|
||||||
- Database registering & recovery
|
|
||||||
- Dynamic library fetching from server
|
|
||||||
- Installing & uninstalling games
|
|
||||||
- Download progress monitoring
|
|
||||||
- Launching / playing games
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
Before setting up a development environemnt, be sure that you have a server set up. The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart).
|
||||||
|
|
||||||
Install dependencies with `yarn`
|
Then, install dependencies with `yarn`. This'll install the custom builder's dependencies. Then, check everything works properly with `yarn tauri build`.
|
||||||
|
|
||||||
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
|
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
|
||||||
|
|
||||||
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
|
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
|
||||||
|
|
||||||
e.g. `RUST_LOG=debug yarn tauri dev`
|
e.g. `RUST_LOG=debug yarn tauri dev`
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines.
|
Check out the contributing guide on our Developer Docs: [Drop Developer Docs - Contributing](https://developer.droposs.org/contributing).
|
||||||
|
|||||||
@ -44,10 +44,6 @@ router.beforeEach(async () => {
|
|||||||
setupHooks();
|
setupHooks();
|
||||||
initialNavigation(state);
|
initialNavigation(state);
|
||||||
|
|
||||||
// Setup playtime event listeners
|
|
||||||
const { setupEventListeners } = usePlaytime();
|
|
||||||
setupEventListeners();
|
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "Drop",
|
title: "Drop",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="stats" class="flex flex-col gap-1">
|
|
||||||
<!-- Main playtime display -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<ClockIcon class="w-5 h-5 text-zinc-400" />
|
|
||||||
<span class="text-base text-zinc-300 font-medium">
|
|
||||||
{{ formatPlaytime(stats.totalPlaytimeSeconds) }} played
|
|
||||||
</span>
|
|
||||||
<span v-if="isActive && showActiveIndicator" class="text-sm text-green-400 font-medium">
|
|
||||||
• Playing
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional details when expanded -->
|
|
||||||
<div v-if="showDetails" class="text-xs text-zinc-400 space-y-1 ml-7">
|
|
||||||
<div>{{ stats.sessionCount }} session{{ stats.sessionCount !== 1 ? 's' : '' }}</div>
|
|
||||||
<div v-if="stats.sessionCount > 0">
|
|
||||||
Avg: {{ formatPlaytime(stats.averageSessionLength) }} per session
|
|
||||||
</div>
|
|
||||||
<div v-if="stats.currentSessionDuration">
|
|
||||||
Current session: {{ formatPlaytime(stats.currentSessionDuration) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No playtime data -->
|
|
||||||
<div v-else-if="showWhenEmpty" class="flex items-center gap-2 text-zinc-500">
|
|
||||||
<ClockIcon class="w-5 h-5" />
|
|
||||||
<span class="text-base">Never played</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ClockIcon } from "@heroicons/vue/20/solid";
|
|
||||||
import type { GamePlaytimeStats } from "~/types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
stats: GamePlaytimeStats | null;
|
|
||||||
isActive?: boolean;
|
|
||||||
showDetails?: boolean;
|
|
||||||
showWhenEmpty?: boolean;
|
|
||||||
showActiveIndicator?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
isActive: false,
|
|
||||||
showDetails: false,
|
|
||||||
showWhenEmpty: true,
|
|
||||||
showActiveIndicator: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { formatPlaytime } = usePlaytime();
|
|
||||||
</script>
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="bg-zinc-800/50 rounded-lg p-4 space-y-4">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<ChartBarIcon class="w-5 h-5 text-zinc-400" />
|
|
||||||
<h3 class="text-lg font-semibold text-zinc-100">Playtime Statistics</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="stats" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<!-- Total Playtime -->
|
|
||||||
<div class="bg-zinc-700/50 rounded-lg p-3">
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<ClockIcon class="w-4 h-4 text-blue-400" />
|
|
||||||
<span class="text-sm font-medium text-zinc-300">Total Playtime</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-2xl font-bold text-zinc-100">
|
|
||||||
{{ formatDetailedPlaytime(stats.totalPlaytimeSeconds) }}
|
|
||||||
</div>
|
|
||||||
<div v-if="stats.currentSessionDuration" class="text-xs text-green-400 mt-1">
|
|
||||||
+{{ formatPlaytime(stats.currentSessionDuration) }} this session
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sessions -->
|
|
||||||
<div class="bg-zinc-700/50 rounded-lg p-3">
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<PlayIcon class="w-4 h-4 text-green-400" />
|
|
||||||
<span class="text-sm font-medium text-zinc-300">Sessions</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-2xl font-bold text-zinc-100">
|
|
||||||
{{ stats.sessionCount }}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-zinc-400 mt-1">
|
|
||||||
Avg: {{ formatPlaytime(stats.averageSessionLength) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No stats available -->
|
|
||||||
<div v-else class="text-center py-8">
|
|
||||||
<ClockIcon class="w-12 h-12 text-zinc-600 mx-auto mb-3" />
|
|
||||||
<p class="text-zinc-400">No playtime data available</p>
|
|
||||||
<p class="text-sm text-zinc-500 mt-1">Statistics will appear after you start playing</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Current session indicator -->
|
|
||||||
<div v-if="isActive && stats" class="border-t border-zinc-700 pt-3">
|
|
||||||
<div class="flex items-center gap-2 text-green-400">
|
|
||||||
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
|
||||||
<span class="text-sm font-medium">Currently playing</span>
|
|
||||||
<span v-if="stats.currentSessionDuration" class="text-xs text-zinc-400">
|
|
||||||
{{ formatPlaytime(stats.currentSessionDuration) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
ChartBarIcon,
|
|
||||||
ClockIcon,
|
|
||||||
PlayIcon
|
|
||||||
} from "@heroicons/vue/20/solid";
|
|
||||||
import type { GamePlaytimeStats } from "~/types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
stats: GamePlaytimeStats | null;
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
isActive: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { formatPlaytime, formatDetailedPlaytime } = usePlaytime();
|
|
||||||
</script>
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
|
||||||
import type {
|
|
||||||
GamePlaytimeStats,
|
|
||||||
PlaytimeUpdateEvent,
|
|
||||||
PlaytimeSessionStartEvent,
|
|
||||||
PlaytimeSessionEndEvent
|
|
||||||
} from "~/types";
|
|
||||||
|
|
||||||
export const usePlaytime = () => {
|
|
||||||
const playtimeStats = useState<Record<string, GamePlaytimeStats>>('playtime-stats', () => ({}));
|
|
||||||
const activeSessions = useState<Set<string>>('active-sessions', () => new Set());
|
|
||||||
|
|
||||||
// Fetch playtime stats for a specific game
|
|
||||||
const fetchGamePlaytime = async (gameId: string): Promise<GamePlaytimeStats | null> => {
|
|
||||||
try {
|
|
||||||
const stats = await invoke<GamePlaytimeStats | null>("fetch_game_playtime", { gameId });
|
|
||||||
if (stats) {
|
|
||||||
playtimeStats.value[gameId] = stats;
|
|
||||||
}
|
|
||||||
return stats;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch playtime for game ${gameId}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch all playtime stats
|
|
||||||
const fetchAllPlaytimeStats = async (): Promise<Record<string, GamePlaytimeStats>> => {
|
|
||||||
try {
|
|
||||||
const stats = await invoke<Record<string, GamePlaytimeStats>>("fetch_all_playtime_stats");
|
|
||||||
playtimeStats.value = stats;
|
|
||||||
return stats;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch all playtime stats:", error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if a session is active
|
|
||||||
const isSessionActive = async (gameId: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
return await invoke<boolean>("is_playtime_session_active", { gameId });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to check session status for game ${gameId}:`, error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get all active sessions
|
|
||||||
const getActiveSessions = async (): Promise<string[]> => {
|
|
||||||
try {
|
|
||||||
const sessions = await invoke<string[]>("get_active_playtime_sessions");
|
|
||||||
activeSessions.value = new Set(sessions);
|
|
||||||
return sessions;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to get active sessions:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format playtime duration
|
|
||||||
const formatPlaytime = (seconds: number): string => {
|
|
||||||
if (seconds < 60) {
|
|
||||||
return `${seconds}s`;
|
|
||||||
} else if (seconds < 3600) {
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
return `${minutes}m`;
|
|
||||||
} else {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
if (minutes === 0) {
|
|
||||||
return `${hours}h`;
|
|
||||||
}
|
|
||||||
return `${hours}h ${minutes}m`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format detailed playtime
|
|
||||||
const formatDetailedPlaytime = (seconds: number): string => {
|
|
||||||
if (seconds < 60) {
|
|
||||||
return `${seconds} seconds`;
|
|
||||||
} else if (seconds < 3600) {
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const remainingSeconds = seconds % 60;
|
|
||||||
if (remainingSeconds === 0) {
|
|
||||||
return `${minutes} minutes`;
|
|
||||||
}
|
|
||||||
return `${minutes} minutes, ${remainingSeconds} seconds`;
|
|
||||||
} else {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
if (minutes === 0) {
|
|
||||||
return `${hours} hours`;
|
|
||||||
}
|
|
||||||
return `${hours} hours, ${minutes} minutes`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format relative time (e.g., "2 hours ago")
|
|
||||||
const formatRelativeTime = (timestamp: string): string => {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const now = new Date();
|
|
||||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
||||||
|
|
||||||
if (diffInSeconds < 60) {
|
|
||||||
return "Just now";
|
|
||||||
} else if (diffInSeconds < 3600) {
|
|
||||||
const minutes = Math.floor(diffInSeconds / 60);
|
|
||||||
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
|
||||||
} else if (diffInSeconds < 86400) {
|
|
||||||
const hours = Math.floor(diffInSeconds / 3600);
|
|
||||||
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
|
||||||
} else if (diffInSeconds < 604800) {
|
|
||||||
const days = Math.floor(diffInSeconds / 86400);
|
|
||||||
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
|
||||||
} else {
|
|
||||||
return date.toLocaleDateString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get playtime stats for a game (from cache or fetch)
|
|
||||||
const getGamePlaytime = async (gameId: string): Promise<GamePlaytimeStats | null> => {
|
|
||||||
if (playtimeStats.value[gameId]) {
|
|
||||||
return playtimeStats.value[gameId];
|
|
||||||
}
|
|
||||||
return await fetchGamePlaytime(gameId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup event listeners
|
|
||||||
const setupEventListeners = () => {
|
|
||||||
// Listen for general playtime updates
|
|
||||||
listen<PlaytimeUpdateEvent>("playtime_update", (event) => {
|
|
||||||
const { gameId, stats, isActive } = event.payload;
|
|
||||||
playtimeStats.value[gameId] = stats;
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
activeSessions.value.add(gameId);
|
|
||||||
} else {
|
|
||||||
activeSessions.value.delete(gameId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for session start events
|
|
||||||
listen<PlaytimeSessionStartEvent>("playtime_session_start", (event) => {
|
|
||||||
const { gameId } = event.payload;
|
|
||||||
activeSessions.value.add(gameId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for session end events
|
|
||||||
listen<PlaytimeSessionEndEvent>("playtime_session_end", (event) => {
|
|
||||||
const { gameId } = event.payload;
|
|
||||||
activeSessions.value.delete(gameId);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup game-specific event listeners
|
|
||||||
const setupGameEventListeners = (gameId: string) => {
|
|
||||||
listen<PlaytimeUpdateEvent>(`playtime_update/${gameId}`, (event) => {
|
|
||||||
const { stats, isActive } = event.payload;
|
|
||||||
playtimeStats.value[gameId] = stats;
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
activeSessions.value.add(gameId);
|
|
||||||
} else {
|
|
||||||
activeSessions.value.delete(gameId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listen<PlaytimeSessionStartEvent>(`playtime_session_start/${gameId}`, () => {
|
|
||||||
activeSessions.value.add(gameId);
|
|
||||||
});
|
|
||||||
|
|
||||||
listen<PlaytimeSessionEndEvent>(`playtime_session_end/${gameId}`, () => {
|
|
||||||
activeSessions.value.delete(gameId);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
playtimeStats: readonly(playtimeStats),
|
|
||||||
activeSessions: readonly(activeSessions),
|
|
||||||
fetchGamePlaytime,
|
|
||||||
fetchAllPlaytimeStats,
|
|
||||||
isSessionActive,
|
|
||||||
getActiveSessions,
|
|
||||||
formatPlaytime,
|
|
||||||
formatDetailedPlaytime,
|
|
||||||
formatRelativeTime,
|
|
||||||
getGamePlaytime,
|
|
||||||
setupEventListeners,
|
|
||||||
setupGameEventListeners,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -18,21 +18,11 @@
|
|||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<div class="px-8 pb-4">
|
<div class="px-8 pb-4">
|
||||||
<h1
|
<h1
|
||||||
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-4"
|
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8"
|
||||||
>
|
>
|
||||||
{{ game.mName }}
|
{{ game.mName }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Playtime Display -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<PlaytimeDisplay
|
|
||||||
:stats="gamePlaytime"
|
|
||||||
:is-active="isPlaytimeActive"
|
|
||||||
:show-details="false"
|
|
||||||
:show-active-indicator="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-row gap-x-4 items-stretch mb-8">
|
<div class="flex flex-row gap-x-4 items-stretch mb-8">
|
||||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||||
<GameStatusButton
|
<GameStatusButton
|
||||||
@ -70,12 +60,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Playtime Statistics -->
|
|
||||||
<PlaytimeStats
|
|
||||||
:stats="gamePlaytime"
|
|
||||||
:is-active="isPlaytimeActive"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
||||||
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
|
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
|
||||||
Game Images
|
Game Images
|
||||||
@ -544,19 +528,6 @@ const currentImageIndex = ref(0);
|
|||||||
|
|
||||||
const configureModalOpen = ref(false);
|
const configureModalOpen = ref(false);
|
||||||
|
|
||||||
// Playtime tracking
|
|
||||||
const {
|
|
||||||
getGamePlaytime,
|
|
||||||
setupGameEventListeners,
|
|
||||||
activeSessions
|
|
||||||
} = usePlaytime();
|
|
||||||
|
|
||||||
const gamePlaytime = ref(await getGamePlaytime(id));
|
|
||||||
const isPlaytimeActive = computed(() => activeSessions.value.has(id));
|
|
||||||
|
|
||||||
// Setup playtime event listeners for this game
|
|
||||||
setupGameEventListeners(id);
|
|
||||||
|
|
||||||
async function installFlow() {
|
async function installFlow() {
|
||||||
installFlowOpen.value = true;
|
installFlowOpen.value = true;
|
||||||
versionOptions.value = undefined;
|
versionOptions.value = undefined;
|
||||||
|
|||||||
@ -94,37 +94,3 @@ export type Settings = {
|
|||||||
maxDownloadThreads: number;
|
maxDownloadThreads: number;
|
||||||
forceOffline: boolean;
|
forceOffline: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GamePlaytimeStats = {
|
|
||||||
gameId: string;
|
|
||||||
totalPlaytimeSeconds: number;
|
|
||||||
sessionCount: number;
|
|
||||||
firstPlayed: string;
|
|
||||||
lastPlayed: string;
|
|
||||||
averageSessionLength: number;
|
|
||||||
currentSessionDuration?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlaytimeSession = {
|
|
||||||
gameId: string;
|
|
||||||
startTime: string;
|
|
||||||
sessionId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlaytimeUpdateEvent = {
|
|
||||||
gameId: string;
|
|
||||||
stats: GamePlaytimeStats;
|
|
||||||
isActive: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlaytimeSessionStartEvent = {
|
|
||||||
gameId: string;
|
|
||||||
startTime: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlaytimeSessionEndEvent = {
|
|
||||||
gameId: string;
|
|
||||||
sessionDurationSeconds: number;
|
|
||||||
totalPlaytimeSeconds: number;
|
|
||||||
sessionCount: number;
|
|
||||||
};
|
|
||||||
@ -14,7 +14,8 @@
|
|||||||
"@tauri-apps/plugin-os": "^2.3.0",
|
"@tauri-apps/plugin-os": "^2.3.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||||
"pino": "^9.7.0",
|
"pino": "^9.7.0",
|
||||||
"pino-pretty": "^13.1.1"
|
"pino-pretty": "^13.1.1",
|
||||||
|
"tauri": "^0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.7.1"
|
"@tauri-apps/cli": "^2.7.1"
|
||||||
|
|||||||
12
src-tauri/Cargo.lock
generated
12
src-tauri/Cargo.lock
generated
@ -5671,9 +5671,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.2.2"
|
version = "2.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8"
|
checksum = "37e5858cc7b455a73ab4ea2ebc08b5be33682c00ff1bf4cad5537d4fb62499d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
@ -5689,9 +5689,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs"
|
name = "tauri-plugin-fs"
|
||||||
version = "2.3.0"
|
version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da"
|
checksum = "8c6ef84ee2f2094ce093e55106d90d763ba343fad57566992962e8f76d113f99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dunce",
|
"dunce",
|
||||||
@ -5751,9 +5751,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.2.1"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d"
|
checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::{lock, AppState};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||||
@ -10,7 +10,7 @@ pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppS
|
|||||||
|
|
||||||
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||||
debug!("cleaning up and exiting application");
|
debug!("cleaning up and exiting application");
|
||||||
let download_manager = state.lock().unwrap().download_manager.clone();
|
let download_manager = lock!(state).download_manager.clone();
|
||||||
match download_manager.ensure_terminated() {
|
match download_manager.ensure_terminated() {
|
||||||
Ok(res) => match res {
|
Ok(res) => match res {
|
||||||
Ok(()) => debug!("download manager terminated correctly"),
|
Ok(()) => debug!("download manager terminated correctly"),
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
use crate::AppState;
|
use crate::{lock, AppState};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_state(
|
pub fn fetch_state(
|
||||||
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let guard = state.lock().unwrap();
|
let guard = lock!(state);
|
||||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
Ok(cloned_state)
|
Ok(cloned_state)
|
||||||
|
|||||||
@ -67,11 +67,15 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn update_settings(new_settings: Value) {
|
pub fn update_settings(new_settings: Value) {
|
||||||
let mut db_lock = borrow_db_mut_checked();
|
let mut db_lock = borrow_db_mut_checked();
|
||||||
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
|
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).expect("Failed to parse existing settings");
|
||||||
for (key, value) in new_settings.as_object().unwrap() {
|
let values = match new_settings.as_object() {
|
||||||
|
Some(values) => values,
|
||||||
|
None => { panic!("Could not parse settings values"); },
|
||||||
|
};
|
||||||
|
for (key, value) in values {
|
||||||
current_settings[key] = value.clone();
|
current_settings[key] = value.clone();
|
||||||
}
|
}
|
||||||
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
|
let new_settings: Settings = serde_json::from_value(current_settings).unwrap_or_else(|e| panic!("Failed to parse settings with error {}", e));
|
||||||
db_lock.settings = new_settings;
|
db_lock.settings = new_settings;
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@ -21,8 +21,13 @@ static DATA_ROOT_PREFIX: &'static str = "drop";
|
|||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
static DATA_ROOT_PREFIX: &str = "drop-debug";
|
static DATA_ROOT_PREFIX: &str = "drop-debug";
|
||||||
|
|
||||||
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> =
|
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = LazyLock::new(|| {
|
||||||
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join(DATA_ROOT_PREFIX)));
|
Arc::new(
|
||||||
|
dirs::data_dir()
|
||||||
|
.expect("Failed to get data dir")
|
||||||
|
.join(DATA_ROOT_PREFIX),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
// Custom JSON serializer to support everything we need
|
// Custom JSON serializer to support everything we need
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
@ -63,13 +68,49 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
let pfx_dir = DATA_ROOT_DIR.join("pfx");
|
let pfx_dir = DATA_ROOT_DIR.join("pfx");
|
||||||
|
|
||||||
debug!("creating data directory at {DATA_ROOT_DIR:?}");
|
debug!("creating data directory at {DATA_ROOT_DIR:?}");
|
||||||
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap();
|
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap_or_else(|e| {
|
||||||
create_dir_all(&games_base_dir).unwrap();
|
panic!(
|
||||||
create_dir_all(&logs_root_dir).unwrap();
|
"Failed to create directory {} with error {}",
|
||||||
create_dir_all(&cache_dir).unwrap();
|
DATA_ROOT_DIR.display(),
|
||||||
create_dir_all(&pfx_dir).unwrap();
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
create_dir_all(&games_base_dir).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to create directory {} with error {}",
|
||||||
|
games_base_dir.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
create_dir_all(&logs_root_dir).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to create directory {} with error {}",
|
||||||
|
logs_root_dir.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
create_dir_all(&cache_dir).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to create directory {} with error {}",
|
||||||
|
cache_dir.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
create_dir_all(&pfx_dir).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to create directory {} with error {}",
|
||||||
|
pfx_dir.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let exists = fs::exists(db_path.clone()).unwrap();
|
let exists = fs::exists(db_path.clone()).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to find if {} exists with error {}",
|
||||||
|
db_path.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
match PathDatabase::load_from_path(db_path.clone()) {
|
match PathDatabase::load_from_path(db_path.clone()) {
|
||||||
@ -78,21 +119,19 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let default = Database::new(games_base_dir, None, cache_dir);
|
let default = Database::new(games_base_dir, None, cache_dir);
|
||||||
debug!(
|
debug!("Creating database at path {}", db_path.display());
|
||||||
"Creating database at path {}",
|
|
||||||
db_path.as_os_str().to_str().unwrap()
|
|
||||||
);
|
|
||||||
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
|
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn database_is_set_up(&self) -> bool {
|
fn database_is_set_up(&self) -> bool {
|
||||||
!self.borrow_data().unwrap().base_url.is_empty()
|
!borrow_db_checked().base_url.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_base_url(&self) -> Url {
|
fn fetch_base_url(&self) -> Url {
|
||||||
let handle = self.borrow_data().unwrap();
|
let handle = borrow_db_checked();
|
||||||
Url::parse(&handle.base_url).unwrap()
|
Url::parse(&handle.base_url)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to parse base url {}", handle.base_url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,13 +150,16 @@ fn handle_invalid_database(
|
|||||||
base
|
base
|
||||||
};
|
};
|
||||||
info!("old database stored at: {}", new_path.to_string_lossy());
|
info!("old database stored at: {}", new_path.to_string_lossy());
|
||||||
fs::rename(&db_path, &new_path).unwrap();
|
fs::rename(&db_path, &new_path).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Could not rename database {} to {} with error {}",
|
||||||
|
db_path.display(),
|
||||||
|
new_path.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let db = Database::new(
|
let db = Database::new(games_base_dir, Some(new_path), cache_dir);
|
||||||
games_base_dir.into_os_string().into_string().unwrap(),
|
|
||||||
Some(new_path),
|
|
||||||
cache_dir,
|
|
||||||
);
|
|
||||||
|
|
||||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ pub mod data {
|
|||||||
// Declare it using the actual version that it is from, i.e. v1::Settings rather than just Settings from here
|
// Declare it using the actual version that it is from, i.e. v1::Settings rather than just Settings from here
|
||||||
|
|
||||||
pub type GameVersion = v1::GameVersion;
|
pub type GameVersion = v1::GameVersion;
|
||||||
pub type Database = v4::Database;
|
pub type Database = v3::Database;
|
||||||
pub type Settings = v1::Settings;
|
pub type Settings = v1::Settings;
|
||||||
pub type DatabaseAuth = v1::DatabaseAuth;
|
pub type DatabaseAuth = v1::DatabaseAuth;
|
||||||
|
|
||||||
@ -20,10 +20,7 @@ pub mod data {
|
|||||||
pub type DownloadableMetadata = v1::DownloadableMetadata;
|
pub type DownloadableMetadata = v1::DownloadableMetadata;
|
||||||
pub type DownloadType = v1::DownloadType;
|
pub type DownloadType = v1::DownloadType;
|
||||||
pub type DatabaseApplications = v2::DatabaseApplications;
|
pub type DatabaseApplications = v2::DatabaseApplications;
|
||||||
//pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
// pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||||
pub type PlaytimeData = v4::PlaytimeData;
|
|
||||||
pub type GamePlaytimeStats = v4::GamePlaytimeStats;
|
|
||||||
pub type PlaytimeSession = v4::PlaytimeSession;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -358,108 +355,6 @@ pub mod data {
|
|||||||
settings: Settings::default(),
|
settings: Settings::default(),
|
||||||
cache_dir,
|
cache_dir,
|
||||||
compat_info: None,
|
compat_info: None,
|
||||||
playtime_data: PlaytimeData::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod v4 {
|
|
||||||
use std::{collections::HashMap, path::PathBuf, time::SystemTime};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
|
|
||||||
Settings, native_model, v3,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[native_model(id = 1, version = 4, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
|
||||||
pub struct Database {
|
|
||||||
#[serde(default)]
|
|
||||||
pub settings: Settings,
|
|
||||||
pub auth: Option<DatabaseAuth>,
|
|
||||||
pub base_url: String,
|
|
||||||
pub applications: DatabaseApplications,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub prev_database: Option<PathBuf>,
|
|
||||||
pub cache_dir: PathBuf,
|
|
||||||
pub compat_info: Option<DatabaseCompatInfo>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub playtime_data: PlaytimeData,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
|
||||||
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
pub struct PlaytimeData {
|
|
||||||
pub game_sessions: HashMap<String, GamePlaytimeStats>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub active_sessions: HashMap<String, PlaytimeSession>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
#[native_model(id = 10, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
pub struct GamePlaytimeStats {
|
|
||||||
pub game_id: String,
|
|
||||||
pub total_playtime_seconds: u64,
|
|
||||||
pub session_count: u32,
|
|
||||||
pub first_played: SystemTime,
|
|
||||||
pub last_played: SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
#[native_model(id = 11, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
pub struct PlaytimeSession {
|
|
||||||
pub game_id: String,
|
|
||||||
pub start_time: SystemTime,
|
|
||||||
pub session_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GamePlaytimeStats {
|
|
||||||
pub fn new(game_id: String) -> Self {
|
|
||||||
let now = SystemTime::now();
|
|
||||||
Self {
|
|
||||||
game_id,
|
|
||||||
total_playtime_seconds: 0,
|
|
||||||
session_count: 0,
|
|
||||||
first_played: now,
|
|
||||||
last_played: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn average_session_length(&self) -> u64 {
|
|
||||||
if self.session_count == 0 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
self.total_playtime_seconds / self.session_count as u64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlaytimeSession {
|
|
||||||
pub fn new(game_id: String) -> Self {
|
|
||||||
Self {
|
|
||||||
game_id,
|
|
||||||
start_time: SystemTime::now(),
|
|
||||||
session_id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn duration(&self) -> std::time::Duration {
|
|
||||||
self.start_time.elapsed().unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<v3::Database> for Database {
|
|
||||||
fn from(value: v3::Database) -> Self {
|
|
||||||
Self {
|
|
||||||
settings: value.settings,
|
|
||||||
auth: value.auth,
|
|
||||||
base_url: value.base_url,
|
|
||||||
applications: value.applications,
|
|
||||||
prev_database: value.prev_database,
|
|
||||||
cache_dir: value.cache_dir,
|
|
||||||
compat_info: value.compat_info,
|
|
||||||
playtime_data: PlaytimeData::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,11 +24,11 @@ pub fn scan_install_dirs() {
|
|||||||
if !drop_data_file.exists() {
|
if !drop_data_file.exists() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let game_id = game.file_name().into_string().unwrap();
|
let game_id = game.file_name().display().to_string();
|
||||||
let Ok(drop_data) = DropData::read(&game.path()) else {
|
let Ok(drop_data) = DropData::read(&game.path()) else {
|
||||||
warn!(
|
warn!(
|
||||||
".dropdata exists for {}, but couldn't read it. is it corrupted?",
|
".dropdata exists for {}, but couldn't read it. is it corrupted?",
|
||||||
game.file_name().into_string().unwrap()
|
game.file_name().display()
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::{database::models::data::DownloadableMetadata, AppState};
|
use crate::{AppState, database::models::data::DownloadableMetadata, lock};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
state.lock().unwrap().download_manager.pause_downloads();
|
lock!(state).download_manager.pause_downloads();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
state.lock().unwrap().download_manager.resume_downloads();
|
lock!(state).download_manager.resume_downloads();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -18,14 +18,12 @@ pub fn move_download_in_queue(
|
|||||||
old_index: usize,
|
old_index: usize,
|
||||||
new_index: usize,
|
new_index: usize,
|
||||||
) {
|
) {
|
||||||
state
|
lock!(state)
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.download_manager
|
.download_manager
|
||||||
.rearrange(old_index, new_index);
|
.rearrange(old_index, new_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||||
state.lock().unwrap().download_manager.cancel(meta);
|
lock!(state).download_manager.cancel(meta);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,7 @@ use log::{debug, error, info, warn};
|
|||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::data::DownloadableMetadata,
|
app_emit, database::models::data::DownloadableMetadata, download_manager::download_manager_frontend::DownloadStatus, error::application_download_error::ApplicationDownloadError, games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}, lock, send
|
||||||
download_manager::download_manager_frontend::DownloadStatus,
|
|
||||||
error::application_download_error::ApplicationDownloadError,
|
|
||||||
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -105,7 +102,7 @@ impl DownloadManagerBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_status(&self, status: DownloadManagerStatus) {
|
fn set_status(&self, status: DownloadManagerStatus) {
|
||||||
*self.status.lock().unwrap() = status;
|
*lock!(self.status) = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
|
fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
|
||||||
@ -119,9 +116,9 @@ impl DownloadManagerBuilder {
|
|||||||
// Make sure the download thread is terminated
|
// Make sure the download thread is terminated
|
||||||
fn cleanup_current_download(&mut self) {
|
fn cleanup_current_download(&mut self) {
|
||||||
self.active_control_flag = None;
|
self.active_control_flag = None;
|
||||||
*self.progress.lock().unwrap() = None;
|
*lock!(self.progress) = None;
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
let mut download_thread_lock = lock!(self.current_download_thread);
|
||||||
|
|
||||||
if let Some(unfinished_thread) = download_thread_lock.take()
|
if let Some(unfinished_thread) = download_thread_lock.take()
|
||||||
&& !unfinished_thread.is_finished()
|
&& !unfinished_thread.is_finished()
|
||||||
@ -137,7 +134,7 @@ impl DownloadManagerBuilder {
|
|||||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
let mut download_thread_lock = lock!(self.current_download_thread);
|
||||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||||
return current_download_thread.join().is_ok();
|
return current_download_thread.join().is_ok();
|
||||||
};
|
};
|
||||||
@ -199,9 +196,7 @@ impl DownloadManagerBuilder {
|
|||||||
self.download_queue.append(meta.clone());
|
self.download_queue.append(meta.clone());
|
||||||
self.download_agent_registry.insert(meta, download_agent);
|
self.download_agent_registry.insert(meta, download_agent);
|
||||||
|
|
||||||
self.sender
|
send!(self.sender, DownloadManagerSignal::UpdateUIQueue);
|
||||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manage_go_signal(&mut self) {
|
fn manage_go_signal(&mut self) {
|
||||||
@ -247,7 +242,7 @@ impl DownloadManagerBuilder {
|
|||||||
|
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
let mut download_thread_lock = lock!(self.current_download_thread);
|
||||||
let app_handle = self.app_handle.clone();
|
let app_handle = self.app_handle.clone();
|
||||||
|
|
||||||
*download_thread_lock = Some(spawn(move || {
|
*download_thread_lock = Some(spawn(move || {
|
||||||
@ -258,7 +253,7 @@ impl DownloadManagerBuilder {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
||||||
download_agent.on_error(&app_handle, &e);
|
download_agent.on_error(&app_handle, &e);
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
send!(sender, DownloadManagerSignal::Error(e));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -282,7 +277,7 @@ impl DownloadManagerBuilder {
|
|||||||
&e
|
&e
|
||||||
);
|
);
|
||||||
download_agent.on_error(&app_handle, &e);
|
download_agent.on_error(&app_handle, &e);
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
send!(sender, DownloadManagerSignal::Error(e));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -293,10 +288,8 @@ impl DownloadManagerBuilder {
|
|||||||
|
|
||||||
if validate_result {
|
if validate_result {
|
||||||
download_agent.on_complete(&app_handle);
|
download_agent.on_complete(&app_handle);
|
||||||
sender
|
send!(sender, DownloadManagerSignal::Completed(download_agent.metadata()));
|
||||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
send!(sender, DownloadManagerSignal::UpdateUIQueue);
|
||||||
.unwrap();
|
|
||||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -323,7 +316,7 @@ impl DownloadManagerBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.push_ui_queue_update();
|
self.push_ui_queue_update();
|
||||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
send!(self.sender, DownloadManagerSignal::Go);
|
||||||
}
|
}
|
||||||
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||||
debug!("got signal Error");
|
debug!("got signal Error");
|
||||||
@ -361,7 +354,7 @@ impl DownloadManagerBuilder {
|
|||||||
let index = self.download_queue.get_by_meta(meta);
|
let index = self.download_queue.get_by_meta(meta);
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
download_agent.on_cancelled(&self.app_handle);
|
download_agent.on_cancelled(&self.app_handle);
|
||||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
let _ = self.download_queue.edit().remove(index);
|
||||||
let removed = self.download_agent_registry.remove(meta);
|
let removed = self.download_agent_registry.remove(meta);
|
||||||
debug!(
|
debug!(
|
||||||
"removed {:?} from queue {:?}",
|
"removed {:?} from queue {:?}",
|
||||||
@ -376,7 +369,7 @@ impl DownloadManagerBuilder {
|
|||||||
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
||||||
let event_data = StatsUpdateEvent { speed: kbs, time };
|
let event_data = StatsUpdateEvent { speed: kbs, time };
|
||||||
|
|
||||||
self.app_handle.emit("update_stats", event_data).unwrap();
|
app_emit!(self.app_handle, "update_stats", event_data);
|
||||||
}
|
}
|
||||||
fn push_ui_queue_update(&self) {
|
fn push_ui_queue_update(&self) {
|
||||||
let queue = &self.download_queue.read();
|
let queue = &self.download_queue.read();
|
||||||
@ -395,6 +388,6 @@ impl DownloadManagerBuilder {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let event_data = QueueUpdateEvent { queue: queue_objs };
|
let event_data = QueueUpdateEvent { queue: queue_objs };
|
||||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
app_emit!(self.app_handle, "update_queue", event_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,8 @@ use std::{
|
|||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
sync::{
|
sync::{
|
||||||
mpsc::{SendError, Sender},
|
|
||||||
Mutex, MutexGuard,
|
Mutex, MutexGuard,
|
||||||
|
mpsc::{SendError, Sender},
|
||||||
},
|
},
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
@ -14,7 +14,7 @@ use serde::Serialize;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::data::DownloadableMetadata,
|
database::models::data::DownloadableMetadata,
|
||||||
error::application_download_error::ApplicationDownloadError,
|
error::application_download_error::ApplicationDownloadError, lock, send,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -119,22 +119,18 @@ impl DownloadManager {
|
|||||||
self.download_queue.read()
|
self.download_queue.read()
|
||||||
}
|
}
|
||||||
pub fn get_current_download_progress(&self) -> Option<f64> {
|
pub fn get_current_download_progress(&self) -> Option<f64> {
|
||||||
let progress_object = (*self.progress.lock().unwrap()).clone()?;
|
let progress_object = (*lock!(self.progress)).clone()?;
|
||||||
Some(progress_object.get_progress())
|
Some(progress_object.get_progress())
|
||||||
}
|
}
|
||||||
pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
|
pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
|
||||||
let mut queue = self.edit();
|
let mut queue = self.edit();
|
||||||
let current_index = get_index_from_id(&mut queue, meta).unwrap();
|
let current_index = get_index_from_id(&mut queue, meta).expect("Failed to get meta index from id");
|
||||||
let to_move = queue.remove(current_index).unwrap();
|
let to_move = queue.remove(current_index).expect("Failed to remove meta at index from queue");
|
||||||
queue.insert(new_index, to_move);
|
queue.insert(new_index, to_move);
|
||||||
self.command_sender
|
send!(self.command_sender, DownloadManagerSignal::UpdateUIQueue);
|
||||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
pub fn cancel(&self, meta: DownloadableMetadata) {
|
pub fn cancel(&self, meta: DownloadableMetadata) {
|
||||||
self.command_sender
|
send!(self.command_sender, DownloadManagerSignal::Cancel(meta));
|
||||||
.send(DownloadManagerSignal::Cancel(meta))
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
pub fn rearrange(&self, current_index: usize, new_index: usize) {
|
pub fn rearrange(&self, current_index: usize, new_index: usize) {
|
||||||
if current_index == new_index {
|
if current_index == new_index {
|
||||||
@ -143,39 +139,31 @@ impl DownloadManager {
|
|||||||
|
|
||||||
let needs_pause = current_index == 0 || new_index == 0;
|
let needs_pause = current_index == 0 || new_index == 0;
|
||||||
if needs_pause {
|
if needs_pause {
|
||||||
self.command_sender
|
send!(self.command_sender, DownloadManagerSignal::Stop);
|
||||||
.send(DownloadManagerSignal::Stop)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("moving download at index {current_index} to index {new_index}");
|
debug!("moving download at index {current_index} to index {new_index}");
|
||||||
|
|
||||||
let mut queue = self.edit();
|
let mut queue = self.edit();
|
||||||
let to_move = queue.remove(current_index).unwrap();
|
let to_move = queue.remove(current_index).expect("Failed to get");
|
||||||
queue.insert(new_index, to_move);
|
queue.insert(new_index, to_move);
|
||||||
drop(queue);
|
drop(queue);
|
||||||
|
|
||||||
if needs_pause {
|
if needs_pause {
|
||||||
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
send!(self.command_sender, DownloadManagerSignal::Go);
|
||||||
}
|
}
|
||||||
self.command_sender
|
send!(self.command_sender, DownloadManagerSignal::UpdateUIQueue);
|
||||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
send!(self.command_sender, DownloadManagerSignal::Go);
|
||||||
.unwrap();
|
|
||||||
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
|
||||||
}
|
}
|
||||||
pub fn pause_downloads(&self) {
|
pub fn pause_downloads(&self) {
|
||||||
self.command_sender
|
send!(self.command_sender, DownloadManagerSignal::Stop);
|
||||||
.send(DownloadManagerSignal::Stop)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
pub fn resume_downloads(&self) {
|
pub fn resume_downloads(&self) {
|
||||||
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
send!(self.command_sender, DownloadManagerSignal::Go);
|
||||||
}
|
}
|
||||||
pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
|
pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
|
||||||
self.command_sender
|
send!(self.command_sender, DownloadManagerSignal::Finish);
|
||||||
.send(DownloadManagerSignal::Finish)
|
let terminator = lock!(self.terminator).take();
|
||||||
.unwrap();
|
|
||||||
let terminator = self.terminator.lock().unwrap().take();
|
|
||||||
terminator.unwrap().join()
|
terminator.unwrap().join()
|
||||||
}
|
}
|
||||||
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
|
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use std::{
|
|||||||
use atomic_instant_full::AtomicInstant;
|
use atomic_instant_full::AtomicInstant;
|
||||||
use throttle_my_fn::throttle;
|
use throttle_my_fn::throttle;
|
||||||
|
|
||||||
use crate::download_manager::download_manager_frontend::DownloadManagerSignal;
|
use crate::{download_manager::download_manager_frontend::DownloadManagerSignal, lock, send};
|
||||||
|
|
||||||
use super::rolling_progress_updates::RollingProgressWindow;
|
use super::rolling_progress_updates::RollingProgressWindow;
|
||||||
|
|
||||||
@ -74,12 +74,10 @@ impl ProgressObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_time_now(&self) {
|
pub fn set_time_now(&self) {
|
||||||
*self.start.lock().unwrap() = Instant::now();
|
*lock!(self.start) = Instant::now();
|
||||||
}
|
}
|
||||||
pub fn sum(&self) -> usize {
|
pub fn sum(&self) -> usize {
|
||||||
self.progress_instances
|
lock!(self.progress_instances)
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|instance| instance.load(Ordering::Acquire))
|
.map(|instance| instance.load(Ordering::Acquire))
|
||||||
.sum()
|
.sum()
|
||||||
@ -88,27 +86,25 @@ impl ProgressObject {
|
|||||||
self.set_time_now();
|
self.set_time_now();
|
||||||
self.bytes_last_update.store(0, Ordering::Release);
|
self.bytes_last_update.store(0, Ordering::Release);
|
||||||
self.rolling.reset();
|
self.rolling.reset();
|
||||||
self.progress_instances
|
lock!(self.progress_instances)
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|x| x.store(0, Ordering::SeqCst));
|
.for_each(|x| x.store(0, Ordering::SeqCst));
|
||||||
}
|
}
|
||||||
pub fn get_max(&self) -> usize {
|
pub fn get_max(&self) -> usize {
|
||||||
*self.max.lock().unwrap()
|
*lock!(self.max)
|
||||||
}
|
}
|
||||||
pub fn set_max(&self, new_max: usize) {
|
pub fn set_max(&self, new_max: usize) {
|
||||||
*self.max.lock().unwrap() = new_max;
|
*lock!(self.max) = new_max;
|
||||||
}
|
}
|
||||||
pub fn set_size(&self, length: usize) {
|
pub fn set_size(&self, length: usize) {
|
||||||
*self.progress_instances.lock().unwrap() =
|
*lock!(self.progress_instances) =
|
||||||
(0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
|
(0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
|
||||||
}
|
}
|
||||||
pub fn get_progress(&self) -> f64 {
|
pub fn get_progress(&self) -> f64 {
|
||||||
self.sum() as f64 / self.get_max() as f64
|
self.sum() as f64 / self.get_max() as f64
|
||||||
}
|
}
|
||||||
pub fn get(&self, index: usize) -> Arc<AtomicUsize> {
|
pub fn get(&self, index: usize) -> Arc<AtomicUsize> {
|
||||||
self.progress_instances.lock().unwrap()[index].clone()
|
lock!(self.progress_instances)[index].clone()
|
||||||
}
|
}
|
||||||
fn update_window(&self, kilobytes_per_second: usize) {
|
fn update_window(&self, kilobytes_per_second: usize) {
|
||||||
self.rolling.update(kilobytes_per_second);
|
self.rolling.update(kilobytes_per_second);
|
||||||
@ -148,18 +144,12 @@ pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
|
fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
|
||||||
progress_object
|
send!(
|
||||||
.sender
|
progress_object.sender,
|
||||||
.send(DownloadManagerSignal::UpdateUIStats(
|
DownloadManagerSignal::UpdateUIStats(kilobytes_per_second, time_remaining)
|
||||||
kilobytes_per_second,
|
);
|
||||||
time_remaining,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_queue(progress: &ProgressObject) {
|
fn update_queue(progress: &ProgressObject) {
|
||||||
progress
|
send!(progress.sender, DownloadManagerSignal::UpdateUIQueue)
|
||||||
.sender
|
|
||||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use std::{
|
|||||||
sync::{Arc, Mutex, MutexGuard},
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::database::models::data::DownloadableMetadata;
|
use crate::{database::models::data::DownloadableMetadata, lock};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Queue {
|
pub struct Queue {
|
||||||
@ -24,10 +24,10 @@ impl Queue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn read(&self) -> VecDeque<DownloadableMetadata> {
|
pub fn read(&self) -> VecDeque<DownloadableMetadata> {
|
||||||
self.inner.lock().unwrap().clone()
|
lock!(self.inner).clone()
|
||||||
}
|
}
|
||||||
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
|
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
|
||||||
self.inner.lock().unwrap()
|
lock!(self.inner)
|
||||||
}
|
}
|
||||||
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
|
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
|
||||||
self.edit().pop_front()
|
self.edit().pop_front()
|
||||||
|
|||||||
@ -18,7 +18,7 @@ pub enum ApplicationDownloadError {
|
|||||||
Checksum,
|
Checksum,
|
||||||
Lock,
|
Lock,
|
||||||
IoError(Arc<io::Error>),
|
IoError(Arc<io::Error>),
|
||||||
DownloadError,
|
DownloadError(RemoteAccessError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ApplicationDownloadError {
|
impl Display for ApplicationDownloadError {
|
||||||
@ -40,10 +40,16 @@ impl Display for ApplicationDownloadError {
|
|||||||
write!(f, "checksum failed to validate for download")
|
write!(f, "checksum failed to validate for download")
|
||||||
}
|
}
|
||||||
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
|
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
|
||||||
ApplicationDownloadError::DownloadError => write!(
|
ApplicationDownloadError::DownloadError(error) => write!(
|
||||||
f,
|
f,
|
||||||
"Download failed. See Download Manager status for specific error"
|
"Download failed with error {error}"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for ApplicationDownloadError {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
ApplicationDownloadError::IoError(Arc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src-tauri/src/error/cache_error.rs
Normal file
26
src-tauri/src/error/cache_error.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use http::{header::ToStrError, HeaderName};
|
||||||
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
|
|
||||||
|
#[derive(Debug, SerializeDisplay)]
|
||||||
|
pub enum CacheError {
|
||||||
|
HeaderNotFound(HeaderName),
|
||||||
|
ParseError(ToStrError),
|
||||||
|
Remote(RemoteAccessError),
|
||||||
|
ConstructionError(http::Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CacheError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
CacheError::HeaderNotFound(header_name) => format!("Could not find header {header_name} in cache"),
|
||||||
|
CacheError::ParseError(to_str_error) => format!("Could not parse cache with error {to_str_error}"),
|
||||||
|
CacheError::Remote(remote_access_error) => format!("Cache got remote access error: {remote_access_error}"),
|
||||||
|
CacheError::ConstructionError(error) => format!("Could not construct cache body with error {error}"),
|
||||||
|
};
|
||||||
|
write!(f, "{s}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +1,21 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::{Display};
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
#[derive(SerializeDisplay)]
|
#[derive(SerializeDisplay)]
|
||||||
pub enum LibraryError {
|
pub enum LibraryError {
|
||||||
MetaNotFound(String),
|
MetaNotFound(String),
|
||||||
|
VersionNotFound(String),
|
||||||
}
|
}
|
||||||
impl Display for LibraryError {
|
impl Display for LibraryError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
write!(f, "{}", match self {
|
||||||
LibraryError::MetaNotFound(id) => write!(
|
LibraryError::MetaNotFound(id) => {
|
||||||
f,
|
format!("Could not locate any installed version of game ID {id} in the database")
|
||||||
"Could not locate any installed version of game ID {id} in the database"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
LibraryError::VersionNotFound(game_id) => {
|
||||||
|
format!("Could not locate any installed version for game id {game_id} in the database")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,3 +4,4 @@ pub mod drop_server_error;
|
|||||||
pub mod library_error;
|
pub mod library_error;
|
||||||
pub mod process_error;
|
pub mod process_error;
|
||||||
pub mod remote_access_error;
|
pub mod remote_access_error;
|
||||||
|
pub mod cache_error;
|
||||||
@ -12,7 +12,7 @@ pub enum ProcessError {
|
|||||||
FormatError(String), // String errors supremacy
|
FormatError(String), // String errors supremacy
|
||||||
InvalidPlatform,
|
InvalidPlatform,
|
||||||
OpenerError(tauri_plugin_opener::Error),
|
OpenerError(tauri_plugin_opener::Error),
|
||||||
PlaytimeError(String),
|
InvalidArguments(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ProcessError {
|
impl Display for ProcessError {
|
||||||
@ -24,9 +24,9 @@ impl Display for ProcessError {
|
|||||||
ProcessError::InvalidVersion => "Invalid game version",
|
ProcessError::InvalidVersion => "Invalid game version",
|
||||||
ProcessError::IOError(error) => &error.to_string(),
|
ProcessError::IOError(error) => &error.to_string(),
|
||||||
ProcessError::InvalidPlatform => "This game cannot be played on the current platform",
|
ProcessError::InvalidPlatform => "This game cannot be played on the current platform",
|
||||||
ProcessError::FormatError(e) => &format!("Failed to format template: {e}"),
|
ProcessError::FormatError(e) => &format!("Could not format template: {e}"),
|
||||||
ProcessError::OpenerError(error) => &format!("Failed to open directory: {error}"),
|
ProcessError::OpenerError(error) => &format!("Could not open directory: {error}"),
|
||||||
ProcessError::PlaytimeError(error) => &format!("Playtime tracking error: {error}"),
|
ProcessError::InvalidArguments(arguments) => &format!("Invalid arguments in command {arguments}"),
|
||||||
};
|
};
|
||||||
write!(f, "{s}")
|
write!(f, "{s}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,8 +44,7 @@ impl Display for RemoteAccessError {
|
|||||||
error
|
error
|
||||||
.source()
|
.source()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.or_else(|| Some("Unknown error".to_string()))
|
.unwrap_or("Unknown error".to_string())
|
||||||
.unwrap()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RemoteAccessError::FetchErrorWS(error) => write!(
|
RemoteAccessError::FetchErrorWS(error) => write!(
|
||||||
@ -54,9 +53,8 @@ impl Display for RemoteAccessError {
|
|||||||
error,
|
error,
|
||||||
error
|
error
|
||||||
.source()
|
.source()
|
||||||
.map(|e| e.to_string())
|
.map(std::string::ToString::to_string)
|
||||||
.or_else(|| Some("Unknown error".to_string()))
|
.unwrap_or("Unknown error".to_string())
|
||||||
.unwrap()
|
|
||||||
),
|
),
|
||||||
RemoteAccessError::ParsingError(parse_error) => {
|
RemoteAccessError::ParsingError(parse_error) => {
|
||||||
write!(f, "{parse_error}")
|
write!(f, "{parse_error}")
|
||||||
|
|||||||
@ -5,13 +5,10 @@ use std::{
|
|||||||
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
|
||||||
database::{
|
database::{
|
||||||
db::borrow_db_checked,
|
db::borrow_db_checked,
|
||||||
models::data::GameDownloadStatus,
|
models::data::GameDownloadStatus,
|
||||||
},
|
}, download_manager::downloadable::Downloadable, error::application_download_error::ApplicationDownloadError, lock, AppState
|
||||||
download_manager::downloadable::Downloadable,
|
|
||||||
error::application_download_error::ApplicationDownloadError,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::download_agent::GameDownloadAgent;
|
use super::download_agent::GameDownloadAgent;
|
||||||
@ -23,16 +20,14 @@ pub async fn download_game(
|
|||||||
install_dir: usize,
|
install_dir: usize,
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
) -> Result<(), ApplicationDownloadError> {
|
) -> Result<(), ApplicationDownloadError> {
|
||||||
let sender = { state.lock().unwrap().download_manager.get_sender().clone() };
|
let sender = { lock!(state).download_manager.get_sender().clone() };
|
||||||
|
|
||||||
let game_download_agent =
|
let game_download_agent =
|
||||||
GameDownloadAgent::new_from_index(game_id.clone(), game_version.clone(), install_dir, sender).await?;
|
GameDownloadAgent::new_from_index(game_id.clone(), game_version.clone(), install_dir, sender).await?;
|
||||||
|
|
||||||
let game_download_agent =
|
let game_download_agent =
|
||||||
Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>);
|
Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>);
|
||||||
state
|
lock!(state)
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.download_manager
|
.download_manager
|
||||||
.queue_download(game_download_agent.clone())
|
.queue_download(game_download_agent.clone())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -62,22 +57,20 @@ pub async fn resume_download(
|
|||||||
} => (version_name, install_dir),
|
} => (version_name, install_dir),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
let sender = lock!(state).download_manager.get_sender();
|
||||||
let parent_dir: PathBuf = install_dir.into();
|
let parent_dir: PathBuf = install_dir.into();
|
||||||
|
|
||||||
let game_download_agent = Arc::new(Box::new(
|
let game_download_agent = Arc::new(Box::new(
|
||||||
GameDownloadAgent::new(
|
GameDownloadAgent::new(
|
||||||
game_id,
|
game_id,
|
||||||
version_name.clone(),
|
version_name.clone(),
|
||||||
parent_dir.parent().unwrap().to_path_buf(),
|
parent_dir.parent().unwrap_or_else(|| panic!("Failed to get parent directry of {}", parent_dir.display())).to_path_buf(),
|
||||||
sender,
|
sender,
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
) as Box<dyn Downloadable + Send + Sync>);
|
) as Box<dyn Downloadable + Send + Sync>);
|
||||||
|
|
||||||
state
|
lock!(state)
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.download_manager
|
.download_manager
|
||||||
.queue_download(game_download_agent)
|
.queue_download(game_download_agent)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@ -20,10 +20,12 @@ use crate::games::state::GameStatusManager;
|
|||||||
use crate::process::utils::get_disk_available;
|
use crate::process::utils::get_disk_available;
|
||||||
use crate::remote::requests::generate_url;
|
use crate::remote::requests::generate_url;
|
||||||
use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
|
use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
|
||||||
|
use crate::{app_emit, lock, send};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use rayon::ThreadPoolBuilder;
|
use rayon::ThreadPoolBuilder;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs::{OpenOptions, create_dir_all};
|
use std::fs::{OpenOptions, create_dir_all};
|
||||||
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@ -101,10 +103,8 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
result.ensure_manifest_exists().await?;
|
result.ensure_manifest_exists().await?;
|
||||||
|
|
||||||
let required_space = result
|
let required_space = lock!(result
|
||||||
.manifest
|
.manifest)
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.values()
|
.values()
|
||||||
@ -172,11 +172,11 @@ impl GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_manifest_exists(&self) -> bool {
|
pub fn check_manifest_exists(&self) -> bool {
|
||||||
self.manifest.lock().unwrap().is_some()
|
lock!(self.manifest).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
if self.manifest.lock().unwrap().is_some() {
|
if lock!(self.manifest).is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +207,10 @@ impl GameDownloadAgent {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let manifest_download: DropManifest = response.json().await.unwrap();
|
let manifest_download: DropManifest = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||||
|
|
||||||
if let Ok(mut manifest) = self.manifest.lock() {
|
if let Ok(mut manifest) = self.manifest.lock() {
|
||||||
*manifest = Some(manifest_download);
|
*manifest = Some(manifest_download);
|
||||||
@ -219,7 +222,7 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
// Sets it up for both download and validate
|
// Sets it up for both download and validate
|
||||||
fn setup_progress(&self) {
|
fn setup_progress(&self) {
|
||||||
let buckets = self.buckets.lock().unwrap();
|
let buckets = lock!(self.buckets);
|
||||||
|
|
||||||
let chunk_count = buckets.iter().map(|e| e.drops.len()).sum();
|
let chunk_count = buckets.iter().map(|e| e.drops.len()).sum();
|
||||||
|
|
||||||
@ -234,21 +237,23 @@ impl GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_buckets(&self) -> Result<(), ApplicationDownloadError> {
|
pub fn ensure_buckets(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
if self.buckets.lock().unwrap().is_empty() {
|
if lock!(self.buckets).is_empty() {
|
||||||
self.generate_buckets()?;
|
self.generate_buckets()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
*self.context_map.lock().unwrap() = self.dropdata.get_contexts();
|
*lock!(self.context_map) = self.dropdata.get_contexts();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_buckets(&self) -> Result<(), ApplicationDownloadError> {
|
pub fn generate_buckets(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
let manifest = lock!(self.manifest)
|
||||||
|
.clone()
|
||||||
|
.ok_or(ApplicationDownloadError::NotInitialized)?;
|
||||||
let game_id = self.id.clone();
|
let game_id = self.id.clone();
|
||||||
|
|
||||||
let base_path = Path::new(&self.dropdata.base_path);
|
let base_path = Path::new(&self.dropdata.base_path);
|
||||||
create_dir_all(base_path).unwrap();
|
create_dir_all(base_path)?;
|
||||||
|
|
||||||
let mut buckets = Vec::new();
|
let mut buckets = Vec::new();
|
||||||
|
|
||||||
@ -258,8 +263,13 @@ impl GameDownloadAgent {
|
|||||||
for (raw_path, chunk) in manifest {
|
for (raw_path, chunk) in manifest {
|
||||||
let path = base_path.join(Path::new(&raw_path));
|
let path = base_path.join(Path::new(&raw_path));
|
||||||
|
|
||||||
let container = path.parent().unwrap();
|
let container = path
|
||||||
create_dir_all(container).unwrap();
|
.parent()
|
||||||
|
.ok_or(ApplicationDownloadError::IoError(Arc::new(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"no parent directory",
|
||||||
|
))))?;
|
||||||
|
create_dir_all(container)?;
|
||||||
|
|
||||||
let already_exists = path.exists();
|
let already_exists = path.exists();
|
||||||
let file = OpenOptions::new()
|
let file = OpenOptions::new()
|
||||||
@ -267,8 +277,7 @@ impl GameDownloadAgent {
|
|||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(false)
|
.truncate(false)
|
||||||
.open(path.clone())
|
.open(&path)?;
|
||||||
.unwrap();
|
|
||||||
let mut file_running_offset = 0;
|
let mut file_running_offset = 0;
|
||||||
|
|
||||||
for (index, length) in chunk.lengths.iter().enumerate() {
|
for (index, length) in chunk.lengths.iter().enumerate() {
|
||||||
@ -352,7 +361,7 @@ impl GameDownloadAgent {
|
|||||||
.collect::<Vec<(String, bool)>>(),
|
.collect::<Vec<(String, bool)>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
*self.buckets.lock().unwrap() = buckets;
|
*lock!(self.buckets) = buckets;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -368,9 +377,11 @@ impl GameDownloadAgent {
|
|||||||
let pool = ThreadPoolBuilder::new()
|
let pool = ThreadPoolBuilder::new()
|
||||||
.num_threads(max_download_threads)
|
.num_threads(max_download_threads)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap_or_else(|_| {
|
||||||
|
panic!("failed to build thread pool with {max_download_threads} threads")
|
||||||
|
});
|
||||||
|
|
||||||
let buckets = self.buckets.lock().unwrap();
|
let buckets = lock!(self.buckets);
|
||||||
|
|
||||||
let mut download_contexts = HashMap::<String, DownloadContext>::new();
|
let mut download_contexts = HashMap::<String, DownloadContext>::new();
|
||||||
|
|
||||||
@ -389,7 +400,7 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
for version in versions {
|
for version in versions {
|
||||||
let download_context = DROP_CLIENT_SYNC
|
let download_context = DROP_CLIENT_SYNC
|
||||||
.post(generate_url(&["/api/v2/client/context"], &[]).unwrap())
|
.post(generate_url(&["/api/v2/client/context"], &[])?)
|
||||||
.json(&ManifestBody {
|
.json(&ManifestBody {
|
||||||
game: self.id.clone(),
|
game: self.id.clone(),
|
||||||
version: version.clone(),
|
version: version.clone(),
|
||||||
@ -412,7 +423,7 @@ impl GameDownloadAgent {
|
|||||||
let download_contexts = &download_contexts;
|
let download_contexts = &download_contexts;
|
||||||
|
|
||||||
pool.scope(|scope| {
|
pool.scope(|scope| {
|
||||||
let context_map = self.context_map.lock().unwrap();
|
let context_map = lock!(self.context_map);
|
||||||
for (index, bucket) in buckets.iter().enumerate() {
|
for (index, bucket) in buckets.iter().enumerate() {
|
||||||
let mut bucket = (*bucket).clone();
|
let mut bucket = (*bucket).clone();
|
||||||
let completed_contexts = completed_indexes_loop_arc.clone();
|
let completed_contexts = completed_indexes_loop_arc.clone();
|
||||||
@ -444,8 +455,7 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
let download_context = download_contexts
|
let download_context = download_contexts
|
||||||
.get(&bucket.version)
|
.get(&bucket.version)
|
||||||
.ok_or(RemoteAccessError::CorruptedState)
|
.unwrap_or_else(|| panic!("Could not get bucket version {}. Corrupted state.", bucket.version));
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
scope.spawn(move |_| {
|
scope.spawn(move |_| {
|
||||||
// 3 attempts
|
// 3 attempts
|
||||||
@ -477,7 +487,7 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
if i == RETRY_COUNT - 1 || !retry {
|
if i == RETRY_COUNT - 1 || !retry {
|
||||||
warn!("retry logic failed, not re-attempting.");
|
warn!("retry logic failed, not re-attempting.");
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
send!(sender, DownloadManagerSignal::Error(e));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -490,7 +500,7 @@ impl GameDownloadAgent {
|
|||||||
let newly_completed = completed_contexts.clone();
|
let newly_completed = completed_contexts.clone();
|
||||||
|
|
||||||
let completed_lock_len = {
|
let completed_lock_len = {
|
||||||
let mut context_map_lock = self.context_map.lock().unwrap();
|
let mut context_map_lock = lock!(self.context_map);
|
||||||
for (_, item) in newly_completed.iter() {
|
for (_, item) in newly_completed.iter() {
|
||||||
context_map_lock.insert(item.clone(), true);
|
context_map_lock.insert(item.clone(), true);
|
||||||
}
|
}
|
||||||
@ -498,7 +508,7 @@ impl GameDownloadAgent {
|
|||||||
context_map_lock.values().filter(|x| **x).count()
|
context_map_lock.values().filter(|x| **x).count()
|
||||||
};
|
};
|
||||||
|
|
||||||
let context_map_lock = self.context_map.lock().unwrap();
|
let context_map_lock = lock!(self.context_map);
|
||||||
let contexts = buckets
|
let contexts = buckets
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|x| x.drops.iter().map(|e| e.checksum.clone()))
|
.flat_map(|x| x.drops.iter().map(|e| e.checksum.clone()))
|
||||||
@ -547,7 +557,7 @@ impl GameDownloadAgent {
|
|||||||
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
self.setup_validate(app_handle);
|
self.setup_validate(app_handle);
|
||||||
|
|
||||||
let buckets = self.buckets.lock().unwrap();
|
let buckets = lock!(self.buckets);
|
||||||
let contexts: Vec<DropValidateContext> = buckets
|
let contexts: Vec<DropValidateContext> = buckets
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -559,7 +569,9 @@ impl GameDownloadAgent {
|
|||||||
let pool = ThreadPoolBuilder::new()
|
let pool = ThreadPoolBuilder::new()
|
||||||
.num_threads(max_download_threads)
|
.num_threads(max_download_threads)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap_or_else(|_| {
|
||||||
|
panic!("failed to build thread pool with {max_download_threads} threads")
|
||||||
|
});
|
||||||
|
|
||||||
let invalid_chunks = Arc::new(boxcar::Vec::new());
|
let invalid_chunks = Arc::new(boxcar::Vec::new());
|
||||||
pool.scope(|scope| {
|
pool.scope(|scope| {
|
||||||
@ -577,7 +589,7 @@ impl GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{e}");
|
error!("{e}");
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
send!(sender, DownloadManagerSignal::Error(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -604,7 +616,7 @@ impl GameDownloadAgent {
|
|||||||
// See docs on usage
|
// See docs on usage
|
||||||
set_partially_installed(
|
set_partially_installed(
|
||||||
&self.metadata(),
|
&self.metadata(),
|
||||||
self.dropdata.base_path.to_str().unwrap().to_string(),
|
self.dropdata.base_path.display().to_string(),
|
||||||
Some(app_handle),
|
Some(app_handle),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -614,12 +626,12 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
impl Downloadable for GameDownloadAgent {
|
impl Downloadable for GameDownloadAgent {
|
||||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
*lock!(self.status) = DownloadStatus::Downloading;
|
||||||
self.download(app_handle)
|
self.download(app_handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Validating;
|
*lock!(self.status) = DownloadStatus::Validating;
|
||||||
self.validate(app_handle)
|
self.validate(app_handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,10 +665,8 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Error;
|
*lock!(self.status) = DownloadStatus::Error;
|
||||||
app_handle
|
app_emit!(app_handle, "download_error", error.to_string());
|
||||||
.emit("download_error", error.to_string())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
error!("error while managing download: {error:?}");
|
error!("error while managing download: {error:?}");
|
||||||
|
|
||||||
@ -675,12 +685,17 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||||
on_game_complete(
|
match on_game_complete(
|
||||||
&self.metadata(),
|
&self.metadata(),
|
||||||
self.dropdata.base_path.to_string_lossy().to_string(),
|
self.dropdata.base_path.to_string_lossy().to_string(),
|
||||||
app_handle,
|
app_handle,
|
||||||
)
|
) {
|
||||||
.unwrap();
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("could not mark game as complete: {e}");
|
||||||
|
send!(self.sender, DownloadManagerSignal::Error(ApplicationDownloadError::DownloadError(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
|
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
|
||||||
@ -689,6 +704,6 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn status(&self) -> DownloadStatus {
|
fn status(&self) -> DownloadStatus {
|
||||||
self.status.lock().unwrap().clone()
|
lock!(self.status).clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,11 +110,10 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
|||||||
let destination = self
|
let destination = self
|
||||||
.destination
|
.destination
|
||||||
.get_mut(index)
|
.get_mut(index)
|
||||||
.ok_or(io::Error::other("no destination"))
|
.ok_or(io::Error::other("no destination"))?;
|
||||||
.unwrap();
|
|
||||||
let mut remaining = drop.length;
|
let mut remaining = drop.length;
|
||||||
if drop.start != 0 {
|
if drop.start != 0 {
|
||||||
destination.seek(SeekFrom::Start(drop.start.try_into().unwrap()))?;
|
destination.seek(SeekFrom::Start(drop.start as u64))?;
|
||||||
}
|
}
|
||||||
let mut last_bump = 0;
|
let mut last_bump = 0;
|
||||||
loop {
|
loop {
|
||||||
@ -215,20 +214,39 @@ pub fn download_game_bucket(
|
|||||||
RemoteAccessError::UnparseableResponse("missing Content-Lengths header".to_owned()),
|
RemoteAccessError::UnparseableResponse("missing Content-Lengths header".to_owned()),
|
||||||
))?
|
))?
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap();
|
.map_err(|e| {
|
||||||
|
ApplicationDownloadError::Communication(RemoteAccessError::UnparseableResponse(
|
||||||
|
e.to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
for (i, raw_length) in lengths.split(",").enumerate() {
|
for (i, raw_length) in lengths.split(",").enumerate() {
|
||||||
let length = raw_length.parse::<usize>().unwrap_or(0);
|
let length = raw_length.parse::<usize>().unwrap_or(0);
|
||||||
let Some(drop) = bucket.drops.get(i) else {
|
let Some(drop) = bucket.drops.get(i) else {
|
||||||
warn!("invalid number of Content-Lengths recieved: {i}, {lengths}");
|
warn!("invalid number of Content-Lengths recieved: {i}, {lengths}");
|
||||||
return Err(ApplicationDownloadError::DownloadError);
|
return Err(ApplicationDownloadError::DownloadError(
|
||||||
|
RemoteAccessError::InvalidResponse(DropServerError {
|
||||||
|
status_code: 400,
|
||||||
|
status_message: format!(
|
||||||
|
"invalid number of Content-Lengths recieved: {i}, {lengths}"
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
if drop.length != length {
|
if drop.length != length {
|
||||||
warn!(
|
warn!(
|
||||||
"for {}, expected {}, got {} ({})",
|
"for {}, expected {}, got {} ({})",
|
||||||
drop.filename, drop.length, raw_length, length
|
drop.filename, drop.length, raw_length, length
|
||||||
);
|
);
|
||||||
return Err(ApplicationDownloadError::DownloadError);
|
return Err(ApplicationDownloadError::DownloadError(
|
||||||
|
RemoteAccessError::InvalidResponse(DropServerError {
|
||||||
|
status_code: 400,
|
||||||
|
status_message: format!(
|
||||||
|
"for {}, expected {}, got {} ({})",
|
||||||
|
drop.filename, drop.length, raw_length, length
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ use std::{
|
|||||||
use log::error;
|
use log::error;
|
||||||
use native_model::{Decode, Encode};
|
use native_model::{Decode, Encode};
|
||||||
|
|
||||||
|
use crate::lock;
|
||||||
|
|
||||||
pub type DropData = v1::DropData;
|
pub type DropData = v1::DropData;
|
||||||
|
|
||||||
pub static DROP_DATA_PATH: &str = ".dropdata";
|
pub static DROP_DATA_PATH: &str = ".dropdata";
|
||||||
@ -49,7 +51,12 @@ impl DropData {
|
|||||||
let mut s = Vec::new();
|
let mut s = Vec::new();
|
||||||
file.read_to_end(&mut s)?;
|
file.read_to_end(&mut s)?;
|
||||||
|
|
||||||
Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap())
|
native_model::rmp_serde_1_3::RmpSerde::decode(s).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("Failed to decode drop data: {e}"),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
pub fn write(&self) {
|
pub fn write(&self) {
|
||||||
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
||||||
@ -71,12 +78,12 @@ impl DropData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
|
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
|
||||||
*self.contexts.lock().unwrap() = completed_contexts.iter().map(|s| (s.0.clone(), s.1)).collect();
|
*lock!(self.contexts) = completed_contexts.iter().map(|s| (s.0.clone(), s.1)).collect();
|
||||||
}
|
}
|
||||||
pub fn set_context(&self, context: String, state: bool) {
|
pub fn set_context(&self, context: String, state: bool) {
|
||||||
self.contexts.lock().unwrap().entry(context).insert_entry(state);
|
lock!(self.contexts).entry(context).insert_entry(state);
|
||||||
}
|
}
|
||||||
pub fn get_contexts(&self) -> HashMap<String, bool> {
|
pub fn get_contexts(&self) -> HashMap<String, bool> {
|
||||||
self.contexts.lock().unwrap().clone()
|
lock!(self.contexts).clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,14 +36,14 @@ pub fn validate_game_chunk(
|
|||||||
|
|
||||||
if ctx.offset != 0 {
|
if ctx.offset != 0 {
|
||||||
source
|
source
|
||||||
.seek(SeekFrom::Start(ctx.offset.try_into().unwrap()))
|
.seek(SeekFrom::Start(ctx.offset as u64))
|
||||||
.expect("Failed to seek to file offset");
|
.expect("Failed to seek to file offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hasher = md5::Context::new();
|
let mut hasher = md5::Context::new();
|
||||||
|
|
||||||
let completed =
|
let completed =
|
||||||
validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress).unwrap();
|
validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress)?;
|
||||||
if !completed {
|
if !completed {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ use tauri::AppHandle;
|
|||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::app_emit;
|
||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||||
use crate::database::models::data::Database;
|
use crate::database::models::data::Database;
|
||||||
use crate::database::models::data::{
|
use crate::database::models::data::{
|
||||||
@ -18,6 +19,7 @@ use crate::error::drop_server_error::DropServerError;
|
|||||||
use crate::error::library_error::LibraryError;
|
use crate::error::library_error::LibraryError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||||
|
use crate::lock;
|
||||||
use crate::remote::auth::generate_authorization_header;
|
use crate::remote::auth::generate_authorization_header;
|
||||||
use crate::remote::cache::cache_object_db;
|
use crate::remote::cache::cache_object_db;
|
||||||
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
||||||
@ -106,7 +108,7 @@ pub async fn fetch_library_logic(
|
|||||||
|
|
||||||
let mut games: Vec<Game> = response.json().await?;
|
let mut games: Vec<Game> = response.json().await?;
|
||||||
|
|
||||||
let mut handle = state.lock().unwrap();
|
let mut handle = lock!(state);
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
|
||||||
@ -172,7 +174,7 @@ pub async fn fetch_game_logic(
|
|||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
let version = {
|
let version = {
|
||||||
let state_handle = state.lock().unwrap();
|
let state_handle = lock!(state);
|
||||||
|
|
||||||
let db_lock = borrow_db_checked();
|
let db_lock = borrow_db_checked();
|
||||||
|
|
||||||
@ -222,14 +224,14 @@ pub async fn fetch_game_logic(
|
|||||||
return Err(RemoteAccessError::GameNotFound(id));
|
return Err(RemoteAccessError::GameNotFound(id));
|
||||||
}
|
}
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap();
|
let err = response.json().await?;
|
||||||
warn!("{err:?}");
|
warn!("{err:?}");
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
let game: Game = response.json().await?;
|
let game: Game = response.json().await?;
|
||||||
|
|
||||||
let mut state_handle = state.lock().unwrap();
|
let mut state_handle = lock!(state);
|
||||||
state_handle.games.insert(id.clone(), game.clone());
|
state_handle.games.insert(id.clone(), game.clone());
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
@ -297,22 +299,18 @@ pub async fn fetch_game_version_options_logic(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap();
|
let err = response.json().await?;
|
||||||
warn!("{err:?}");
|
warn!("{err:?}");
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Vec<GameVersion> = response.json().await?;
|
let data: Vec<GameVersion> = response.json().await?;
|
||||||
|
|
||||||
let state_lock = state.lock().unwrap();
|
let state_lock = lock!(state);
|
||||||
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
let process_manager_lock = lock!(state_lock.process_manager);
|
||||||
let data: Vec<GameVersion> = data
|
let data: Vec<GameVersion> = data
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|v| {
|
.filter(|v| process_manager_lock.valid_platform(&v.platform, &state_lock))
|
||||||
process_manager_lock
|
|
||||||
.valid_platform(&v.platform, &state_lock)
|
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
drop(process_manager_lock);
|
drop(process_manager_lock);
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
@ -379,11 +377,13 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||||||
);
|
);
|
||||||
|
|
||||||
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||||
if previous_state.is_none() {
|
|
||||||
|
let previous_state = if let Some(state) = previous_state {
|
||||||
|
state
|
||||||
|
} else {
|
||||||
warn!("uninstall job doesn't have previous state, failing silently");
|
warn!("uninstall job doesn't have previous state, failing silently");
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
let previous_state = previous_state.unwrap();
|
|
||||||
|
|
||||||
if let Some((_, install_dir)) = match previous_state {
|
if let Some((_, install_dir)) = match previous_state {
|
||||||
GameDownloadStatus::Installed {
|
GameDownloadStatus::Installed {
|
||||||
@ -432,7 +432,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||||||
);
|
);
|
||||||
|
|
||||||
debug!("uninstalled game id {}", &meta.id);
|
debug!("uninstalled game id {}", &meta.id);
|
||||||
app_handle.emit("update_library", ()).unwrap();
|
app_emit!(app_handle, "update_library", ());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -505,17 +505,15 @@ pub fn on_game_complete(
|
|||||||
.game_statuses
|
.game_statuses
|
||||||
.insert(meta.id.clone(), status.clone());
|
.insert(meta.id.clone(), status.clone());
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
app_emit!(
|
||||||
app_handle
|
app_handle,
|
||||||
.emit(
|
|
||||||
&format!("update_game/{}", meta.id),
|
&format!("update_game/{}", meta.id),
|
||||||
GameUpdateEvent {
|
GameUpdateEvent {
|
||||||
game_id: meta.id.clone(),
|
game_id: meta.id.clone(),
|
||||||
status: (Some(status), None),
|
status: (Some(status), None),
|
||||||
version: Some(game_version),
|
version: Some(game_version),
|
||||||
},
|
}
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -533,16 +531,15 @@ pub fn push_game_update(
|
|||||||
panic!("pushed game for installed game that doesn't have version information");
|
panic!("pushed game for installed game that doesn't have version information");
|
||||||
}
|
}
|
||||||
|
|
||||||
app_handle
|
app_emit!(
|
||||||
.emit(
|
app_handle,
|
||||||
&format!("update_game/{game_id}"),
|
&format!("update_game/{game_id}"),
|
||||||
GameUpdateEvent {
|
GameUpdateEvent {
|
||||||
game_id: game_id.clone(),
|
game_id: game_id.clone(),
|
||||||
status,
|
status,
|
||||||
version,
|
version,
|
||||||
},
|
}
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -564,7 +561,7 @@ pub fn update_game_configuration(
|
|||||||
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
||||||
|
|
||||||
let id = installed_version.id.clone();
|
let id = installed_version.id.clone();
|
||||||
let version = installed_version.version.clone().unwrap();
|
let version = installed_version.version.clone().ok_or(LibraryError::VersionNotFound(id.clone()))?;
|
||||||
|
|
||||||
let mut existing_configuration = handle
|
let mut existing_configuration = handle
|
||||||
.applications
|
.applications
|
||||||
|
|||||||
@ -11,14 +11,16 @@ mod games;
|
|||||||
mod client;
|
mod client;
|
||||||
mod download_manager;
|
mod download_manager;
|
||||||
mod error;
|
mod error;
|
||||||
mod playtime;
|
|
||||||
mod process;
|
mod process;
|
||||||
mod remote;
|
mod remote;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use crate::database::scan::scan_install_dirs;
|
use crate::database::scan::scan_install_dirs;
|
||||||
use crate::process::commands::open_process_logs;
|
use crate::process::commands::open_process_logs;
|
||||||
use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE;
|
use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE;
|
||||||
use crate::remote::commands::auth_initiate_code;
|
use crate::remote::commands::auth_initiate_code;
|
||||||
|
use crate::remote::fetch_object::fetch_object_wrapper;
|
||||||
|
use crate::remote::server_proto::handle_server_proto_wrapper;
|
||||||
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
|
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
|
||||||
use bitcode::{Decode, Encode};
|
use bitcode::{Decode, Encode};
|
||||||
use client::commands::fetch_state;
|
use client::commands::fetch_state;
|
||||||
@ -47,12 +49,6 @@ use games::commands::{
|
|||||||
use games::downloads::commands::download_game;
|
use games::downloads::commands::download_game;
|
||||||
use games::library::{Game, update_game_configuration};
|
use games::library::{Game, update_game_configuration};
|
||||||
use log::{LevelFilter, debug, info, warn};
|
use log::{LevelFilter, debug, info, warn};
|
||||||
use playtime::manager::PlaytimeManager;
|
|
||||||
use playtime::commands::{
|
|
||||||
start_playtime_tracking, end_playtime_tracking, fetch_game_playtime,
|
|
||||||
fetch_all_playtime_stats, is_playtime_session_active, get_active_playtime_sessions,
|
|
||||||
cleanup_orphaned_playtime_sessions
|
|
||||||
};
|
|
||||||
use log4rs::Config;
|
use log4rs::Config;
|
||||||
use log4rs::append::console::ConsoleAppender;
|
use log4rs::append::console::ConsoleAppender;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
@ -65,8 +61,7 @@ use remote::commands::{
|
|||||||
auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect,
|
auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect,
|
||||||
sign_out, use_remote,
|
sign_out, use_remote,
|
||||||
};
|
};
|
||||||
use remote::fetch_object::fetch_object;
|
use remote::server_proto::handle_server_proto_offline_wrapper;
|
||||||
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@ -134,11 +129,7 @@ pub struct AppState<'a> {
|
|||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
playtime_manager: Arc<Mutex<PlaytimeManager>>,
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
compat_info: Option<CompatInfo>,
|
compat_info: Option<CompatInfo>,
|
||||||
#[serde(skip_serializing)]
|
|
||||||
app_handle: AppHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup(handle: AppHandle) -> AppState<'static> {
|
async fn setup(handle: AppHandle) -> AppState<'static> {
|
||||||
@ -148,7 +139,7 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
)))
|
)))
|
||||||
.append(false)
|
.append(false)
|
||||||
.build(DATA_ROOT_DIR.join("./drop.log"))
|
.build(DATA_ROOT_DIR.join("./drop.log"))
|
||||||
.unwrap();
|
.expect("Failed to setup logfile");
|
||||||
|
|
||||||
let console = ConsoleAppender::builder()
|
let console = ConsoleAppender::builder()
|
||||||
.encoder(Box::new(PatternEncoder::new(
|
.encoder(Box::new(PatternEncoder::new(
|
||||||
@ -168,14 +159,13 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
.appenders(vec!["logfile", "console"])
|
.appenders(vec!["logfile", "console"])
|
||||||
.build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
|
.build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.expect("Failed to build config");
|
||||||
|
|
||||||
log4rs::init_config(config).unwrap();
|
log4rs::init_config(config).expect("Failed to initialise log4rs");
|
||||||
|
|
||||||
let games = HashMap::new();
|
let games = HashMap::new();
|
||||||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
||||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
||||||
let playtime_manager = Arc::new(Mutex::new(PlaytimeManager::new(handle.clone())));
|
|
||||||
let compat_info = create_new_compat_info();
|
let compat_info = create_new_compat_info();
|
||||||
|
|
||||||
debug!("checking if database is set up");
|
debug!("checking if database is set up");
|
||||||
@ -190,9 +180,7 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
games,
|
games,
|
||||||
download_manager,
|
download_manager,
|
||||||
process_manager,
|
process_manager,
|
||||||
playtime_manager,
|
|
||||||
compat_info,
|
compat_info,
|
||||||
app_handle: handle.clone(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,20 +239,13 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
warn!("failed to sync autostart state: {e}");
|
warn!("failed to sync autostart state: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any orphaned playtime sessions
|
|
||||||
if let Err(e) = playtime_manager.lock().unwrap().cleanup_orphaned_sessions() {
|
|
||||||
warn!("failed to cleanup orphaned playtime sessions: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
AppState {
|
AppState {
|
||||||
status: app_status,
|
status: app_status,
|
||||||
user,
|
user,
|
||||||
games,
|
games,
|
||||||
download_manager,
|
download_manager,
|
||||||
process_manager,
|
process_manager,
|
||||||
playtime_manager,
|
|
||||||
compat_info,
|
compat_info,
|
||||||
app_handle: handle.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,15 +336,7 @@ pub fn run() {
|
|||||||
kill_game,
|
kill_game,
|
||||||
toggle_autostart,
|
toggle_autostart,
|
||||||
get_autostart_enabled,
|
get_autostart_enabled,
|
||||||
open_process_logs,
|
open_process_logs
|
||||||
// Playtime tracking
|
|
||||||
start_playtime_tracking,
|
|
||||||
end_playtime_tracking,
|
|
||||||
fetch_game_playtime,
|
|
||||||
fetch_all_playtime_stats,
|
|
||||||
is_playtime_session_active,
|
|
||||||
get_active_playtime_sessions,
|
|
||||||
cleanup_orphaned_playtime_sessions
|
|
||||||
])
|
])
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@ -399,42 +372,57 @@ pub fn run() {
|
|||||||
.shadow(false)
|
.shadow(false)
|
||||||
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.expect("Failed to build main window");
|
||||||
|
|
||||||
app.deep_link().on_open_url(move |event| {
|
app.deep_link().on_open_url(move |event| {
|
||||||
debug!("handling drop:// url");
|
debug!("handling drop:// url");
|
||||||
let binding = event.urls();
|
let binding = event.urls();
|
||||||
let url = binding.first().unwrap();
|
let url = match binding.first() {
|
||||||
if url.host_str().unwrap() == "handshake" {
|
Some(url) => url,
|
||||||
|
None => {
|
||||||
|
warn!("No value recieved from deep link. Is this a drop server?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some("handshake") = url.host_str() {
|
||||||
tauri::async_runtime::spawn(recieve_handshake(
|
tauri::async_runtime::spawn(recieve_handshake(
|
||||||
handle.clone(),
|
handle.clone(),
|
||||||
url.path().to_string(),
|
url.path().to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let open_menu_item = MenuItem::with_id(app, "open", "Open", true, None::<&str>).expect("Failed to generate open menu item");
|
||||||
|
|
||||||
|
let sep = PredefinedMenuItem::separator(app).expect("Failed to generate menu separator item");
|
||||||
|
|
||||||
|
let quit_menu_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).expect("Failed to generate quit menu item");
|
||||||
|
|
||||||
let menu = Menu::with_items(
|
let menu = Menu::with_items(
|
||||||
app,
|
app,
|
||||||
&[
|
&[
|
||||||
&MenuItem::with_id(app, "open", "Open", true, None::<&str>).unwrap(),
|
&open_menu_item,
|
||||||
&PredefinedMenuItem::separator(app).unwrap(),
|
&sep,
|
||||||
/*
|
/*
|
||||||
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
||||||
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
*/
|
*/
|
||||||
&MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap(),
|
&quit_menu_item,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.expect("Failed to generate menu");
|
||||||
|
|
||||||
run_on_tray(|| {
|
run_on_tray(|| {
|
||||||
TrayIconBuilder::new()
|
TrayIconBuilder::new()
|
||||||
.icon(app.default_window_icon().unwrap().clone())
|
.icon(app.default_window_icon().expect("Failed to get default window icon").clone())
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||||
"open" => {
|
"open" => {
|
||||||
app.webview_windows().get("main").unwrap().show().unwrap();
|
app.webview_windows()
|
||||||
|
.get("main")
|
||||||
|
.expect("Failed to get webview")
|
||||||
|
.show()
|
||||||
|
.expect("Failed to show window");
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
cleanup_and_exit(app, &app.state());
|
cleanup_and_exit(app, &app.state());
|
||||||
@ -451,15 +439,19 @@ pub fn run() {
|
|||||||
{
|
{
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
if let Some(original) = db_handle.prev_database.take() {
|
if let Some(original) = db_handle.prev_database.take() {
|
||||||
|
let canonicalised = match original.canonicalize() {
|
||||||
|
Ok(o) => o,
|
||||||
|
Err(_) => original,
|
||||||
|
};
|
||||||
warn!(
|
warn!(
|
||||||
"Database corrupted. Original file at {}",
|
"Database corrupted. Original file at {}",
|
||||||
original.canonicalize().unwrap().to_string_lossy()
|
canonicalised.display()
|
||||||
);
|
);
|
||||||
app.dialog()
|
app.dialog()
|
||||||
.message(
|
.message(format!(
|
||||||
"Database corrupted. A copy has been saved at: ".to_string()
|
"Database corrupted. A copy has been saved at: {}",
|
||||||
+ original.to_str().unwrap(),
|
canonicalised.display()
|
||||||
)
|
))
|
||||||
.title("Database corrupted")
|
.title("Database corrupted")
|
||||||
.show(|_| {});
|
.show(|_| {});
|
||||||
}
|
}
|
||||||
@ -470,7 +462,7 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
|
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
fetch_object(request, responder).await;
|
fetch_object_wrapper(request, responder).await;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.register_asynchronous_uri_scheme_protocol("server", |ctx, request, responder| {
|
.register_asynchronous_uri_scheme_protocol("server", |ctx, request, responder| {
|
||||||
@ -481,8 +473,8 @@ pub fn run() {
|
|||||||
|
|
||||||
offline!(
|
offline!(
|
||||||
state,
|
state,
|
||||||
handle_server_proto,
|
handle_server_proto_wrapper,
|
||||||
handle_server_proto_offline,
|
handle_server_proto_offline_wrapper,
|
||||||
request,
|
request,
|
||||||
responder
|
responder
|
||||||
)
|
)
|
||||||
@ -492,7 +484,7 @@ pub fn run() {
|
|||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
run_on_tray(|| {
|
run_on_tray(|| {
|
||||||
window.hide().unwrap();
|
window.hide().expect("Failed to close window in tray");
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use tauri::State;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use crate::AppState;
|
|
||||||
use super::manager::PlaytimeStats;
|
|
||||||
use super::events::{push_playtime_update, push_session_start, push_session_end};
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn start_playtime_tracking(
|
|
||||||
game_id: String,
|
|
||||||
state: State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let state_lock = state.lock().unwrap();
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
|
|
||||||
match playtime_manager_lock.start_session(game_id.clone()) {
|
|
||||||
Ok(()) => {
|
|
||||||
push_session_start(&state_lock.app_handle, &game_id);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn end_playtime_tracking(
|
|
||||||
game_id: String,
|
|
||||||
state: State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<PlaytimeStats, String> {
|
|
||||||
let state_lock = state.lock().unwrap();
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
|
|
||||||
match playtime_manager_lock.end_session(game_id.clone()) {
|
|
||||||
Ok(stats) => {
|
|
||||||
push_session_end(&state_lock.app_handle, &game_id, &stats);
|
|
||||||
push_playtime_update(&state_lock.app_handle, &game_id, stats.clone(), false);
|
|
||||||
Ok(stats)
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn fetch_game_playtime(
|
|
||||||
game_id: String,
|
|
||||||
state: State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<Option<PlaytimeStats>, String> {
|
|
||||||
let state_lock = state.lock().unwrap();
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
|
|
||||||
Ok(playtime_manager_lock.get_game_stats(&game_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn fetch_all_playtime_stats(
|
|
||||||
state: State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<HashMap<String, PlaytimeStats>, String> {
|
|
||||||
let state_lock = state.lock().unwrap();
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
|
|
||||||
Ok(playtime_manager_lock.get_all_stats())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn is_playtime_session_active(
|
|
||||||
game_id: String,
|
|
||||||
state: State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
let state_lock = state.lock().unwrap();
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
|
|
||||||
Ok(playtime_manager_lock.is_session_active(&game_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_active_playtime_sessions(
|
|
||||||
state: State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<Vec<String>, String> {
|
|
||||||
let state_lock = state.lock().unwrap();
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
|
|
||||||
Ok(playtime_manager_lock.get_active_sessions())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn cleanup_orphaned_playtime_sessions(
|
|
||||||
state: State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let state_lock = state.lock().unwrap();
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
|
|
||||||
playtime_manager_lock.cleanup_orphaned_sessions()
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
use serde::Serialize;
|
|
||||||
use tauri::{AppHandle, Emitter};
|
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use super::manager::PlaytimeStats;
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PlaytimeUpdateEvent {
|
|
||||||
pub game_id: String,
|
|
||||||
pub stats: PlaytimeStats,
|
|
||||||
pub is_active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PlaytimeSessionStartEvent {
|
|
||||||
pub game_id: String,
|
|
||||||
pub start_time: std::time::SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PlaytimeSessionEndEvent {
|
|
||||||
pub game_id: String,
|
|
||||||
pub session_duration_seconds: u64,
|
|
||||||
pub total_playtime_seconds: u64,
|
|
||||||
pub session_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a playtime update event to the frontend
|
|
||||||
pub fn push_playtime_update(app_handle: &AppHandle, game_id: &str, stats: PlaytimeStats, is_active: bool) {
|
|
||||||
let event = PlaytimeUpdateEvent {
|
|
||||||
game_id: game_id.to_string(),
|
|
||||||
stats,
|
|
||||||
is_active,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = app_handle.emit(&format!("playtime_update/{}", game_id), &event) {
|
|
||||||
warn!("Failed to emit playtime update event for {}: {}", game_id, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also emit a general playtime update event for global listeners
|
|
||||||
if let Err(e) = app_handle.emit("playtime_update", &event) {
|
|
||||||
warn!("Failed to emit general playtime update event: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a session start event to the frontend
|
|
||||||
pub fn push_session_start(app_handle: &AppHandle, game_id: &str) {
|
|
||||||
let event = PlaytimeSessionStartEvent {
|
|
||||||
game_id: game_id.to_string(),
|
|
||||||
start_time: std::time::SystemTime::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = app_handle.emit(&format!("playtime_session_start/{}", game_id), &event) {
|
|
||||||
warn!("Failed to emit session start event for {}: {}", game_id, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = app_handle.emit("playtime_session_start", &event) {
|
|
||||||
warn!("Failed to emit general session start event: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a session end event to the frontend
|
|
||||||
pub fn push_session_end(app_handle: &AppHandle, game_id: &str, stats: &PlaytimeStats) {
|
|
||||||
let event = PlaytimeSessionEndEvent {
|
|
||||||
game_id: game_id.to_string(),
|
|
||||||
session_duration_seconds: stats.current_session_duration.unwrap_or(0),
|
|
||||||
total_playtime_seconds: stats.total_playtime_seconds,
|
|
||||||
session_count: stats.session_count,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = app_handle.emit(&format!("playtime_session_end/{}", game_id), &event) {
|
|
||||||
warn!("Failed to emit session end event for {}: {}", game_id, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = app_handle.emit("playtime_session_end", &event) {
|
|
||||||
warn!("Failed to emit general session end event: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,255 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use log::{debug, warn};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tauri::AppHandle;
|
|
||||||
|
|
||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
|
||||||
use crate::database::models::data::{GamePlaytimeStats, PlaytimeSession};
|
|
||||||
use crate::error::process_error::ProcessError;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum PlaytimeError {
|
|
||||||
DatabaseError(String),
|
|
||||||
SessionNotFound(String),
|
|
||||||
SessionAlreadyActive(String),
|
|
||||||
InvalidGameId(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for PlaytimeError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
PlaytimeError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
|
|
||||||
PlaytimeError::SessionNotFound(game_id) => write!(f, "Session not found for game: {}", game_id),
|
|
||||||
PlaytimeError::SessionAlreadyActive(game_id) => write!(f, "Session already active for game: {}", game_id),
|
|
||||||
PlaytimeError::InvalidGameId(game_id) => write!(f, "Invalid game ID: {}", game_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for PlaytimeError {}
|
|
||||||
|
|
||||||
impl From<PlaytimeError> for ProcessError {
|
|
||||||
fn from(error: PlaytimeError) -> Self {
|
|
||||||
ProcessError::PlaytimeError(error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PlaytimeStats {
|
|
||||||
pub game_id: String,
|
|
||||||
pub total_playtime_seconds: u64,
|
|
||||||
pub session_count: u32,
|
|
||||||
pub first_played: SystemTime,
|
|
||||||
pub last_played: SystemTime,
|
|
||||||
pub average_session_length: u64,
|
|
||||||
pub current_session_duration: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<GamePlaytimeStats> for PlaytimeStats {
|
|
||||||
fn from(stats: GamePlaytimeStats) -> Self {
|
|
||||||
let average_length = stats.average_session_length();
|
|
||||||
Self {
|
|
||||||
game_id: stats.game_id,
|
|
||||||
total_playtime_seconds: stats.total_playtime_seconds,
|
|
||||||
session_count: stats.session_count,
|
|
||||||
first_played: stats.first_played,
|
|
||||||
last_played: stats.last_played,
|
|
||||||
average_session_length: average_length,
|
|
||||||
current_session_duration: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PlaytimeManager {
|
|
||||||
app_handle: AppHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlaytimeManager {
|
|
||||||
pub fn new(app_handle: AppHandle) -> Self {
|
|
||||||
Self { app_handle }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start tracking playtime for a game
|
|
||||||
pub fn start_session(&self, game_id: String) -> Result<(), PlaytimeError> {
|
|
||||||
debug!("Starting playtime session for game: {}", game_id);
|
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
|
||||||
|
|
||||||
// Check if session is already active
|
|
||||||
if db_handle.playtime_data.active_sessions.contains_key(&game_id) {
|
|
||||||
warn!("Session already active for game: {}", game_id);
|
|
||||||
return Err(PlaytimeError::SessionAlreadyActive(game_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new session
|
|
||||||
let session = PlaytimeSession::new(game_id.clone());
|
|
||||||
db_handle.playtime_data.active_sessions.insert(game_id.clone(), session);
|
|
||||||
|
|
||||||
debug!("Started playtime tracking for game: {}", game_id);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// End tracking playtime for a game and update stats
|
|
||||||
pub fn end_session(&self, game_id: String) -> Result<PlaytimeStats, PlaytimeError> {
|
|
||||||
debug!("Ending playtime session for game: {}", game_id);
|
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
|
||||||
|
|
||||||
// Get active session
|
|
||||||
let session = db_handle.playtime_data.active_sessions.remove(&game_id)
|
|
||||||
.ok_or_else(|| PlaytimeError::SessionNotFound(game_id.clone()))?;
|
|
||||||
|
|
||||||
let session_duration = session.duration().as_secs();
|
|
||||||
debug!("Session duration for {}: {} seconds", game_id, session_duration);
|
|
||||||
|
|
||||||
// Update or create game stats
|
|
||||||
let stats = db_handle.playtime_data.game_sessions
|
|
||||||
.entry(game_id.clone())
|
|
||||||
.or_insert_with(|| GamePlaytimeStats::new(game_id.clone()));
|
|
||||||
|
|
||||||
// Update stats
|
|
||||||
stats.total_playtime_seconds += session_duration;
|
|
||||||
stats.session_count += 1;
|
|
||||||
stats.last_played = SystemTime::now();
|
|
||||||
|
|
||||||
// If this is the first session, update first_played
|
|
||||||
if stats.session_count == 1 {
|
|
||||||
stats.first_played = session.start_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result_stats = PlaytimeStats {
|
|
||||||
game_id: stats.game_id.clone(),
|
|
||||||
total_playtime_seconds: stats.total_playtime_seconds,
|
|
||||||
session_count: stats.session_count,
|
|
||||||
first_played: stats.first_played,
|
|
||||||
last_played: stats.last_played,
|
|
||||||
average_session_length: stats.average_session_length(),
|
|
||||||
current_session_duration: Some(session_duration),
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("Updated playtime stats for {}: {} total seconds, {} sessions",
|
|
||||||
game_id, stats.total_playtime_seconds, stats.session_count);
|
|
||||||
|
|
||||||
Ok(result_stats)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get playtime stats for a specific game
|
|
||||||
pub fn get_game_stats(&self, game_id: &str) -> Option<PlaytimeStats> {
|
|
||||||
let db_handle = borrow_db_checked();
|
|
||||||
|
|
||||||
if let Some(stats) = db_handle.playtime_data.game_sessions.get(game_id) {
|
|
||||||
let mut playtime_stats: PlaytimeStats = stats.clone().into();
|
|
||||||
|
|
||||||
// If there's an active session, include current session duration
|
|
||||||
if let Some(session) = db_handle.playtime_data.active_sessions.get(game_id) {
|
|
||||||
playtime_stats.current_session_duration = Some(session.duration().as_secs());
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(playtime_stats)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get playtime stats for all games
|
|
||||||
pub fn get_all_stats(&self) -> HashMap<String, PlaytimeStats> {
|
|
||||||
let db_handle = borrow_db_checked();
|
|
||||||
let mut result = HashMap::new();
|
|
||||||
|
|
||||||
for (game_id, stats) in &db_handle.playtime_data.game_sessions {
|
|
||||||
let mut playtime_stats: PlaytimeStats = stats.clone().into();
|
|
||||||
|
|
||||||
// If there's an active session, include current session duration
|
|
||||||
if let Some(session) = db_handle.playtime_data.active_sessions.get(game_id) {
|
|
||||||
playtime_stats.current_session_duration = Some(session.duration().as_secs());
|
|
||||||
}
|
|
||||||
|
|
||||||
result.insert(game_id.clone(), playtime_stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a game has an active session
|
|
||||||
pub fn is_session_active(&self, game_id: &str) -> bool {
|
|
||||||
let db_handle = borrow_db_checked();
|
|
||||||
db_handle.playtime_data.active_sessions.contains_key(game_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get active sessions (for debugging/monitoring)
|
|
||||||
pub fn get_active_sessions(&self) -> Vec<String> {
|
|
||||||
let db_handle = borrow_db_checked();
|
|
||||||
db_handle.playtime_data.active_sessions.keys().cloned().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clean up any orphaned sessions (called on startup)
|
|
||||||
pub fn cleanup_orphaned_sessions(&self) -> Result<(), PlaytimeError> {
|
|
||||||
debug!("Cleaning up orphaned playtime sessions");
|
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
|
||||||
let orphaned_sessions: Vec<String> = db_handle.playtime_data.active_sessions.keys().cloned().collect();
|
|
||||||
|
|
||||||
for game_id in orphaned_sessions {
|
|
||||||
warn!("Found orphaned session for game: {}, ending it", game_id);
|
|
||||||
|
|
||||||
if let Some(session) = db_handle.playtime_data.active_sessions.remove(&game_id) {
|
|
||||||
let session_duration = session.duration().as_secs();
|
|
||||||
|
|
||||||
// Only count sessions that lasted more than 5 seconds to avoid counting crashes
|
|
||||||
if session_duration > 5 {
|
|
||||||
let stats = db_handle.playtime_data.game_sessions
|
|
||||||
.entry(game_id.clone())
|
|
||||||
.or_insert_with(|| GamePlaytimeStats::new(game_id.clone()));
|
|
||||||
|
|
||||||
stats.total_playtime_seconds += session_duration;
|
|
||||||
stats.session_count += 1;
|
|
||||||
stats.last_played = SystemTime::now();
|
|
||||||
|
|
||||||
if stats.session_count == 1 {
|
|
||||||
stats.first_played = session.start_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Recovered orphaned session for {}: {} seconds", game_id, session_duration);
|
|
||||||
} else {
|
|
||||||
debug!("Discarded short orphaned session for {}: {} seconds", game_id, session_duration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Future server-side methods (ready for migration)
|
|
||||||
|
|
||||||
/// Start session with server sync (placeholder for future implementation)
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn sync_session_start(&self, game_id: String) -> Result<(), PlaytimeError> {
|
|
||||||
// For now, just call local method
|
|
||||||
self.start_session(game_id)?;
|
|
||||||
|
|
||||||
// Future: Send to server
|
|
||||||
// let response = self.api_client.post("/api/v1/playtime/start")
|
|
||||||
// .json(&StartSessionRequest { game_id })
|
|
||||||
// .send().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// End session with server sync (placeholder for future implementation)
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn sync_session_end(&self, game_id: String) -> Result<PlaytimeStats, PlaytimeError> {
|
|
||||||
// For now, just call local method
|
|
||||||
let stats = self.end_session(game_id)?;
|
|
||||||
|
|
||||||
// Future: Send to server
|
|
||||||
// let response = self.api_client.post("/api/v1/playtime/end")
|
|
||||||
// .json(&EndSessionRequest { game_id, duration: stats.current_session_duration })
|
|
||||||
// .send().await?;
|
|
||||||
|
|
||||||
Ok(stats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
pub mod commands;
|
|
||||||
pub mod events;
|
|
||||||
pub mod manager;
|
|
||||||
@ -1,14 +1,14 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::{error::process_error::ProcessError, AppState};
|
use crate::{error::process_error::ProcessError, lock, AppState};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn launch_game(
|
pub fn launch_game(
|
||||||
id: String,
|
id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), ProcessError> {
|
) -> Result<(), ProcessError> {
|
||||||
let state_lock = state.lock().unwrap();
|
let state_lock = lock!(state);
|
||||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
let mut process_manager_lock = lock!(state_lock.process_manager);
|
||||||
|
|
||||||
//let meta = DownloadableMetadata {
|
//let meta = DownloadableMetadata {
|
||||||
// id,
|
// id,
|
||||||
@ -16,28 +16,14 @@ pub fn launch_game(
|
|||||||
// download_type: DownloadType::Game,
|
// download_type: DownloadType::Game,
|
||||||
//};
|
//};
|
||||||
|
|
||||||
match process_manager_lock.launch_process(id.clone(), &state_lock) {
|
match process_manager_lock.launch_process(id, &state_lock) {
|
||||||
Ok(()) => {
|
Ok(()) => {}
|
||||||
// Start playtime tracking after successful launch
|
Err(e) => return Err(e),
|
||||||
drop(process_manager_lock);
|
|
||||||
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
if let Err(e) = playtime_manager_lock.start_session(id.clone()) {
|
|
||||||
log::warn!("Failed to start playtime tracking for {}: {}", id, e);
|
|
||||||
} else {
|
|
||||||
log::debug!("Started playtime tracking for game: {}", id);
|
|
||||||
crate::playtime::events::push_session_start(&state_lock.app_handle, &id);
|
|
||||||
}
|
|
||||||
drop(playtime_manager_lock);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
drop(process_manager_lock);
|
|
||||||
drop(state_lock);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drop(process_manager_lock);
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,20 +32,8 @@ pub fn kill_game(
|
|||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), ProcessError> {
|
) -> Result<(), ProcessError> {
|
||||||
let state_lock = state.lock().unwrap();
|
let state_lock = lock!(state);
|
||||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
let mut process_manager_lock = lock!(state_lock.process_manager);
|
||||||
|
|
||||||
// End playtime tracking before killing the game
|
|
||||||
drop(process_manager_lock);
|
|
||||||
let playtime_manager_lock = state_lock.playtime_manager.lock().unwrap();
|
|
||||||
if let Ok(stats) = playtime_manager_lock.end_session(game_id.clone()) {
|
|
||||||
log::debug!("Ended playtime tracking for game: {} (manual kill)", game_id);
|
|
||||||
crate::playtime::events::push_session_end(&state_lock.app_handle, &game_id, &stats);
|
|
||||||
crate::playtime::events::push_playtime_update(&state_lock.app_handle, &game_id, stats, false);
|
|
||||||
}
|
|
||||||
drop(playtime_manager_lock);
|
|
||||||
|
|
||||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
|
||||||
process_manager_lock
|
process_manager_lock
|
||||||
.kill_game(game_id)
|
.kill_game(game_id)
|
||||||
.map_err(ProcessError::IOError)
|
.map_err(ProcessError::IOError)
|
||||||
@ -70,7 +44,7 @@ pub fn open_process_logs(
|
|||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), ProcessError> {
|
) -> Result<(), ProcessError> {
|
||||||
let state_lock = state.lock().unwrap();
|
let state_lock = lock!(state);
|
||||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
let mut process_manager_lock = lock!(state_lock.process_manager);
|
||||||
process_manager_lock.open_process_logs(game_id)
|
process_manager_lock.open_process_logs(game_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ use log::{debug, info};
|
|||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
database::models::data::{Database, DownloadableMetadata, GameVersion},
|
database::models::data::{Database, DownloadableMetadata, GameVersion},
|
||||||
|
error::process_error::ProcessError,
|
||||||
process::process_manager::{Platform, ProcessHandler},
|
process::process_manager::{Platform, ProcessHandler},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -22,8 +23,8 @@ impl ProcessHandler for NativeGameLauncher {
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
_game_version: &GameVersion,
|
_game_version: &GameVersion,
|
||||||
_current_dir: &str,
|
_current_dir: &str,
|
||||||
) -> String {
|
) -> Result<String, ProcessError> {
|
||||||
format!("\"{}\" {}", launch_command, args.join(" "))
|
Ok(format!("\"{}\" {}", launch_command, args.join(" ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool {
|
fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool {
|
||||||
@ -65,7 +66,7 @@ impl ProcessHandler for UMULauncher {
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
game_version: &GameVersion,
|
game_version: &GameVersion,
|
||||||
_current_dir: &str,
|
_current_dir: &str,
|
||||||
) -> String {
|
) -> Result<String, ProcessError> {
|
||||||
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
|
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
|
||||||
let game_id = match &game_version.umu_id_override {
|
let game_id = match &game_version.umu_id_override {
|
||||||
Some(game_override) => {
|
Some(game_override) => {
|
||||||
@ -77,12 +78,12 @@ impl ProcessHandler for UMULauncher {
|
|||||||
}
|
}
|
||||||
None => game_version.game_id.clone(),
|
None => game_version.game_id.clone(),
|
||||||
};
|
};
|
||||||
format!(
|
Ok(format!(
|
||||||
"GAMEID={game_id} {umu:?} \"{launch}\" {args}",
|
"GAMEID={game_id} {umu:?} \"{launch}\" {args}",
|
||||||
umu = UMU_LAUNCHER_EXECUTABLE.as_ref().unwrap(),
|
umu = UMU_LAUNCHER_EXECUTABLE.as_ref().expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"),
|
||||||
launch = launch_command,
|
launch = launch_command,
|
||||||
args = args.join(" ")
|
args = args.join(" ")
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
|
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
|
||||||
@ -102,7 +103,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
game_version: &GameVersion,
|
game_version: &GameVersion,
|
||||||
current_dir: &str,
|
current_dir: &str,
|
||||||
) -> String {
|
) -> Result<String, ProcessError> {
|
||||||
let umu_launcher = UMULauncher {};
|
let umu_launcher = UMULauncher {};
|
||||||
let umu_string = umu_launcher.create_launch_process(
|
let umu_string = umu_launcher.create_launch_process(
|
||||||
meta,
|
meta,
|
||||||
@ -110,15 +111,18 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
|||||||
args,
|
args,
|
||||||
game_version,
|
game_version,
|
||||||
current_dir,
|
current_dir,
|
||||||
);
|
)?;
|
||||||
let mut args_cmd = umu_string
|
let mut args_cmd = umu_string
|
||||||
.split("umu-run")
|
.split("umu-run")
|
||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
.into_iter();
|
.into_iter();
|
||||||
let args = args_cmd.next().unwrap().trim();
|
let args = args_cmd
|
||||||
let cmd = format!("umu-run{}", args_cmd.next().unwrap());
|
.next()
|
||||||
|
.ok_or(ProcessError::InvalidArguments(umu_string.clone()))?
|
||||||
|
.trim();
|
||||||
|
let cmd = format!("umu-run{}", args_cmd.next().ok_or(ProcessError::InvalidArguments(umu_string.clone()))?);
|
||||||
|
|
||||||
format!("{args} muvm -- {cmd}")
|
Ok(format!("{args} muvm -- {cmd}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
|
|||||||
@ -19,7 +19,7 @@ use tauri::{AppHandle, Emitter, Manager};
|
|||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, DB,
|
AppState,
|
||||||
database::{
|
database::{
|
||||||
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
|
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
|
||||||
models::data::{
|
models::data::{
|
||||||
@ -29,11 +29,11 @@ use crate::{
|
|||||||
},
|
},
|
||||||
error::process_error::ProcessError,
|
error::process_error::ProcessError,
|
||||||
games::{library::push_game_update, state::GameStatusManager},
|
games::{library::push_game_update, state::GameStatusManager},
|
||||||
playtime::events::{push_session_end, push_playtime_update},
|
|
||||||
process::{
|
process::{
|
||||||
format::DropFormatArgs,
|
format::DropFormatArgs,
|
||||||
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
|
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
|
||||||
},
|
},
|
||||||
|
lock,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct RunningProcess {
|
pub struct RunningProcess {
|
||||||
@ -119,7 +119,7 @@ impl ProcessManager<'_> {
|
|||||||
let dir = self.get_log_dir(game_id);
|
let dir = self.get_log_dir(game_id);
|
||||||
self.app_handle
|
self.app_handle
|
||||||
.opener()
|
.opener()
|
||||||
.open_path(dir.to_str().unwrap(), None::<&str>)
|
.open_path(dir.display().to_string(), None::<&str>)
|
||||||
.map_err(ProcessError::OpenerError)?;
|
.map_err(ProcessError::OpenerError)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -134,7 +134,13 @@ impl ProcessManager<'_> {
|
|||||||
|
|
||||||
debug!("process for {:?} exited with {:?}", &game_id, result);
|
debug!("process for {:?} exited with {:?}", &game_id, result);
|
||||||
|
|
||||||
let process = self.processes.remove(&game_id).unwrap();
|
let process = match self.processes.remove(&game_id) {
|
||||||
|
Some(process) => process,
|
||||||
|
None => {
|
||||||
|
info!("Attempted to stop process {game_id} which didn't exist");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
let meta = db_handle
|
let meta = db_handle
|
||||||
@ -142,7 +148,7 @@ impl ProcessManager<'_> {
|
|||||||
.installed_game_version
|
.installed_game_version
|
||||||
.get(&game_id)
|
.get(&game_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap();
|
.unwrap_or_else(|| panic!("Could not get installed version of {}", &game_id));
|
||||||
db_handle.applications.transient_statuses.remove(&meta);
|
db_handle.applications.transient_statuses.remove(&meta);
|
||||||
|
|
||||||
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
|
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
|
||||||
@ -167,20 +173,17 @@ impl ProcessManager<'_> {
|
|||||||
// Or if the status isn't 0
|
// Or if the status isn't 0
|
||||||
// Or if it's an error
|
// Or if it's an error
|
||||||
if !process.manually_killed
|
if !process.manually_killed
|
||||||
&& (elapsed.as_secs() <= 2 || result.is_err() || !result.unwrap().success())
|
&& (elapsed.as_secs() <= 2 || result.map_or(true, |r| !r.success()))
|
||||||
{
|
{
|
||||||
warn!("drop detected that the game {game_id} may have failed to launch properly");
|
warn!("drop detected that the game {game_id} may have failed to launch properly");
|
||||||
let _ = self.app_handle.emit("launch_external_error", &game_id);
|
let _ = self.app_handle.emit("launch_external_error", &game_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is too many unwraps for me to be comfortable
|
let version_data = match db_handle.applications.game_versions.get(&game_id) {
|
||||||
let version_data = db_handle
|
// This unwrap here should be resolved by just making the hashmap accept an option rather than just a String
|
||||||
.applications
|
Some(res) => res.get(&meta.version.unwrap()).expect("Failed to get game version from installed game versions. Is the database corrupted?"),
|
||||||
.game_versions
|
None => todo!(),
|
||||||
.get(&game_id)
|
};
|
||||||
.unwrap()
|
|
||||||
.get(&meta.version.unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let status = GameStatusManager::fetch_state(&game_id, &db_handle);
|
let status = GameStatusManager::fetch_state(&game_id, &db_handle);
|
||||||
|
|
||||||
@ -211,10 +214,10 @@ impl ProcessManager<'_> {
|
|||||||
.1)
|
.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result<bool, String> {
|
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> bool {
|
||||||
let db_lock = borrow_db_checked();
|
let db_lock = borrow_db_checked();
|
||||||
let process_handler = self.fetch_process_handler(&db_lock, state, platform);
|
let process_handler = self.fetch_process_handler(&db_lock, state, platform);
|
||||||
Ok(process_handler.is_ok())
|
process_handler.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch_process(
|
pub fn launch_process(
|
||||||
@ -226,9 +229,7 @@ impl ProcessManager<'_> {
|
|||||||
return Err(ProcessError::AlreadyRunning);
|
return Err(ProcessError::AlreadyRunning);
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = match DB
|
let version = match borrow_db_checked()
|
||||||
.borrow_data()
|
|
||||||
.unwrap()
|
|
||||||
.applications
|
.applications
|
||||||
.game_statuses
|
.game_statuses
|
||||||
.get(&game_id)
|
.get(&game_id)
|
||||||
@ -267,7 +268,7 @@ impl ProcessManager<'_> {
|
|||||||
debug!(
|
debug!(
|
||||||
"Launching process {:?} with version {:?}",
|
"Launching process {:?} with version {:?}",
|
||||||
&game_id,
|
&game_id,
|
||||||
db_lock.applications.game_versions.get(&game_id).unwrap()
|
db_lock.applications.game_versions.get(&game_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
let game_version = db_lock
|
let game_version = db_lock
|
||||||
@ -323,8 +324,9 @@ impl ProcessManager<'_> {
|
|||||||
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
|
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
let launch = PathBuf::from_str(install_dir).unwrap().join(launch);
|
let launch = PathBuf::from_str(install_dir).unwrap().join(launch);
|
||||||
let launch = launch.to_str().unwrap();
|
let launch = launch.display().to_string();
|
||||||
|
|
||||||
let launch_string = process_handler.create_launch_process(
|
let launch_string = process_handler.create_launch_process(
|
||||||
&meta,
|
&meta,
|
||||||
@ -332,7 +334,7 @@ impl ProcessManager<'_> {
|
|||||||
args.clone(),
|
args.clone(),
|
||||||
game_version,
|
game_version,
|
||||||
install_dir,
|
install_dir,
|
||||||
);
|
)?;
|
||||||
|
|
||||||
let format_args = DropFormatArgs::new(
|
let format_args = DropFormatArgs::new(
|
||||||
launch_string,
|
launch_string,
|
||||||
@ -393,18 +395,12 @@ impl ProcessManager<'_> {
|
|||||||
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
||||||
|
|
||||||
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
|
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
|
||||||
let app_state_handle = app_state.lock().unwrap();
|
let app_state_handle = lock!(app_state);
|
||||||
|
|
||||||
// End playtime tracking before processing finish
|
let mut process_manager_handle = app_state_handle
|
||||||
let playtime_manager_lock = app_state_handle.playtime_manager.lock().unwrap();
|
.process_manager
|
||||||
if let Ok(stats) = playtime_manager_lock.end_session(wait_thread_game_id.id.clone()) {
|
.lock()
|
||||||
debug!("Ended playtime tracking for game: {} (process finished)", wait_thread_game_id.id);
|
.expect("Failed to lock onto process manager");
|
||||||
push_session_end(&app_state_handle.app_handle, &wait_thread_game_id.id, &stats);
|
|
||||||
push_playtime_update(&app_state_handle.app_handle, &wait_thread_game_id.id, stats, false);
|
|
||||||
}
|
|
||||||
drop(playtime_manager_lock);
|
|
||||||
|
|
||||||
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
|
|
||||||
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
|
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
|
||||||
|
|
||||||
// As everything goes out of scope, they should get dropped
|
// As everything goes out of scope, they should get dropped
|
||||||
@ -478,7 +474,7 @@ pub trait ProcessHandler: Send + 'static {
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
game_version: &GameVersion,
|
game_version: &GameVersion,
|
||||||
current_dir: &str,
|
current_dir: &str,
|
||||||
) -> String;
|
) -> Result<String, ProcessError>;
|
||||||
|
|
||||||
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
|
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,10 @@ use tauri::{AppHandle, Emitter, Manager};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{
|
app_emit, database::{
|
||||||
db::{borrow_db_checked, borrow_db_mut_checked},
|
db::{borrow_db_checked, borrow_db_mut_checked},
|
||||||
models::data::DatabaseAuth,
|
models::data::DatabaseAuth,
|
||||||
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::{cache::clear_cached_object, requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}}, AppState, AppStatus, User
|
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, lock, remote::{cache::clear_cached_object, requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}}, AppState, AppStatus, User
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -51,12 +51,13 @@ struct HandshakeResponse {
|
|||||||
pub fn generate_authorization_header() -> String {
|
pub fn generate_authorization_header() -> String {
|
||||||
let certs = {
|
let certs = {
|
||||||
let db = borrow_db_checked();
|
let db = borrow_db_checked();
|
||||||
db.auth.clone().unwrap()
|
db.auth.clone().expect("Authorisation not initialised")
|
||||||
};
|
};
|
||||||
|
|
||||||
let nonce = Utc::now().timestamp_millis().to_string();
|
let nonce = Utc::now().timestamp_millis().to_string();
|
||||||
|
|
||||||
let signature = sign_nonce(certs.private, nonce.clone()).unwrap();
|
let signature =
|
||||||
|
sign_nonce(certs.private, nonce.clone()).expect("Failed to generate authorisation header");
|
||||||
|
|
||||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||||
}
|
}
|
||||||
@ -83,7 +84,7 @@ pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
|||||||
async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||||
let path_chunks: Vec<&str> = path.split('/').collect();
|
let path_chunks: Vec<&str> = path.split('/').collect();
|
||||||
if path_chunks.len() != 3 {
|
if path_chunks.len() != 3 {
|
||||||
app.emit("auth/failed", ()).unwrap();
|
app_emit!(app, "auth/failed", ());
|
||||||
return Err(RemoteAccessError::HandshakeFailed(
|
return Err(RemoteAccessError::HandshakeFailed(
|
||||||
"failed to parse token".to_string(),
|
"failed to parse token".to_string(),
|
||||||
));
|
));
|
||||||
@ -94,11 +95,15 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
|||||||
Url::parse(handle.base_url.as_str())?
|
Url::parse(handle.base_url.as_str())?
|
||||||
};
|
};
|
||||||
|
|
||||||
let client_id = path_chunks.get(1).unwrap();
|
let client_id = path_chunks
|
||||||
let token = path_chunks.get(2).unwrap();
|
.get(1)
|
||||||
|
.expect("Failed to get client id from path chunks");
|
||||||
|
let token = path_chunks
|
||||||
|
.get(2)
|
||||||
|
.expect("Failed to get token from path chunks");
|
||||||
let body = HandshakeRequestBody {
|
let body = HandshakeRequestBody {
|
||||||
client_id: (*client_id).to_string(),
|
client_id: (client_id).to_string(),
|
||||||
token: (*token).to_string(),
|
token: (token).to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||||
@ -116,37 +121,34 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
|||||||
private: response_struct.private,
|
private: response_struct.private,
|
||||||
cert: response_struct.certificate,
|
cert: response_struct.certificate,
|
||||||
client_id: response_struct.id,
|
client_id: response_struct.id,
|
||||||
web_token: None, // gets created later
|
web_token: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let web_token = {
|
let web_token = {
|
||||||
let header = generate_authorization_header();
|
let header = generate_authorization_header();
|
||||||
let token = client
|
let token = client
|
||||||
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
.post(base_url.join("/api/v1/client/user/webtoken")?)
|
||||||
.header("Authorization", header)
|
.header("Authorization", header)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
token.text().await.unwrap()
|
token.text().await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut handle = borrow_db_mut_checked();
|
let mut handle = borrow_db_mut_checked();
|
||||||
let mut_auth = handle.auth.as_mut().unwrap();
|
handle.auth.as_mut().unwrap().web_token = Some(web_token);
|
||||||
mut_auth.web_token = Some(web_token);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recieve_handshake(app: AppHandle, path: String) {
|
pub async fn recieve_handshake(app: AppHandle, path: String) {
|
||||||
// Tell the app we're processing
|
// Tell the app we're processing
|
||||||
app.emit("auth/processing", ()).unwrap();
|
app_emit!(app, "auth/processing", ());
|
||||||
|
|
||||||
let handshake_result = recieve_handshake_logic(&app, path).await;
|
let handshake_result = recieve_handshake_logic(&app, path).await;
|
||||||
if let Err(e) = handshake_result {
|
if let Err(e) = handshake_result {
|
||||||
warn!("error with authentication: {e}");
|
warn!("error with authentication: {e}");
|
||||||
app.emit("auth/failed", e.to_string()).unwrap();
|
app_emit!(app, "auth/failed", e.to_string());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +156,7 @@ pub async fn recieve_handshake(app: AppHandle, path: String) {
|
|||||||
|
|
||||||
let (app_status, user) = setup().await;
|
let (app_status, user) = setup().await;
|
||||||
|
|
||||||
let mut state_lock = app_state.lock().unwrap();
|
let mut state_lock = lock!(app_state);
|
||||||
|
|
||||||
state_lock.status = app_status;
|
state_lock.status = app_status;
|
||||||
state_lock.user = user;
|
state_lock.user = user;
|
||||||
@ -164,7 +166,7 @@ pub async fn recieve_handshake(app: AppHandle, path: String) {
|
|||||||
|
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
|
|
||||||
app.emit("auth/finished", ()).unwrap();
|
app_emit!(app, "auth/finished", ());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
||||||
@ -177,7 +179,7 @@ pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
|||||||
|
|
||||||
let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
|
let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
|
||||||
let body = InitiateRequestBody {
|
let body = InitiateRequestBody {
|
||||||
name: format!("{} (Desktop)", hostname.into_string().unwrap()),
|
name: format!("{} (Desktop)", hostname.display()),
|
||||||
platform: env::consts::OS.to_string(),
|
platform: env::consts::OS.to_string(),
|
||||||
capabilities: HashMap::from([
|
capabilities: HashMap::from([
|
||||||
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
||||||
@ -211,12 +213,14 @@ pub async fn setup() -> (AppStatus, Option<User>) {
|
|||||||
let user_result = match fetch_user().await {
|
let user_result = match fetch_user().await {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(RemoteAccessError::FetchError(_)) => {
|
Err(RemoteAccessError::FetchError(_)) => {
|
||||||
let user = get_cached_object::<User>("user").unwrap();
|
let user = get_cached_object::<User>("user").ok();
|
||||||
return (AppStatus::Offline, Some(user));
|
return (AppStatus::Offline, user);
|
||||||
}
|
}
|
||||||
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
||||||
};
|
};
|
||||||
cache_object("user", &user_result).unwrap();
|
if let Err(e) = cache_object("user", &user_result) {
|
||||||
|
warn!("Could not cache user object with error {e}");
|
||||||
|
}
|
||||||
return (AppStatus::SignedIn, Some(user_result));
|
return (AppStatus::SignedIn, Some(user_result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,16 +7,16 @@ use std::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{db::borrow_db_checked, models::data::Database},
|
database::{db::borrow_db_checked, models::data::Database},
|
||||||
error::remote_access_error::RemoteAccessError,
|
error::{cache_error::CacheError, remote_access_error::RemoteAccessError},
|
||||||
};
|
};
|
||||||
use bitcode::{Decode, DecodeOwned, Encode};
|
use bitcode::{Decode, DecodeOwned, Encode};
|
||||||
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
use http::{header::{CONTENT_TYPE}, response::Builder as ResponseBuilder, Response};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! offline {
|
macro_rules! offline {
|
||||||
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
||||||
|
|
||||||
async move { if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
|
async move { if $crate::borrow_db_checked().settings.force_offline || $crate::lock!($var).status == $crate::AppStatus::Offline {
|
||||||
$func2( $( $arg ), *).await
|
$func2( $( $arg ), *).await
|
||||||
} else {
|
} else {
|
||||||
$func1( $( $arg ), *).await
|
$func1( $( $arg ), *).await
|
||||||
@ -104,30 +104,36 @@ impl ObjectCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Response<Vec<u8>>> for ObjectCache {
|
impl TryFrom<Response<Vec<u8>>> for ObjectCache {
|
||||||
fn from(value: Response<Vec<u8>>) -> Self {
|
type Error = CacheError;
|
||||||
ObjectCache {
|
|
||||||
|
fn try_from(value: Response<Vec<u8>>) -> Result<Self, Self::Error> {
|
||||||
|
Ok(ObjectCache {
|
||||||
content_type: value
|
content_type: value
|
||||||
.headers()
|
.headers()
|
||||||
.get(CONTENT_TYPE)
|
.get(CONTENT_TYPE)
|
||||||
.unwrap()
|
.ok_or(CacheError::HeaderNotFound(CONTENT_TYPE))?
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.map_err(CacheError::ParseError)?
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
body: value.body().clone(),
|
body: value.body().clone(),
|
||||||
expiry: get_sys_time_in_secs() + 60 * 60 * 24,
|
expiry: get_sys_time_in_secs() + 60 * 60 * 24,
|
||||||
}
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<ObjectCache> for Response<Vec<u8>> {
|
impl TryFrom<ObjectCache> for Response<Vec<u8>> {
|
||||||
fn from(value: ObjectCache) -> Self {
|
type Error = CacheError;
|
||||||
|
fn try_from(value: ObjectCache) -> Result<Self, Self::Error> {
|
||||||
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type);
|
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type);
|
||||||
resp_builder.body(value.body).unwrap()
|
resp_builder.body(value.body).map_err(CacheError::ConstructionError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<&ObjectCache> for Response<Vec<u8>> {
|
impl TryFrom<&ObjectCache> for Response<Vec<u8>> {
|
||||||
fn from(value: &ObjectCache) -> Self {
|
type Error = CacheError;
|
||||||
|
|
||||||
|
fn try_from(value: &ObjectCache) -> Result<Self, Self::Error> {
|
||||||
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type.clone());
|
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type.clone());
|
||||||
resp_builder.body(value.body.clone()).unwrap()
|
resp_builder.body(value.body.clone()).map_err(CacheError::ConstructionError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,14 +8,16 @@ use tauri::{AppHandle, Emitter, Manager};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, AppStatus,
|
AppState, AppStatus, app_emit,
|
||||||
database::db::{borrow_db_checked, borrow_db_mut_checked},
|
database::db::{borrow_db_checked, borrow_db_mut_checked},
|
||||||
error::remote_access_error::RemoteAccessError,
|
error::remote_access_error::RemoteAccessError,
|
||||||
|
lock,
|
||||||
remote::{
|
remote::{
|
||||||
auth::generate_authorization_header,
|
auth::generate_authorization_header,
|
||||||
requests::generate_url,
|
requests::generate_url,
|
||||||
utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT},
|
utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT},
|
||||||
},
|
},
|
||||||
|
utils::webbrowser_open::webbrowser_open,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -40,7 +42,7 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
|||||||
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let url = base_url.join(&path).unwrap();
|
let url = base_url.join(&path)?;
|
||||||
|
|
||||||
Ok(url.to_string())
|
Ok(url.to_string())
|
||||||
}
|
}
|
||||||
@ -77,20 +79,20 @@ pub fn sign_out(app: AppHandle) {
|
|||||||
// Update app state
|
// Update app state
|
||||||
{
|
{
|
||||||
let app_state = app.state::<Mutex<AppState>>();
|
let app_state = app.state::<Mutex<AppState>>();
|
||||||
let mut app_state_handle = app_state.lock().unwrap();
|
let mut app_state_handle = lock!(app_state);
|
||||||
app_state_handle.status = AppStatus::SignedOut;
|
app_state_handle.status = AppStatus::SignedOut;
|
||||||
app_state_handle.user = None;
|
app_state_handle.user = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit event for frontend
|
// Emit event for frontend
|
||||||
app.emit("auth/signedout", ()).unwrap();
|
app_emit!(app, "auth/signedout", ());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn retry_connect(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<(), ()> {
|
pub async fn retry_connect(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<(), ()> {
|
||||||
let (app_status, user) = setup().await;
|
let (app_status, user) = setup().await;
|
||||||
|
|
||||||
let mut guard = state.lock().unwrap();
|
let mut guard = lock!(state);
|
||||||
guard.status = app_status;
|
guard.status = app_status;
|
||||||
guard.user = user;
|
guard.user = user;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
@ -109,7 +111,7 @@ pub fn auth_initiate() -> Result<(), RemoteAccessError> {
|
|||||||
let complete_redir_url = base_url.join(&redir_url)?;
|
let complete_redir_url = base_url.join(&redir_url)?;
|
||||||
|
|
||||||
debug!("opening web browser to continue authentication");
|
debug!("opening web browser to continue authentication");
|
||||||
webbrowser::open(complete_redir_url.as_ref()).unwrap();
|
webbrowser_open(complete_redir_url.as_ref());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +126,7 @@ struct CodeWebsocketResponse {
|
|||||||
pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
||||||
let base_url = {
|
let base_url = {
|
||||||
let db_lock = borrow_db_checked();
|
let db_lock = borrow_db_checked();
|
||||||
Url::parse(&db_lock.base_url.clone())?
|
Url::parse(&db_lock.base_url.clone())?.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let code = auth_initiate_logic("code".to_string())?;
|
let code = auth_initiate_logic("code".to_string())?;
|
||||||
@ -151,14 +153,13 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
|||||||
match response.response_type.as_str() {
|
match response.response_type.as_str() {
|
||||||
"token" => {
|
"token" => {
|
||||||
let recieve_app = app.clone();
|
let recieve_app = app.clone();
|
||||||
manual_recieve_handshake(recieve_app, response.value).await.unwrap();
|
manual_recieve_handshake(recieve_app, response.value).await;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
|
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(RemoteAccessError::HandshakeFailed(
|
Err(RemoteAccessError::HandshakeFailed(
|
||||||
"Failed to connect to websocket".to_string(),
|
"Failed to connect to websocket".to_string(),
|
||||||
))
|
))
|
||||||
@ -167,7 +168,7 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
|||||||
let result = load().await;
|
let result = load().await;
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
warn!("{err}");
|
warn!("{err}");
|
||||||
app.emit("auth/failed", err.to_string()).unwrap();
|
app_emit!(app, "auth/failed", err.to_string());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,8 +176,6 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), ()> {
|
pub async fn manual_recieve_handshake(app: AppHandle, token: String) {
|
||||||
recieve_handshake(app, format!("handshake/{token}")).await;
|
recieve_handshake(app, format!("handshake/{token}")).await;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,26 @@
|
|||||||
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
|
||||||
use log::warn;
|
use log::{debug, warn};
|
||||||
use tauri::UriSchemeResponder;
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
use crate::{database::db::DatabaseImpls, remote::utils::DROP_CLIENT_ASYNC, DB};
|
use crate::{database::db::DatabaseImpls, error::cache_error::CacheError, remote::utils::DROP_CLIENT_ASYNC, DB};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
auth::generate_authorization_header,
|
auth::generate_authorization_header,
|
||||||
cache::{ObjectCache, cache_object, get_cached_object},
|
cache::{ObjectCache, cache_object, get_cached_object},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub async fn fetch_object_wrapper(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
|
match fetch_object(request).await {
|
||||||
|
Ok(r) => responder.respond(r),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Cache error: {e}");
|
||||||
|
responder.respond(Response::builder().status(500).body(Vec::new()).expect("Failed to build error response"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_object(request: http::Request<Vec<u8>>) -> Result<Response<Vec<u8>>, CacheError>
|
||||||
|
{
|
||||||
// Drop leading /
|
// Drop leading /
|
||||||
let object_id = &request.uri().path()[1..];
|
let object_id = &request.uri().path()[1..];
|
||||||
|
|
||||||
@ -17,8 +28,7 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
|
|||||||
if let Ok(cache_result) = &cache_result
|
if let Ok(cache_result) = &cache_result
|
||||||
&& !cache_result.has_expired()
|
&& !cache_result.has_expired()
|
||||||
{
|
{
|
||||||
responder.respond(cache_result.into());
|
return cache_result.try_into();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let header = generate_authorization_header();
|
let header = generate_authorization_header();
|
||||||
@ -26,26 +36,40 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
|
|||||||
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
|
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
|
||||||
let response = client.get(url).header("Authorization", header).send().await;
|
let response = client.get(url).header("Authorization", header).send().await;
|
||||||
|
|
||||||
if response.is_err() {
|
match response {
|
||||||
match cache_result {
|
Ok(r) => {
|
||||||
Ok(cache_result) => responder.respond(cache_result.into()),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let response = response.unwrap();
|
|
||||||
|
|
||||||
let resp_builder = ResponseBuilder::new().header(
|
let resp_builder = ResponseBuilder::new().header(
|
||||||
CONTENT_TYPE,
|
CONTENT_TYPE,
|
||||||
response.headers().get("Content-Type").unwrap(),
|
r.headers()
|
||||||
|
.get("Content-Type")
|
||||||
|
.expect("Failed get Content-Type header"),
|
||||||
);
|
);
|
||||||
let data = Vec::from(response.bytes().await.unwrap());
|
let data = match r.bytes().await {
|
||||||
let resp = resp_builder.body(data).unwrap();
|
Ok(data) => Vec::from(data),
|
||||||
if cache_result.is_err() || cache_result.unwrap().has_expired() {
|
Err(e) => {
|
||||||
cache_object::<ObjectCache>(object_id, &resp.clone().into()).unwrap();
|
warn!(
|
||||||
|
"Could not get data from cache object {object_id} with error {e}",
|
||||||
|
);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let resp = resp_builder.body(data).expect("Failed to build object cache response body");
|
||||||
|
if cache_result.map_or(true, |x| x.has_expired()) {
|
||||||
|
cache_object::<ObjectCache>(object_id, &resp.clone().try_into()?)
|
||||||
|
.expect("Failed to create cached object");
|
||||||
}
|
}
|
||||||
|
|
||||||
responder.respond(resp);
|
Ok(resp)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Object fetch failed with error {e}. Attempting to download from cache");
|
||||||
|
match cache_result {
|
||||||
|
Ok(cache_result) => cache_result.try_into(),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("{e}");
|
||||||
|
Err(CacheError::Remote(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,57 +1,91 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
|
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
|
||||||
|
use log::{error, warn};
|
||||||
use tauri::UriSchemeResponder;
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC};
|
use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC, utils::webbrowser_open::webbrowser_open};
|
||||||
|
|
||||||
pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub async fn handle_server_proto_offline_wrapper(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
let four_oh_four = Response::builder()
|
responder.respond(match handle_server_proto_offline(request).await {
|
||||||
.status(StatusCode::NOT_FOUND)
|
Ok(res) => res,
|
||||||
.body(Vec::new())
|
Err(_) => unreachable!()
|
||||||
.unwrap();
|
});
|
||||||
responder.respond(four_oh_four);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode>{
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Vec::new())
|
||||||
|
.expect("Failed to build error response for proto offline"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
|
match handle_server_proto(request).await {
|
||||||
|
Ok(r) => responder.respond(r),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Cache error: {e}");
|
||||||
|
responder.respond(Response::builder().status(e).body(Vec::new()).expect("Failed to build error response"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode> {
|
||||||
let db_handle = borrow_db_checked();
|
let db_handle = borrow_db_checked();
|
||||||
let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
|
let auth = match db_handle.auth.as_ref() {
|
||||||
Some(e) => e,
|
Some(auth) => auth,
|
||||||
None => return,
|
None => {
|
||||||
|
error!("Could not find auth in database");
|
||||||
|
return Err(StatusCode::UNAUTHORIZED)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let remote_uri = db_handle.base_url.parse::<Uri>().unwrap();
|
let web_token = match &auth.web_token {
|
||||||
|
Some(token) => token,
|
||||||
|
None => return Err(StatusCode::UNAUTHORIZED),
|
||||||
|
};
|
||||||
|
let remote_uri = db_handle.base_url.parse::<Uri>().expect("Failed to parse base url");
|
||||||
|
|
||||||
let path = request.uri().path();
|
let path = request.uri().path();
|
||||||
|
|
||||||
let mut new_uri = request.uri().clone().into_parts();
|
let mut new_uri = request.uri().clone().into_parts();
|
||||||
new_uri.path_and_query =
|
new_uri.path_and_query =
|
||||||
Some(PathAndQuery::from_str(&format!("{path}?noWrapper=true")).unwrap());
|
Some(PathAndQuery::from_str(&format!("{path}?noWrapper=true")).expect("Failed to parse request path in proto"));
|
||||||
new_uri.authority = remote_uri.authority().cloned();
|
new_uri.authority = remote_uri.authority().cloned();
|
||||||
new_uri.scheme = remote_uri.scheme().cloned();
|
new_uri.scheme = remote_uri.scheme().cloned();
|
||||||
let new_uri = Uri::from_parts(new_uri).unwrap();
|
let err_msg = &format!("Failed to build new uri from parts {new_uri:?}");
|
||||||
|
let new_uri = Uri::from_parts(new_uri).expect(err_msg);
|
||||||
|
|
||||||
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
|
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
|
||||||
|
|
||||||
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
|
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
|
||||||
webbrowser::open(&new_uri.to_string()).unwrap();
|
webbrowser_open(new_uri.to_string());
|
||||||
return;
|
return Ok(Response::new(Vec::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = DROP_CLIENT_SYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
let response = client
|
let response = match client
|
||||||
.request(request.method().clone(), new_uri.to_string())
|
.request(request.method().clone(), new_uri.to_string())
|
||||||
.header("Authorization", format!("Bearer {web_token}"))
|
.header("Authorization", format!("Bearer {web_token}"))
|
||||||
.headers(request.headers().clone())
|
.headers(request.headers().clone())
|
||||||
.send()
|
.send() {
|
||||||
.unwrap();
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Could not send response. Got {e} when sending");
|
||||||
|
return Err(e.status().unwrap_or(StatusCode::BAD_REQUEST))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let response_status = response.status();
|
let response_status = response.status();
|
||||||
let response_body = response.bytes().unwrap();
|
let response_body = match response.bytes() {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(e) => return Err(e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
|
||||||
|
};
|
||||||
|
|
||||||
let http_response = Response::builder()
|
let http_response = Response::builder()
|
||||||
.status(response_status)
|
.status(response_status)
|
||||||
.body(response_body.to_vec())
|
.body(response_body.to_vec())
|
||||||
.unwrap();
|
.expect("Failed to build server proto response");
|
||||||
|
|
||||||
responder.respond(http_response);
|
Ok(http_response)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,7 @@ use serde::Deserialize;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, AppStatus,
|
database::db::{borrow_db_mut_checked, DATA_ROOT_DIR}, error::remote_access_error::RemoteAccessError, lock, AppState, AppStatus
|
||||||
database::db::{DATA_ROOT_DIR, borrow_db_mut_checked},
|
|
||||||
error::remote_access_error::RemoteAccessError,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -37,17 +35,42 @@ fn fetch_certificates() -> Vec<Certificate> {
|
|||||||
match entry {
|
match entry {
|
||||||
Ok(c) => {
|
Ok(c) => {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
File::open(c.path()).unwrap().read_to_end(&mut buf).unwrap();
|
match File::open(c.path()) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to open file at {} with error {}",
|
||||||
|
c.path().display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.read_to_end(&mut buf)
|
||||||
|
.unwrap_or_else(|e| panic!(
|
||||||
|
"Failed to read to end of certificate file {} with error {}",
|
||||||
|
c.path().display(),
|
||||||
|
e
|
||||||
|
));
|
||||||
|
|
||||||
for cert in Certificate::from_pem_bundle(&buf).unwrap() {
|
match Certificate::from_pem_bundle(&buf) {
|
||||||
|
Ok(certificates) => {
|
||||||
|
for cert in certificates {
|
||||||
certs.push(cert);
|
certs.push(cert);
|
||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
"added {} certificate(s) from {}",
|
"added {} certificate(s) from {}",
|
||||||
certs.len(),
|
certs.len(),
|
||||||
c.file_name().into_string().unwrap()
|
c.file_name().display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Err(e) => warn!(
|
||||||
|
"Invalid certificate file {} with error {}",
|
||||||
|
c.path().display(),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(_) => todo!(),
|
Err(_) => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +88,7 @@ pub fn get_client_sync() -> reqwest::blocking::Client {
|
|||||||
for cert in DROP_CERT_BUNDLE.iter() {
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
client = client.add_root_certificate(cert.clone());
|
client = client.add_root_certificate(cert.clone());
|
||||||
}
|
}
|
||||||
client.use_rustls_tls().build().unwrap()
|
client.use_rustls_tls().build().expect("Failed to build synchronous client")
|
||||||
}
|
}
|
||||||
pub fn get_client_async() -> reqwest::Client {
|
pub fn get_client_async() -> reqwest::Client {
|
||||||
let mut client = reqwest::ClientBuilder::new();
|
let mut client = reqwest::ClientBuilder::new();
|
||||||
@ -73,7 +96,7 @@ pub fn get_client_async() -> reqwest::Client {
|
|||||||
for cert in DROP_CERT_BUNDLE.iter() {
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
client = client.add_root_certificate(cert.clone());
|
client = client.add_root_certificate(cert.clone());
|
||||||
}
|
}
|
||||||
client.use_rustls_tls().build().unwrap()
|
client.use_rustls_tls().build().expect("Failed to build asynchronous client")
|
||||||
}
|
}
|
||||||
pub fn get_client_ws() -> reqwest::Client {
|
pub fn get_client_ws() -> reqwest::Client {
|
||||||
let mut client = reqwest::ClientBuilder::new();
|
let mut client = reqwest::ClientBuilder::new();
|
||||||
@ -81,7 +104,11 @@ pub fn get_client_ws() -> reqwest::Client {
|
|||||||
for cert in DROP_CERT_BUNDLE.iter() {
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
client = client.add_root_certificate(cert.clone());
|
client = client.add_root_certificate(cert.clone());
|
||||||
}
|
}
|
||||||
client.use_rustls_tls().http1_only().build().unwrap()
|
client
|
||||||
|
.use_rustls_tls()
|
||||||
|
.http1_only()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build websocket client")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn use_remote_logic(
|
pub async fn use_remote_logic(
|
||||||
@ -107,7 +134,7 @@ pub async fn use_remote_logic(
|
|||||||
return Err(RemoteAccessError::InvalidEndpoint);
|
return Err(RemoteAccessError::InvalidEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut app_state = state.lock().unwrap();
|
let mut app_state = lock!(state);
|
||||||
app_state.status = AppStatus::SignedOut;
|
app_state.status = AppStatus::SignedOut;
|
||||||
drop(app_state);
|
drop(app_state);
|
||||||
|
|
||||||
|
|||||||
6
src-tauri/src/utils/app_emit.rs
Normal file
6
src-tauri/src/utils/app_emit.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! app_emit {
|
||||||
|
($app:expr, $event:expr, $p:expr) => {
|
||||||
|
$app.emit($event, $p).expect(&format!("Failed to emit event {}", $event));
|
||||||
|
};
|
||||||
|
}
|
||||||
6
src-tauri/src/utils/download_manager_send.rs
Normal file
6
src-tauri/src/utils/download_manager_send.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! send {
|
||||||
|
($download_manager:expr, $signal:expr) => {
|
||||||
|
$download_manager.send($signal).unwrap_or_else(|_| panic!("Failed to send signal {} to the download manager", stringify!(signal)))
|
||||||
|
};
|
||||||
|
}
|
||||||
6
src-tauri/src/utils/lock.rs
Normal file
6
src-tauri/src/utils/lock.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! lock {
|
||||||
|
($mutex:expr) => {
|
||||||
|
$mutex.lock().unwrap_or_else(|_| panic!("Failed to lock onto {}", stringify!($mutex)))
|
||||||
|
};
|
||||||
|
}
|
||||||
4
src-tauri/src/utils/mod.rs
Normal file
4
src-tauri/src/utils/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mod app_emit;
|
||||||
|
mod download_manager_send;
|
||||||
|
mod lock;
|
||||||
|
pub mod webbrowser_open;
|
||||||
7
src-tauri/src/utils/webbrowser_open.rs
Normal file
7
src-tauri/src/utils/webbrowser_open.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use log::warn;
|
||||||
|
|
||||||
|
pub fn webbrowser_open<T: AsRef<str>>(url: T) {
|
||||||
|
if let Err(e) = webbrowser::open(url.as_ref()) {
|
||||||
|
warn!("Could not open web browser to url {} with error {}", url.as_ref(), e);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user