3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 | {{ nav.label }}
21 |
22 |
23 |
24 |
25 |
window.startDragging()"
27 | class="flex cursor-pointer grow h-full"
28 | />
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
97 |
--------------------------------------------------------------------------------
/components/HeaderButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/components/HeaderQueueWidget.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
20 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/components/HeaderUserWidget.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{
8 | state.user.displayName
9 | }}
10 |
11 |
12 |
13 |
14 |
15 |
23 |
26 |
27 |
31 |
32 |
33 |
{{
34 | state.user.displayName
35 | }}
36 |
37 |
38 |
39 |
40 |
41 |
49 | Admin Dashboard
50 |
51 |
52 |
56 | navigate(close, nav)"
58 | :href="nav.route"
59 | :class="[
60 | active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
61 | 'transition text-left block px-4 py-2 text-sm',
62 | ]"
63 | >
64 | {{ nav.label }}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
114 |
--------------------------------------------------------------------------------
/components/HeaderWidget.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 | {{ props.notifications }}
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/components/InitiateAuthModule.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
13 |
14 |
15 |
16 |
authWrapper_wrapper()"
18 | :disabled="loading"
19 | class="text-sm text-left font-semibold leading-7 text-blue-600"
20 | >
21 |
22 |
29 |
33 |
37 |
38 |
Loading...
39 |
40 |
41 | Sign in with your browser →
42 |
43 |
44 |
45 |
46 |
Having trouble?
47 |
48 | You can manually enter the token from your web browser.
49 |
50 |
51 |
60 | continueManual_wrapper()"
63 | class="w-fit"
64 | >
65 | Submit
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | {{ error }}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
110 |
113 |
118 |
119 |
120 |
121 |
122 |
166 |
--------------------------------------------------------------------------------
/components/LibrarySearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
33 |
34 |
43 |
44 |
47 | {{ nav.label }}
48 |
49 |
53 | {{ gameStatusText[games[nav.id].status.value.type] }}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
160 |
161 |
178 |
--------------------------------------------------------------------------------
/components/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/components/MiniHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
window.startDragging()">
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/components/OfflineHeaderWidget.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
17 |
18 |
--------------------------------------------------------------------------------
/components/PageWidget.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/components/WindowControl.vue:
--------------------------------------------------------------------------------
1 |
2 | minimise()">
3 |
4 |
5 | close()">
6 |
7 |
8 |
9 |
10 |
25 |
--------------------------------------------------------------------------------
/components/Wordmark.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
8 |
9 |
Drop
10 |
11 |
--------------------------------------------------------------------------------
/composables/app-state.ts:
--------------------------------------------------------------------------------
1 | import type { AppState } from "~/types";
2 |
3 | export const useAppState = () => useState
("state");
--------------------------------------------------------------------------------
/composables/current-page-engine.ts:
--------------------------------------------------------------------------------
1 | import type { RouteLocationNormalized } from "vue-router";
2 | import type { NavigationItem } from "~/types";
3 |
4 | export const useCurrentNavigationIndex = (
5 | navigation: Array
6 | ) => {
7 | const router = useRouter();
8 | const route = useRoute();
9 |
10 | const currentNavigation = ref(-1);
11 |
12 | function calculateCurrentNavIndex(to: RouteLocationNormalized) {
13 | const validOptions = navigation
14 | .map((e, i) => ({ ...e, index: i }))
15 | .filter((e) => to.fullPath.startsWith(e.prefix));
16 | const bestOption = validOptions
17 | .sort((a, b) => b.route.length - a.route.length)
18 | .at(0);
19 |
20 | return bestOption?.index ?? -1;
21 | }
22 |
23 | currentNavigation.value = calculateCurrentNavIndex(route);
24 |
25 | router.afterEach((to) => {
26 | currentNavigation.value = calculateCurrentNavIndex(to);
27 | });
28 |
29 | return {currentNavigation, recalculateNavigation: () => {
30 | currentNavigation.value = calculateCurrentNavIndex(route);
31 | }};
32 | };
33 |
--------------------------------------------------------------------------------
/composables/downloads.ts:
--------------------------------------------------------------------------------
1 | import { listen } from "@tauri-apps/api/event";
2 | import type { DownloadableMetadata } from "~/types";
3 |
4 | export type QueueState = {
5 | queue: Array<{
6 | meta: DownloadableMetadata;
7 | status: string;
8 | progress: number | null;
9 | current: number;
10 | max: number;
11 | }>;
12 | status: string;
13 | };
14 |
15 | export type StatsState = {
16 | speed: number; // Bytes per second
17 | time: number; // Seconds,
18 | };
19 |
20 | export const useQueueState = () =>
21 | useState("queue", () => ({ queue: [], status: "Unknown" }));
22 |
23 | export const useStatsState = () =>
24 | useState("stats", () => ({ speed: 0, time: 0 }));
25 |
26 | listen("update_queue", (event) => {
27 | const queue = useQueueState();
28 | queue.value = event.payload as QueueState;
29 | });
30 |
31 | listen("update_stats", (event) => {
32 | const stats = useStatsState();
33 | stats.value = event.payload as StatsState;
34 | });
35 |
--------------------------------------------------------------------------------
/composables/game.ts:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api/core";
2 | import { listen } from "@tauri-apps/api/event";
3 | import type { Game, GameStatus, GameStatusEnum, GameVersion } from "~/types";
4 |
5 | const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } =
6 | {};
7 |
8 | const gameStatusRegistry: { [key: string]: Ref } = {};
9 |
10 | type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } };
11 | export type SerializedGameStatus = [
12 | { type: GameStatusEnum },
13 | OptionGameStatus | null
14 | ];
15 |
16 | export const parseStatus = (status: SerializedGameStatus): GameStatus => {
17 | console.log(status);
18 | if (status[0]) {
19 | return {
20 | type: status[0].type,
21 | };
22 | } else if (status[1]) {
23 | const [[gameStatus, options]] = Object.entries(status[1]);
24 | return {
25 | type: gameStatus as GameStatusEnum,
26 | ...options,
27 | };
28 | } else {
29 | throw new Error("No game status");
30 | }
31 | };
32 |
33 | export const useGame = async (gameId: string) => {
34 | if (!gameRegistry[gameId]) {
35 | const data: {
36 | game: Game;
37 | status: SerializedGameStatus;
38 | version?: GameVersion;
39 | } = await invoke("fetch_game", {
40 | gameId,
41 | });
42 | gameRegistry[gameId] = { game: data.game, version: data.version };
43 | if (!gameStatusRegistry[gameId]) {
44 | gameStatusRegistry[gameId] = ref(parseStatus(data.status));
45 |
46 | listen(`update_game/${gameId}`, (event) => {
47 | const payload: {
48 | status: SerializedGameStatus;
49 | version?: GameVersion;
50 | } = event.payload as any;
51 | console.log(payload.status);
52 | gameStatusRegistry[gameId].value = parseStatus(payload.status);
53 |
54 | /**
55 | * I am not super happy about this.
56 | *
57 | * This will mean that we will still have a version assigned if we have a game installed then uninstall it.
58 | * It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
59 | * on transient state updates.
60 | */
61 | if (payload.version) {
62 | gameRegistry[gameId].version = payload.version;
63 | }
64 | });
65 | }
66 | }
67 |
68 | const game = gameRegistry[gameId];
69 | const status = gameStatusRegistry[gameId];
70 | return { ...game, status };
71 | };
72 |
73 | export type FrontendGameConfiguration = {
74 | launchString: string;
75 | };
76 |
--------------------------------------------------------------------------------
/composables/generateGameMeta.ts:
--------------------------------------------------------------------------------
1 | import { type DownloadableMetadata, DownloadableType } from '~/types'
2 |
3 | export default function generateGameMeta(gameId: string, version: string): DownloadableMetadata {
4 | return {
5 | id: gameId,
6 | version,
7 | downloadType: DownloadableType.Game
8 | }
9 | }
--------------------------------------------------------------------------------
/composables/state-navigation.ts:
--------------------------------------------------------------------------------
1 | import { listen } from "@tauri-apps/api/event";
2 | import { data } from "autoprefixer";
3 | import { AppStatus, type AppState } from "~/types";
4 |
5 | export function setupHooks() {
6 | const router = useRouter();
7 |
8 | listen("auth/processing", (event) => {
9 | router.push("/auth/processing");
10 | });
11 |
12 | listen("auth/failed", (event) => {
13 | router.push(
14 | `/auth/failed?error=${encodeURIComponent(event.payload as string)}`
15 | );
16 | });
17 |
18 | listen("auth/finished", (event) => {
19 | router.push("/store");
20 | });
21 |
22 | listen("download_error", (event) => {
23 | createModal(
24 | ModalType.Notification,
25 | {
26 | title: "Drop encountered an error while downloading",
27 | description: `Drop encountered an error while downloading your game: "${(
28 | event.payload as unknown as string
29 | ).toString()}"`,
30 | buttonText: "Close"
31 | },
32 | (e, c) => c()
33 | );
34 | });
35 |
36 | /*
37 |
38 | document.addEventListener("contextmenu", (event) => {
39 | event.target?.dispatchEvent(new Event("contextmenu"));
40 | event.preventDefault();
41 | });
42 |
43 | */
44 | }
45 |
46 | export function initialNavigation(state: Ref) {
47 | const router = useRouter();
48 |
49 | switch (state.value.status) {
50 | case AppStatus.NotConfigured:
51 | router.push({ path: "/setup" });
52 | break;
53 | case AppStatus.SignedOut:
54 | router.push("/auth");
55 | break;
56 | case AppStatus.SignedInNeedsReauth:
57 | router.push("/auth/signedout");
58 | break;
59 | case AppStatus.ServerUnavailable:
60 | router.push("/error/serverunavailable");
61 | break;
62 | default:
63 | router.push("/store");
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/composables/use-object.ts:
--------------------------------------------------------------------------------
1 | import { convertFileSrc } from "@tauri-apps/api/core";
2 |
3 | export const useObject = async (id: string) => {
4 | return convertFileSrc(id, "object");
5 | };
6 |
--------------------------------------------------------------------------------
/drop.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/error.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
11 |
14 |
15 |
16 | {{ error?.statusCode }}
17 |
18 |
21 | Oh no!
22 |
23 |
27 | {{ message }}
28 |
29 |
30 | An error occurred while responding to your request. If you believe
31 | this to be a bug, please report it. Try signing in and see if it
32 | resolves the issue.
33 |
34 |
42 |
43 |
44 |
63 |
66 |
71 |
72 |
73 |
74 |
75 |
76 |
91 |
--------------------------------------------------------------------------------
/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
19 |
22 |
23 |
26 | Unrecoverable error
27 |
28 |
29 | Drop encountered an error that it couldn't handle. Please
30 | restart the application and file a bug report.
31 |
32 |
33 | Error: {{ error }}
34 |
35 |
36 |
37 |
64 |
67 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
83 |
--------------------------------------------------------------------------------
/layouts/mini.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | // https://nuxt.com/docs/api/configuration/nuxt-config
2 | export default defineNuxtConfig({
3 | compatibilityDate: "2024-04-03",
4 |
5 | postcss: {
6 | plugins: {
7 | tailwindcss: {},
8 | autoprefixer: {},
9 | },
10 | },
11 |
12 | css: ["~/assets/main.scss"],
13 |
14 | ssr: false,
15 |
16 | extends: [["./drop-base"]],
17 | });
18 |
--------------------------------------------------------------------------------
/nvidia-prop-dev.sh:
--------------------------------------------------------------------------------
1 | WEBKIT_DISABLE_DMABUF_RENDERER=1 yarn tauri dev
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "drop-app",
3 | "private": true,
4 | "version": "0.3.0-rc-2",
5 | "type": "module",
6 | "scripts": {
7 | "build": "nuxt build",
8 | "dev": "nuxt dev",
9 | "generate": "nuxt generate",
10 | "preview": "nuxt preview",
11 | "postinstall": "nuxt prepare",
12 | "tauri": "tauri"
13 | },
14 | "dependencies": {
15 | "@headlessui/vue": "^1.7.23",
16 | "@heroicons/vue": "^2.1.5",
17 | "@nuxtjs/tailwindcss": "^6.12.2",
18 | "@tauri-apps/api": ">=2.0.0",
19 | "@tauri-apps/plugin-deep-link": "~2",
20 | "@tauri-apps/plugin-dialog": "^2.0.1",
21 | "@tauri-apps/plugin-os": "~2",
22 | "@tauri-apps/plugin-shell": "^2.2.1",
23 | "koa": "^2.16.1",
24 | "markdown-it": "^14.1.0",
25 | "micromark": "^4.0.1",
26 | "nuxt": "^3.16.0",
27 | "scss": "^0.2.4",
28 | "vue": "latest",
29 | "vue-router": "latest",
30 | "vuedraggable": "^4.1.0"
31 | },
32 | "devDependencies": {
33 | "@tailwindcss/forms": "^0.5.9",
34 | "@tailwindcss/typography": "^0.5.15",
35 | "@tauri-apps/cli": ">=2.0.0",
36 | "@types/markdown-it": "^14.1.2",
37 | "autoprefixer": "^10.4.20",
38 | "postcss": "^8.4.47",
39 | "sass-embedded": "^1.79.4",
40 | "tailwindcss": "^3.4.13",
41 | "typescript": "^5.8.3",
42 | "vue-tsc": "^2.2.10"
43 | },
44 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
45 | }
46 |
--------------------------------------------------------------------------------
/pages/auth/failed.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Authentication failed
8 |
9 |
10 |
11 | Drop encountered an error while connecting to your instance. Error:
12 | {{ message }}
13 |
14 |
15 |
16 | ← Back to authentication
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
35 |
--------------------------------------------------------------------------------
/pages/auth/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | Sign in to Drop
7 |
8 |
9 | To get started, sign in to your Drop instance by clicking below.
10 |
11 |
12 |
13 |
14 |
19 |
--------------------------------------------------------------------------------
/pages/auth/processing.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
16 |
20 |
21 |
Loading...
22 |
23 |
24 |
25 | Connecting to instance...
26 |
27 |
28 |
29 | Connecting to Drop server and fetching application information...
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
42 |
--------------------------------------------------------------------------------
/pages/auth/signedout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | You've been signed out
7 |
8 |
9 | Unfortunately, you've been signed out. To sign back in, click below.
10 |
11 |
12 |
13 |
14 |
19 |
--------------------------------------------------------------------------------
/pages/error/serverunavailable.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
13 |
14 |
17 | Server is unavailable
18 |
19 |
20 | We were unable to contact your Drop instance. See if you can open it
21 | in your web browser, or contact your server admin for help.
22 |
23 |
24 |
retry()"
26 | class="inline-flex gap-x-2 items-center text-sm text-left font-semibold leading-7 text-white"
27 | >
28 | Retry
29 |
30 |
34 | Connect to different instance →
35 |
36 |
37 |
38 |
39 |
64 |
67 |
72 |
73 |
74 |
75 |
76 |
89 |
--------------------------------------------------------------------------------
/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/pages/library.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
Error
18 |
21 | Failed to load library
22 |
23 |
24 | Drop couldn't load your library: "{{ error }}".
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
53 |
--------------------------------------------------------------------------------
/pages/library/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
Select a game
11 |
Choose a game from your library to view details
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/pages/quit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/pages/settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Settings
6 |
7 |
8 |
9 |
10 |
11 |
12 |
18 |
24 | {{ item.label }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
129 |
--------------------------------------------------------------------------------
/pages/settings/account.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | General
5 |
6 |
7 |
8 |
9 |
10 |
11 |
Sign out
12 |
13 | Sign out of your Drop account on this device
14 |
15 |
16 |
21 | Sign out
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{ error }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
65 |
--------------------------------------------------------------------------------
/pages/settings/debug.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug Information
6 |
7 |
8 | Technical information about your Drop client installation, helpful for
9 | debugging.
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Client ID
18 |
19 |
20 |
21 | {{ clientId || "Not signed in" }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Platform
30 |
31 |
32 |
33 | {{ platformInfo }}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Server URL
42 |
43 |
44 |
45 | {{ baseUrl || "Not connected" }}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Data Directory
54 |
55 |
56 |
57 | {{ dataDir || "Unknown" }}
58 |
59 |
60 |
61 |
62 | openDataDir()"
64 | type="button"
65 | class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
66 | >
67 |
68 | Open Data Directory
69 |
70 | openLogFile()"
72 | type="button"
73 | class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
74 | >
75 |
76 | Open Log File
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
137 |
--------------------------------------------------------------------------------
/pages/settings/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | General
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Start with system
13 |
14 |
15 | Drop will automatically start when you log into your computer
16 |
17 |
18 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
60 |
--------------------------------------------------------------------------------
/pages/settings/interface.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
--------------------------------------------------------------------------------
/pages/setup/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
12 | Let's get you set up
13 |
14 |
15 | You'll need to have set up a Drop instance and created an account on it. If you're connecting to another person's instance, you'll need the url and an account.
16 |
17 |
18 | Get started ->
23 |
24 |
25 |
26 |
27 |
28 |
33 |
--------------------------------------------------------------------------------
/pages/setup/server.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 | Connect to your Drop instance
11 |
12 |
13 |
14 |
74 |
75 |
76 |
77 |
121 |
--------------------------------------------------------------------------------
/pages/store/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/plugins/global-error-handler.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtPlugin((nuxtApp) => {
2 | // Also possible
3 | /*
4 | nuxtApp.hook("vue:error", (error, instance, info) => {
5 |
6 | console.error(error, info);
7 | const router = useRouter();
8 | router.replace(`/error`);
9 | });
10 | */
11 | });
12 |
--------------------------------------------------------------------------------
/plugins/vuedraggable.ts:
--------------------------------------------------------------------------------
1 | import draggable from "vuedraggable";
2 |
3 | export default defineNuxtPlugin((nuxtApp) => {
4 | nuxtApp.vueApp.component("draggable", draggable);
5 | });
6 |
--------------------------------------------------------------------------------
/public/fonts/helvetica/Helvetica-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/helvetica/Helvetica-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/helvetica/Helvetica-BoldOblique.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/helvetica/Helvetica-BoldOblique.woff
--------------------------------------------------------------------------------
/public/fonts/helvetica/Helvetica-Oblique.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/helvetica/Helvetica-Oblique.woff
--------------------------------------------------------------------------------
/public/fonts/helvetica/Helvetica.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/helvetica/Helvetica.woff
--------------------------------------------------------------------------------
/public/fonts/helvetica/helvetica-compressed-5871d14b6903a.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/helvetica/helvetica-compressed-5871d14b6903a.woff
--------------------------------------------------------------------------------
/public/fonts/helvetica/helvetica-light-587ebe5a59211.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/helvetica/helvetica-light-587ebe5a59211.woff
--------------------------------------------------------------------------------
/public/fonts/helvetica/helvetica-light-587ebe5a59211.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/helvetica/helvetica-light-587ebe5a59211.woff2
--------------------------------------------------------------------------------
/public/fonts/helvetica/helvetica-rounded-bold-5871d05ead8de.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/helvetica/helvetica-rounded-bold-5871d05ead8de.woff
--------------------------------------------------------------------------------
/public/fonts/inter/InterVariable-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/inter/InterVariable-Italic.ttf
--------------------------------------------------------------------------------
/public/fonts/inter/InterVariable.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/inter/InterVariable.ttf
--------------------------------------------------------------------------------
/public/fonts/motiva/MotivaSansBlack.woff.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/motiva/MotivaSansBlack.woff.ttf
--------------------------------------------------------------------------------
/public/fonts/motiva/MotivaSansBold.woff.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/motiva/MotivaSansBold.woff.ttf
--------------------------------------------------------------------------------
/public/fonts/motiva/MotivaSansExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/motiva/MotivaSansExtraBold.ttf
--------------------------------------------------------------------------------
/public/fonts/motiva/MotivaSansLight.woff.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/motiva/MotivaSansLight.woff.ttf
--------------------------------------------------------------------------------
/public/fonts/motiva/MotivaSansMedium.woff.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/motiva/MotivaSansMedium.woff.ttf
--------------------------------------------------------------------------------
/public/fonts/motiva/MotivaSansRegular.woff.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/motiva/MotivaSansRegular.woff.ttf
--------------------------------------------------------------------------------
/public/fonts/motiva/MotivaSansThin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/public/fonts/motiva/MotivaSansThin.ttf
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "drop-app"
3 | version = "0.3.0-rc-2"
4 | description = "The client application for the open-source, self-hosted game distribution platform Drop"
5 | authors = ["Drop OSS"]
6 | edition = "2021"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
11 | tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
12 |
13 | [lib]
14 | # The `_lib` suffix may seem redundant but it is necessary
15 | # to make the lib name unique and wouldn't conflict with the bin name.
16 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
17 | name = "drop_app_lib"
18 | crate-type = ["staticlib", "cdylib", "rlib"]
19 | rustflags = ["-C", "target-feature=+aes,+sse2"]
20 |
21 |
22 | [build-dependencies]
23 | tauri-build = { version = "2.0.0", features = [] }
24 |
25 | [dependencies]
26 | tauri-plugin-shell = "2.2.1"
27 | serde_json = "1"
28 | serde-binary = "0.5.0"
29 | rayon = "1.10.0"
30 | webbrowser = "1.0.2"
31 | url = "2.5.2"
32 | tauri-plugin-deep-link = "2"
33 | log = "0.4.22"
34 | hex = "0.4.3"
35 | tauri-plugin-dialog = "2"
36 | http = "1.1.0"
37 | urlencoding = "2.1.3"
38 | md5 = "0.7.0"
39 | chrono = "0.4.38"
40 | tauri-plugin-os = "2"
41 | boxcar = "0.2.7"
42 | umu-wrapper-lib = "0.1.0"
43 | tauri-plugin-autostart = "2.0.0"
44 | shared_child = "1.0.1"
45 | serde_with = "3.12.0"
46 | slice-deque = "0.3.0"
47 | throttle_my_fn = "0.2.6"
48 | parking_lot = "0.12.3"
49 | atomic-instant-full = "0.1.0"
50 | cacache = "13.1.0"
51 | http-serde = "2.1.1"
52 | reqwest-middleware = "0.4.0"
53 | reqwest-middleware-cache = "0.1.1"
54 | deranged = "=0.4.0"
55 | droplet-rs = "0.7.3"
56 | gethostname = "1.0.1"
57 | zstd = "0.13.3"
58 | tar = "0.4.44"
59 | rand = "0.9.1"
60 | regex = "1.11.1"
61 | tempfile = "3.19.1"
62 | schemars = "0.8.22"
63 | sha1 = "0.10.6"
64 | dirs = "6.0.0"
65 | whoami = "1.6.0"
66 | filetime = "0.2.25"
67 | walkdir = "2.5.0"
68 | known-folders = "1.2.0"
69 | native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
70 | # tailscale = { path = "./tailscale" }
71 |
72 | [dependencies.dynfmt]
73 | version = "0.1.5"
74 | features = ["curly"]
75 |
76 | [dependencies.tauri]
77 | version = "2.1.1"
78 | features = ["tray-icon"]
79 |
80 | [dependencies.tokio]
81 | version = "1.40.0"
82 | features = ["rt", "tokio-macros", "signal"]
83 |
84 | [dependencies.log4rs]
85 | version = "1.3.0"
86 | features = ["console_appender", "file_appender"]
87 |
88 | [dependencies.rustix]
89 | version = "0.38.37"
90 | features = ["fs"]
91 |
92 | [dependencies.uuid]
93 | version = "1.10.0"
94 | features = ["v4", "fast-rng", "macro-diagnostics"]
95 |
96 | [dependencies.rustbreak]
97 | version = "2"
98 | features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
99 |
100 | [dependencies.reqwest]
101 | version = "0.12"
102 | default-features = false
103 | features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
104 |
105 | [dependencies.serde]
106 | version = "1"
107 | features = ["derive", "rc"]
108 |
109 | [profile.release]
110 | lto = true
111 | codegen-units = 1
112 | panic = 'abort'
113 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capability for the main window",
5 | "windows": [
6 | "main"
7 | ],
8 | "permissions": [
9 | "core:default",
10 | "shell:allow-open",
11 | "core:window:allow-start-dragging",
12 | "core:window:allow-minimize",
13 | "core:window:allow-maximize",
14 | "core:window:allow-close",
15 | "deep-link:default",
16 | "dialog:default",
17 | "os:default"
18 | ]
19 | }
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-20x20@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-20x20@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-20x20@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-29x29@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-29x29@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-29x29@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-40x40@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-40x40@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-40x40@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-512@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-60x60@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-60x60@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-76x76@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-76x76@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/src-tauri/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly"
--------------------------------------------------------------------------------
/src-tauri/src/client/autostart.rs:
--------------------------------------------------------------------------------
1 | use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
2 | use log::debug;
3 | use tauri::AppHandle;
4 | use tauri_plugin_autostart::ManagerExt;
5 |
6 | pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
7 | let manager = app.autolaunch();
8 | if enabled {
9 | manager.enable().map_err(|e| e.to_string())?;
10 | debug!("enabled autostart");
11 | } else {
12 | manager.disable().map_err(|e| e.to_string())?;
13 | debug!("eisabled autostart");
14 | }
15 |
16 | // Store the state in DB
17 | let mut db_handle = borrow_db_mut_checked();
18 | db_handle.settings.autostart = enabled;
19 | drop(db_handle);
20 | save_db();
21 |
22 | Ok(())
23 | }
24 |
25 | pub fn get_autostart_enabled_logic(app: AppHandle) -> Result {
26 | // First check DB state
27 | let db_handle = borrow_db_checked();
28 | let db_state = db_handle.settings.autostart;
29 | drop(db_handle);
30 |
31 | // Get actual system state
32 | let manager = app.autolaunch();
33 | let system_state = manager.is_enabled()?;
34 |
35 | // If they don't match, sync to DB state
36 | if db_state != system_state {
37 | if db_state {
38 | manager.enable()?;
39 | } else {
40 | manager.disable()?;
41 | }
42 | }
43 |
44 | Ok(db_state)
45 | }
46 |
47 | // New function to sync state on startup
48 | pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
49 | let db_handle = borrow_db_checked();
50 | let should_be_enabled = db_handle.settings.autostart;
51 | drop(db_handle);
52 |
53 | let manager = app.autolaunch();
54 | let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
55 |
56 | if current_state != should_be_enabled {
57 | if should_be_enabled {
58 | manager.enable().map_err(|e| e.to_string())?;
59 | debug!("synced autostart: enabled");
60 | } else {
61 | manager.disable().map_err(|e| e.to_string())?;
62 | debug!("synced autostart: disabled");
63 | }
64 | }
65 |
66 | Ok(())
67 | }
68 | #[tauri::command]
69 | pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
70 | toggle_autostart_logic(app, enabled)
71 | }
72 |
73 | #[tauri::command]
74 | pub fn get_autostart_enabled(app: AppHandle) -> Result {
75 | get_autostart_enabled_logic(app)
76 | }
77 |
--------------------------------------------------------------------------------
/src-tauri/src/client/cleanup.rs:
--------------------------------------------------------------------------------
1 | use log::{debug, error};
2 | use tauri::AppHandle;
3 |
4 | use crate::AppState;
5 |
6 | #[tauri::command]
7 | pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex>>) {
8 | cleanup_and_exit(&app, &state);
9 | }
10 |
11 | pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex>>) {
12 | debug!("cleaning up and exiting application");
13 | let download_manager = state.lock().unwrap().download_manager.clone();
14 | match download_manager.ensure_terminated() {
15 | Ok(res) => match res {
16 | Ok(_) => debug!("download manager terminated correctly"),
17 | Err(_) => error!("download manager failed to terminate correctly"),
18 | },
19 | Err(e) => panic!("{:?}", e),
20 | }
21 |
22 | app.exit(0);
23 | }
24 |
--------------------------------------------------------------------------------
/src-tauri/src/client/commands.rs:
--------------------------------------------------------------------------------
1 | use crate::AppState;
2 |
3 | #[tauri::command]
4 | pub fn fetch_state(
5 | state: tauri::State<'_, std::sync::Mutex>>,
6 | ) -> Result {
7 | let guard = state.lock().unwrap();
8 | let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
9 | drop(guard);
10 | Ok(cloned_state)
11 | }
12 |
--------------------------------------------------------------------------------
/src-tauri/src/client/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod autostart;
2 | pub mod cleanup;
3 | pub mod commands;
--------------------------------------------------------------------------------
/src-tauri/src/cloud_saves/backup_manager.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, path::PathBuf, str::FromStr};
2 |
3 | use log::warn;
4 |
5 | use crate::{database::db::{GameVersion, DATA_ROOT_DIR}, error::backup_error::BackupError, process::process_manager::Platform};
6 |
7 | use super::path::CommonPath;
8 |
9 | pub struct BackupManager<'a> {
10 | pub current_platform: Platform,
11 | pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>,
12 | }
13 |
14 | impl BackupManager<'_> {
15 | pub fn new() -> Self {
16 | BackupManager {
17 | #[cfg(target_os = "windows")]
18 | current_platform: Platform::Windows,
19 |
20 | #[cfg(target_os = "macos")]
21 | current_platform: Platform::MacOs,
22 |
23 | #[cfg(target_os = "linux")]
24 | current_platform: Platform::Linux,
25 |
26 | sources: HashMap::from([
27 | // Current platform to target platform
28 | (
29 | (Platform::Windows, Platform::Windows),
30 | &WindowsBackupManager {} as &(dyn BackupHandler + Sync + Send),
31 | ),
32 | (
33 | (Platform::Linux, Platform::Linux),
34 | &LinuxBackupManager {} as &(dyn BackupHandler + Sync + Send),
35 | ),
36 | (
37 | (Platform::MacOs, Platform::MacOs),
38 | &MacBackupManager {} as &(dyn BackupHandler + Sync + Send),
39 | ),
40 |
41 | ]),
42 | }
43 | }
44 |
45 | }
46 |
47 | pub trait BackupHandler: Send + Sync {
48 | fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { Ok(DATA_ROOT_DIR.lock().unwrap().join("games")) }
49 | fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result { Ok(PathBuf::from_str(&game.game_id).unwrap()) }
50 | fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result { Ok(self.root_translate(path, game)?.join(self.game_translate(path, game)?)) }
51 | fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { let c = CommonPath::Home.get().ok_or(BackupError::NotFound); println!("{:?}", c); c }
52 | fn store_user_id_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result { PathBuf::from_str(&game.game_id).map_err(|_| BackupError::ParseError) }
53 | fn os_user_name_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { Ok(PathBuf::from_str(&whoami::username()).unwrap()) }
54 | fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) }
55 | fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) }
56 | fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) }
57 | fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) }
58 | fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) }
59 | fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) }
60 | fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) }
61 | fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result { warn!("Unexpected XDG Reference in Backup "); Err(BackupError::InvalidSystem) }
62 | fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result { warn!("Unexpected XDG Reference in Backup "); Err(BackupError::InvalidSystem) }
63 | fn skip_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { Ok(PathBuf::new()) }
64 | }
65 |
66 | pub struct LinuxBackupManager {}
67 | impl BackupHandler for LinuxBackupManager {
68 | fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result {
69 | Ok(CommonPath::Data.get().ok_or(BackupError::NotFound)?)
70 | }
71 | fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result {
72 | Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
73 | }
74 | }
75 | pub struct WindowsBackupManager {}
76 | impl BackupHandler for WindowsBackupManager {
77 | fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result {
78 | Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
79 | }
80 | fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result {
81 | Ok(CommonPath::DataLocal.get().ok_or(BackupError::NotFound)?)
82 | }
83 | fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result {
84 | Ok(CommonPath::DataLocalLow.get().ok_or(BackupError::NotFound)?)
85 | }
86 | fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result {
87 | Ok(PathBuf::from_str("C:/Windows").unwrap())
88 | }
89 | fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result {
90 | Ok(CommonPath::Document.get().ok_or(BackupError::NotFound)?)
91 |
92 | }
93 | fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result {
94 | Ok(PathBuf::from_str("C:/ProgramData").unwrap())
95 | }
96 | fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result {
97 | Ok(CommonPath::Public.get().ok_or(BackupError::NotFound)?)
98 |
99 | }
100 | }
101 | pub struct MacBackupManager {}
102 | impl BackupHandler for MacBackupManager {}
--------------------------------------------------------------------------------
/src-tauri/src/cloud_saves/conditions.rs:
--------------------------------------------------------------------------------
1 | use crate::process::process_manager::Platform;
2 |
3 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
4 | pub enum Condition {
5 | Os(Platform)
6 | }
7 |
--------------------------------------------------------------------------------
/src-tauri/src/cloud_saves/metadata.rs:
--------------------------------------------------------------------------------
1 | use crate::database::db::GameVersion;
2 |
3 | use super::conditions::{Condition};
4 |
5 |
6 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
7 | pub struct CloudSaveMetadata {
8 | pub files: Vec,
9 | pub game_version: GameVersion,
10 | pub save_id: String,
11 | }
12 |
13 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14 | pub struct GameFile {
15 | pub path: String,
16 | pub id: Option,
17 | pub data_type: DataType,
18 | pub tags: Vec,
19 | pub conditions: Vec
20 | }
21 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
22 | pub enum DataType {
23 | Registry,
24 | File,
25 | Other
26 | }
27 | #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
28 | #[serde(rename_all = "camelCase")]
29 | pub enum Tag {
30 | Config,
31 | Save,
32 | #[default]
33 | #[serde(other)]
34 | Other,
35 | }
--------------------------------------------------------------------------------
/src-tauri/src/cloud_saves/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod conditions;
2 | pub mod metadata;
3 | pub mod resolver;
4 | pub mod placeholder;
5 | pub mod normalise;
6 | pub mod path;
7 | pub mod backup_manager;
--------------------------------------------------------------------------------
/src-tauri/src/cloud_saves/normalise.rs:
--------------------------------------------------------------------------------
1 | use std::sync::LazyLock;
2 |
3 | use regex::Regex;
4 | use crate::process::process_manager::Platform;
5 |
6 | use super::placeholder::*;
7 |
8 |
9 | pub fn normalize(path: &str, os: Platform) -> String {
10 | let mut path = path.trim().trim_end_matches(['/', '\\']).replace('\\', "/");
11 |
12 | if path == "~" || path.starts_with("~/") {
13 | path = path.replacen('~', HOME, 1);
14 | }
15 |
16 | static CONSECUTIVE_SLASHES: LazyLock = LazyLock::new(|| Regex::new(r"/{2,}").unwrap());
17 | static UNNECESSARY_DOUBLE_STAR_1: LazyLock = LazyLock::new(|| Regex::new(r"([^/*])\*{2,}").unwrap());
18 | static UNNECESSARY_DOUBLE_STAR_2: LazyLock = LazyLock::new(|| Regex::new(r"\*{2,}([^/*])").unwrap());
19 | static ENDING_WILDCARD: LazyLock = LazyLock::new(|| Regex::new(r"(/\*)+$").unwrap());
20 | static ENDING_DOT: LazyLock = LazyLock::new(|| Regex::new(r"(/\.)$").unwrap());
21 | static INTERMEDIATE_DOT: LazyLock = LazyLock::new(|| Regex::new(r"(/\./)").unwrap());
22 | static BLANK_SEGMENT: LazyLock = LazyLock::new(|| Regex::new(r"(/\s+/)").unwrap());
23 | static APP_DATA: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%appdata%").unwrap());
24 | static APP_DATA_ROAMING: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Roaming").unwrap());
25 | static APP_DATA_LOCAL: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%localappdata%").unwrap());
26 | static APP_DATA_LOCAL_2: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Local/").unwrap());
27 | static USER_PROFILE: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%userprofile%").unwrap());
28 | static DOCUMENTS: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/Documents").unwrap());
29 |
30 | for (pattern, replacement) in [
31 | (&CONSECUTIVE_SLASHES, "/"),
32 | (&UNNECESSARY_DOUBLE_STAR_1, "${1}*"),
33 | (&UNNECESSARY_DOUBLE_STAR_2, "*${1}"),
34 | (&ENDING_WILDCARD, ""),
35 | (&ENDING_DOT, ""),
36 | (&INTERMEDIATE_DOT, "/"),
37 | (&BLANK_SEGMENT, "/"),
38 | (&APP_DATA, WIN_APP_DATA),
39 | (&APP_DATA_ROAMING, WIN_APP_DATA),
40 | (&APP_DATA_LOCAL, WIN_LOCAL_APP_DATA),
41 | (&APP_DATA_LOCAL_2, &format!("{}/", WIN_LOCAL_APP_DATA)),
42 | (&USER_PROFILE, HOME),
43 | (&DOCUMENTS, WIN_DOCUMENTS),
44 | ] {
45 | path = pattern.replace_all(&path, replacement).to_string();
46 | }
47 |
48 | if os == Platform::Windows {
49 | let documents_2: Regex = Regex::new(r"(?i)/Documents").unwrap();
50 |
51 | #[allow(clippy::single_element_loop)]
52 | for (pattern, replacement) in [(&documents_2, WIN_DOCUMENTS)] {
53 | path = pattern.replace_all(&path, replacement).to_string();
54 | }
55 | }
56 |
57 | for (pattern, replacement) in [
58 | ("{64BitSteamID}", STORE_USER_ID),
59 | ("{Steam3AccountID}", STORE_USER_ID),
60 | ] {
61 | path = path.replace(pattern, replacement);
62 | }
63 |
64 | path
65 | }
66 |
67 | fn too_broad(path: &str) -> bool {
68 | println!("Path: {}", path);
69 | use {BASE, HOME, ROOT, STORE_USER_ID, WIN_APP_DATA, WIN_DIR, WIN_DOCUMENTS, XDG_CONFIG, XDG_DATA};
70 |
71 | let path_lower = path.to_lowercase();
72 |
73 | for item in ALL {
74 | if path == *item {
75 | return true;
76 | }
77 | }
78 |
79 | for item in AVOID_WILDCARDS {
80 | if path.starts_with(&format!("{}/*", item)) || path.starts_with(&format!("{}/{}", item, STORE_USER_ID)) {
81 | return true;
82 | }
83 | }
84 |
85 | // These paths are present whether or not the game is installed.
86 | // If possible, they should be narrowed down on the wiki.
87 | for item in [
88 | format!("{}/{}", BASE, STORE_USER_ID), // because `` is handled as `*`
89 | format!("{}/Documents", HOME),
90 | format!("{}/Saved Games", HOME),
91 | format!("{}/AppData", HOME),
92 | format!("{}/AppData/Local", HOME),
93 | format!("{}/AppData/Local/Packages", HOME),
94 | format!("{}/AppData/LocalLow", HOME),
95 | format!("{}/AppData/Roaming", HOME),
96 | format!("{}/Documents/My Games", HOME),
97 | format!("{}/Library/Application Support", HOME),
98 | format!("{}/Library/Application Support/UserData", HOME),
99 | format!("{}/Library/Preferences", HOME),
100 | format!("{}/.renpy", HOME),
101 | format!("{}/.renpy/persistent", HOME),
102 | format!("{}/Library", HOME),
103 | format!("{}/Library/RenPy", HOME),
104 | format!("{}/Telltale Games", HOME),
105 | format!("{}/config", ROOT),
106 | format!("{}/MMFApplications", WIN_APP_DATA),
107 | format!("{}/RenPy", WIN_APP_DATA),
108 | format!("{}/RenPy/persistent", WIN_APP_DATA),
109 | format!("{}/win.ini", WIN_DIR),
110 | format!("{}/SysWOW64", WIN_DIR),
111 | format!("{}/My Games", WIN_DOCUMENTS),
112 | format!("{}/Telltale Games", WIN_DOCUMENTS),
113 | format!("{}/unity3d", XDG_CONFIG),
114 | format!("{}/unity3d", XDG_DATA),
115 | "C:/Program Files".to_string(),
116 | "C:/Program Files (x86)".to_string(),
117 | ] {
118 | let item = item.to_lowercase();
119 | if path_lower == item
120 | || path_lower.starts_with(&format!("{}/*", item))
121 | || path_lower.starts_with(&format!("{}/{}", item, STORE_USER_ID.to_lowercase()))
122 | || path_lower.starts_with(&format!("{}/savesdir", item))
123 | {
124 | return true;
125 | }
126 | }
127 |
128 |
129 | // Drive letters:
130 | let drives: Regex = Regex::new(r"^[a-zA-Z]:$").unwrap();
131 | if drives.is_match(path) {
132 | return true;
133 | }
134 |
135 | // Colon not for a drive letter
136 | if path.get(2..).is_some_and(|path| path.contains(':')) {
137 | return true;
138 | }
139 |
140 | // Root:
141 | if path == "/" {
142 | return true;
143 | }
144 |
145 | // Relative path wildcard:
146 | if path.starts_with('*') {
147 | return true;
148 | }
149 |
150 | false
151 | }
152 |
153 | pub fn usable(path: &str) -> bool {
154 | let unprintable: Regex = Regex::new(r"(\p{Cc}|\p{Cf})").unwrap();
155 |
156 | !path.is_empty()
157 | && !path.contains("{{")
158 | && !path.starts_with("./")
159 | && !path.starts_with("../")
160 | && !too_broad(path)
161 | && !unprintable.is_match(path)
162 | }
--------------------------------------------------------------------------------
/src-tauri/src/cloud_saves/path.rs:
--------------------------------------------------------------------------------
1 | use std::{path::PathBuf, sync::LazyLock};
2 |
3 | pub enum CommonPath {
4 | Config,
5 | Data,
6 | DataLocal,
7 | DataLocalLow,
8 | Document,
9 | Home,
10 | Public,
11 | SavedGames,
12 | }
13 |
14 | impl CommonPath {
15 | pub fn get(&self) -> Option {
16 | static CONFIG: LazyLock> = LazyLock::new(|| dirs::config_dir());
17 | static DATA: LazyLock > = LazyLock::new(|| dirs::data_dir());
18 | static DATA_LOCAL: LazyLock > = LazyLock::new(|| dirs::data_local_dir());
19 | static DOCUMENT: LazyLock > = LazyLock::new(|| dirs::document_dir());
20 | static HOME: LazyLock > = LazyLock::new(|| dirs::home_dir());
21 | static PUBLIC: LazyLock > = LazyLock::new(|| dirs::public_dir());
22 |
23 | #[cfg(windows)]
24 | static DATA_LOCAL_LOW: LazyLock > = LazyLock::new(|| {
25 | known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppDataLow)
26 | });
27 | #[cfg(not(windows))]
28 | static DATA_LOCAL_LOW: Option = None;
29 |
30 | #[cfg(windows)]
31 | static SAVED_GAMES: LazyLock> = LazyLock::new(|| {
32 | known_folders::get_known_folder_path(known_folders::KnownFolder::SavedGames)
33 | });
34 | #[cfg(not(windows))]
35 | static SAVED_GAMES: Option = None;
36 |
37 | match self {
38 | Self::Config => CONFIG.clone(),
39 | Self::Data => DATA.clone(),
40 | Self::DataLocal => DATA_LOCAL.clone(),
41 | Self::DataLocalLow => DATA_LOCAL_LOW.clone(),
42 | Self::Document => DOCUMENT.clone(),
43 | Self::Home => HOME.clone(),
44 | Self::Public => PUBLIC.clone(),
45 | Self::SavedGames => SAVED_GAMES.clone(),
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src-tauri/src/cloud_saves/placeholder.rs:
--------------------------------------------------------------------------------
1 | use std::sync::LazyLock;
2 |
3 | pub const ALL: &[&str] = &[
4 | ROOT,
5 | GAME,
6 | BASE,
7 | HOME,
8 | STORE_USER_ID,
9 | OS_USER_NAME,
10 | WIN_APP_DATA,
11 | WIN_LOCAL_APP_DATA,
12 | WIN_DOCUMENTS,
13 | WIN_PUBLIC,
14 | WIN_PROGRAM_DATA,
15 | WIN_DIR,
16 | XDG_DATA,
17 | XDG_CONFIG,
18 | ];
19 |
20 | /// These are paths where `/*/` is suspicious.
21 | pub const AVOID_WILDCARDS: &[&str] = &[
22 | ROOT,
23 | HOME,
24 | WIN_APP_DATA,
25 | WIN_LOCAL_APP_DATA,
26 | WIN_DOCUMENTS,
27 | WIN_PUBLIC,
28 | WIN_PROGRAM_DATA,
29 | WIN_DIR,
30 | XDG_DATA,
31 | XDG_CONFIG,
32 | ];
33 |
34 | pub const ROOT: &str = ""; // a directory where games are installed (configured in backup tool)
35 | pub const GAME: &str = ""; // an installDir (if defined) or the game's canonical name in the manifest
36 | pub const BASE: &str = " "; // shorthand for / (unless overridden by store-specific rules)
37 | pub const HOME: &str = ""; // current user's home directory in the OS (~)
38 | pub const STORE_USER_ID: &str = ""; // a store-specific id from the manifest, corresponding to the root's store type
39 | pub const OS_USER_NAME: &str = ""; // current user's ID in the game store
40 | pub const WIN_APP_DATA: &str = ""; // current user's name in the OS
41 | pub const WIN_LOCAL_APP_DATA: &str = ""; // %APPDATA% on Windows
42 | pub const WIN_LOCAL_APP_DATA_LOW: &str = ""; // %LOCALAPPDATA% on Windows
43 | pub const WIN_DOCUMENTS: &str = ""; // /AppData/LocalLow on Windows
44 | pub const WIN_PUBLIC: &str = ""; // /Documents (f.k.a. /My Documents) or a localized equivalent on Windows
45 | pub const WIN_PROGRAM_DATA: &str = ""; // %PUBLIC% on Windows
46 | pub const WIN_DIR: &str = ""; // %PROGRAMDATA% on Windows
47 | pub const XDG_DATA: &str = ""; // %WINDIR% on Windows
48 | pub const XDG_CONFIG: &str = ""; // $XDG_DATA_HOME on Linux
49 | pub const SKIP: &str = ""; // $XDG_CONFIG_HOME on Linux
50 |
51 | pub static OS_USERNAME: LazyLock = LazyLock::new(|| whoami::username());
--------------------------------------------------------------------------------
/src-tauri/src/cloud_saves/strict_path.rs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Drop-OSS/drop-app/065eb2356affe9fe709932e5b143ca9f705b041c/src-tauri/src/cloud_saves/strict_path.rs
--------------------------------------------------------------------------------
/src-tauri/src/database/commands.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::create_dir_all,
3 | io::{Error, ErrorKind},
4 | path::{Path, PathBuf},
5 | };
6 |
7 | use serde_json::Value;
8 |
9 | use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError};
10 |
11 | use super::{
12 | db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
13 | debug::SystemData,
14 | models::data::Settings,
15 | };
16 |
17 | // Will, in future, return disk/remaining size
18 | // Just returns the directories that have been set up
19 | #[tauri::command]
20 | pub fn fetch_download_dir_stats() -> Vec {
21 | let lock = borrow_db_checked();
22 | lock.applications.install_dirs.clone()
23 | }
24 |
25 | #[tauri::command]
26 | pub fn delete_download_dir(index: usize) {
27 | let mut lock = borrow_db_mut_checked();
28 | lock.applications.install_dirs.remove(index);
29 | drop(lock);
30 | save_db();
31 | }
32 |
33 | #[tauri::command]
34 | pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> {
35 | // Check the new directory is all good
36 | let new_dir_path = Path::new(&new_dir);
37 | if new_dir_path.exists() {
38 | let dir_contents = new_dir_path.read_dir()?;
39 | if dir_contents.count() != 0 {
40 | return Err(Error::new(
41 | ErrorKind::DirectoryNotEmpty,
42 | "Selected directory cannot contain any existing files",
43 | )
44 | .into());
45 | }
46 | } else {
47 | create_dir_all(new_dir_path)?;
48 | }
49 |
50 | // Add it to the dictionary
51 | let mut lock = borrow_db_mut_checked();
52 | if lock.applications.install_dirs.contains(&new_dir) {
53 | return Err(Error::new(
54 | ErrorKind::AlreadyExists,
55 | "Selected directory already exists in database",
56 | )
57 | .into());
58 | }
59 | lock.applications.install_dirs.push(new_dir);
60 | drop(lock);
61 | save_db();
62 |
63 | Ok(())
64 | }
65 |
66 | #[tauri::command]
67 | pub fn update_settings(new_settings: Value) {
68 | let mut db_lock = borrow_db_mut_checked();
69 | let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
70 | for (key, value) in new_settings.as_object().unwrap() {
71 | current_settings[key] = value.clone();
72 | }
73 | let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
74 | db_lock.settings = new_settings;
75 | drop(db_lock);
76 | save_db();
77 | }
78 | #[tauri::command]
79 | pub fn fetch_settings() -> Settings {
80 | borrow_db_checked().settings.clone()
81 | }
82 | #[tauri::command]
83 | pub fn fetch_system_data() -> SystemData {
84 | let db_handle = borrow_db_checked();
85 | SystemData::new(
86 | db_handle.auth.as_ref().unwrap().client_id.clone(),
87 | db_handle.base_url.clone(),
88 | DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
89 | std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
90 | )
91 | }
92 |
--------------------------------------------------------------------------------
/src-tauri/src/database/db.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::{self, create_dir_all},
3 | path::PathBuf,
4 | sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
5 | };
6 |
7 | use chrono::Utc;
8 | use log::{debug, error, info, warn};
9 | use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
10 | use serde::{de::DeserializeOwned, Serialize};
11 | use url::Url;
12 |
13 | use crate::DB;
14 |
15 | use super::models::data::Database;
16 |
17 | pub static DATA_ROOT_DIR: LazyLock> =
18 | LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
19 |
20 |
21 | // Custom JSON serializer to support everything we need
22 | #[derive(Debug, Default, Clone)]
23 | pub struct DropDatabaseSerializer;
24 |
25 | impl DeSerializer
26 | for DropDatabaseSerializer
27 | {
28 | fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult> {
29 | native_model::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
30 | }
31 |
32 | fn deserialize(&self, mut s: R) -> rustbreak::error::DeSerResult {
33 | let mut buf = Vec::new();
34 | s.read_to_end(&mut buf)
35 | .map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
36 | let (val, _version) =
37 | native_model::decode::(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
38 | Ok(val)
39 | }
40 | }
41 |
42 | pub type DatabaseInterface =
43 | rustbreak::Database;
44 |
45 | pub trait DatabaseImpls {
46 | fn set_up_database() -> DatabaseInterface;
47 | fn database_is_set_up(&self) -> bool;
48 | fn fetch_base_url(&self) -> Url;
49 | }
50 | impl DatabaseImpls for DatabaseInterface {
51 | fn set_up_database() -> DatabaseInterface {
52 | let data_root_dir = DATA_ROOT_DIR.lock().unwrap();
53 | let db_path = data_root_dir.join("drop.db");
54 | let games_base_dir = data_root_dir.join("games");
55 | let logs_root_dir = data_root_dir.join("logs");
56 | let cache_dir = data_root_dir.join("cache");
57 | let pfx_dir = data_root_dir.join("pfx");
58 |
59 | debug!("creating data directory at {:?}", data_root_dir);
60 | create_dir_all(data_root_dir.clone()).unwrap();
61 | create_dir_all(&games_base_dir).unwrap();
62 | create_dir_all(&logs_root_dir).unwrap();
63 | create_dir_all(&cache_dir).unwrap();
64 | create_dir_all(&pfx_dir).unwrap();
65 |
66 | let exists = fs::exists(db_path.clone()).unwrap();
67 |
68 | match exists {
69 | true => match PathDatabase::load_from_path(db_path.clone()) {
70 | Ok(db) => db,
71 | Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
72 | },
73 | false => {
74 | let default = Database::new(games_base_dir, None, cache_dir);
75 | debug!(
76 | "Creating database at path {}",
77 | db_path.as_os_str().to_str().unwrap()
78 | );
79 | PathDatabase::create_at_path(db_path, default)
80 | .expect("Database could not be created")
81 | }
82 | }
83 | }
84 |
85 | fn database_is_set_up(&self) -> bool {
86 | !self.borrow_data().unwrap().base_url.is_empty()
87 | }
88 |
89 | fn fetch_base_url(&self) -> Url {
90 | let handle = self.borrow_data().unwrap();
91 | Url::parse(&handle.base_url).unwrap()
92 | }
93 | }
94 |
95 | // TODO: Make the error relelvant rather than just assume that it's a Deserialize error
96 | fn handle_invalid_database(
97 | _e: RustbreakError,
98 | db_path: PathBuf,
99 | games_base_dir: PathBuf,
100 | cache_dir: PathBuf,
101 | ) -> rustbreak::Database {
102 | warn!("{}", _e);
103 | let new_path = {
104 | let time = Utc::now().timestamp();
105 | let mut base = db_path.clone();
106 | base.set_file_name(format!("drop.db.backup-{}", time));
107 | base
108 | };
109 | info!(
110 | "old database stored at: {}",
111 | new_path.to_string_lossy().to_string()
112 | );
113 | fs::rename(&db_path, &new_path).unwrap();
114 |
115 | let db = Database::new(
116 | games_base_dir.into_os_string().into_string().unwrap(),
117 | Some(new_path),
118 | cache_dir,
119 | );
120 |
121 | PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
122 | }
123 |
124 | pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
125 | match DB.borrow_data() {
126 | Ok(data) => data,
127 | Err(e) => {
128 | error!("database borrow failed with error {}", e);
129 | panic!("database borrow failed with error {}", e);
130 | }
131 | }
132 | }
133 |
134 | pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
135 | match DB.borrow_data_mut() {
136 | Ok(data) => data,
137 | Err(e) => {
138 | error!("database borrow mut failed with error {}", e);
139 | panic!("database borrow mut failed with error {}", e);
140 | }
141 | }
142 | }
143 |
144 | pub fn save_db() {
145 | match DB.save() {
146 | Ok(_) => {}
147 | Err(e) => {
148 | error!("database failed to save with error {}", e);
149 | panic!("database failed to save with error {}", e)
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src-tauri/src/database/debug.rs:
--------------------------------------------------------------------------------
1 | use serde::Serialize;
2 |
3 | #[derive(Serialize)]
4 | #[serde(rename_all = "camelCase")]
5 | pub struct SystemData {
6 | client_id: String,
7 | base_url: String,
8 | data_dir: String,
9 | log_level: String,
10 | }
11 |
12 | impl SystemData {
13 | pub fn new(client_id: String, base_url: String, data_dir: String, log_level: String) -> Self {
14 | Self {
15 | client_id,
16 | base_url,
17 | data_dir,
18 | log_level,
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src-tauri/src/database/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod commands;
2 | pub mod db;
3 | pub mod debug;
4 | pub mod models;
5 |
--------------------------------------------------------------------------------
/src-tauri/src/download_manager/commands.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Mutex;
2 |
3 | use crate::{database::models::data::DownloadableMetadata, AppState};
4 |
5 | #[tauri::command]
6 | pub fn pause_downloads(state: tauri::State<'_, Mutex>) {
7 | state.lock().unwrap().download_manager.pause_downloads()
8 | }
9 |
10 | #[tauri::command]
11 | pub fn resume_downloads(state: tauri::State<'_, Mutex>) {
12 | state.lock().unwrap().download_manager.resume_downloads()
13 | }
14 |
15 | #[tauri::command]
16 | pub fn move_download_in_queue(
17 | state: tauri::State<'_, Mutex>,
18 | old_index: usize,
19 | new_index: usize,
20 | ) {
21 | state
22 | .lock()
23 | .unwrap()
24 | .download_manager
25 | .rearrange(old_index, new_index)
26 | }
27 |
28 | #[tauri::command]
29 | pub fn cancel_game(state: tauri::State<'_, Mutex>, meta: DownloadableMetadata) {
30 | state.lock().unwrap().download_manager.cancel(meta)
31 | }
32 |
--------------------------------------------------------------------------------
/src-tauri/src/download_manager/downloadable.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use tauri::AppHandle;
4 |
5 | use crate::{
6 | database::models::data::DownloadableMetadata,
7 | error::application_download_error::ApplicationDownloadError,
8 | };
9 |
10 | use super::{
11 | download_manager::DownloadStatus, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
12 | };
13 |
14 | pub trait Downloadable: Send + Sync {
15 | fn download(&self, app_handle: &AppHandle) -> Result;
16 | fn progress(&self) -> Arc;
17 | fn control_flag(&self) -> DownloadThreadControl;
18 | fn status(&self) -> DownloadStatus;
19 | fn metadata(&self) -> DownloadableMetadata;
20 | fn on_initialised(&self, app_handle: &AppHandle);
21 | fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
22 | fn on_complete(&self, app_handle: &AppHandle);
23 | fn on_incomplete(&self, app_handle: &AppHandle);
24 | fn on_cancelled(&self, app_handle: &AppHandle);
25 | }
26 |
--------------------------------------------------------------------------------
/src-tauri/src/download_manager/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod commands;
2 | pub mod download_manager;
3 | pub mod download_manager_builder;
4 | pub mod downloadable;
5 | pub mod util;
--------------------------------------------------------------------------------
/src-tauri/src/download_manager/util/download_thread_control_flag.rs:
--------------------------------------------------------------------------------
1 | use std::sync::{
2 | atomic::{AtomicBool, Ordering},
3 | Arc,
4 | };
5 |
6 | #[derive(PartialEq, Eq, PartialOrd, Ord)]
7 | pub enum DownloadThreadControlFlag {
8 | Stop,
9 | Go,
10 | }
11 | /// Go => true
12 | /// Stop => false
13 | impl From for bool {
14 | fn from(value: DownloadThreadControlFlag) -> Self {
15 | match value {
16 | DownloadThreadControlFlag::Go => true,
17 | DownloadThreadControlFlag::Stop => false,
18 | }
19 | }
20 | }
21 | /// true => Go
22 | /// false => Stop
23 | impl From for DownloadThreadControlFlag {
24 | fn from(value: bool) -> Self {
25 | match value {
26 | true => DownloadThreadControlFlag::Go,
27 | false => DownloadThreadControlFlag::Stop,
28 | }
29 | }
30 | }
31 |
32 | #[derive(Clone)]
33 | pub struct DownloadThreadControl {
34 | inner: Arc,
35 | }
36 |
37 | impl DownloadThreadControl {
38 | pub fn new(flag: DownloadThreadControlFlag) -> Self {
39 | Self {
40 | inner: Arc::new(AtomicBool::new(flag.into())),
41 | }
42 | }
43 | pub fn get(&self) -> DownloadThreadControlFlag {
44 | self.inner.load(Ordering::Relaxed).into()
45 | }
46 | pub fn set(&self, flag: DownloadThreadControlFlag) {
47 | self.inner.store(flag.into(), Ordering::Relaxed);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src-tauri/src/download_manager/util/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod progress_object;
2 | pub mod queue;
3 | pub mod rolling_progress_updates;
4 | pub mod download_thread_control_flag;
--------------------------------------------------------------------------------
/src-tauri/src/download_manager/util/progress_object.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | sync::{
3 | atomic::{AtomicUsize, Ordering},
4 | mpsc::Sender,
5 | Arc, Mutex,
6 | },
7 | time::{Duration, Instant},
8 | };
9 |
10 | use atomic_instant_full::AtomicInstant;
11 | use throttle_my_fn::throttle;
12 |
13 | use crate::download_manager::download_manager::DownloadManagerSignal;
14 |
15 | use super::{
16 | rolling_progress_updates::RollingProgressWindow,
17 | };
18 |
19 | #[derive(Clone)]
20 | pub struct ProgressObject {
21 | max: Arc>,
22 | progress_instances: Arc>>>,
23 | start: Arc>,
24 | sender: Sender,
25 | //last_update: Arc>,
26 | last_update_time: Arc,
27 | bytes_last_update: Arc,
28 | rolling: RollingProgressWindow<250>,
29 | }
30 |
31 | pub struct ProgressHandle {
32 | progress: Arc,
33 | progress_object: Arc,
34 | }
35 |
36 | impl ProgressHandle {
37 | pub fn new(progress: Arc, progress_object: Arc) -> Self {
38 | Self {
39 | progress,
40 | progress_object,
41 | }
42 | }
43 | pub fn set(&self, amount: usize) {
44 | self.progress.store(amount, Ordering::Relaxed);
45 | }
46 | pub fn add(&self, amount: usize) {
47 | self.progress
48 | .fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
49 | calculate_update(&self.progress_object);
50 | }
51 | pub fn skip(&self, amount: usize) {
52 | self.progress
53 | .fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
54 | // Offset the bytes at last offset by this amount
55 | self.progress_object
56 | .bytes_last_update
57 | .fetch_add(amount, Ordering::Relaxed);
58 | // Dont' fire update
59 | }
60 | }
61 |
62 | impl ProgressObject {
63 | pub fn new(max: usize, length: usize, sender: Sender) -> Self {
64 | let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
65 | // TODO: consolidate this calculation with the set_max function below
66 | Self {
67 | max: Arc::new(Mutex::new(max)),
68 | progress_instances: Arc::new(arr),
69 | start: Arc::new(Mutex::new(Instant::now())),
70 | sender,
71 |
72 | last_update_time: Arc::new(AtomicInstant::now()),
73 | bytes_last_update: Arc::new(AtomicUsize::new(0)),
74 | rolling: RollingProgressWindow::new(),
75 | }
76 | }
77 |
78 | pub fn set_time_now(&self) {
79 | *self.start.lock().unwrap() = Instant::now();
80 | }
81 | pub fn sum(&self) -> usize {
82 | self.progress_instances
83 | .lock()
84 | .unwrap()
85 | .iter()
86 | .map(|instance| instance.load(Ordering::Relaxed))
87 | .sum()
88 | }
89 | pub fn get_max(&self) -> usize {
90 | *self.max.lock().unwrap()
91 | }
92 | pub fn set_max(&self, new_max: usize) {
93 | *self.max.lock().unwrap() = new_max;
94 | }
95 | pub fn set_size(&self, length: usize) {
96 | *self.progress_instances.lock().unwrap() =
97 | (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
98 | }
99 | pub fn get_progress(&self) -> f64 {
100 | self.sum() as f64 / self.get_max() as f64
101 | }
102 | pub fn get(&self, index: usize) -> Arc {
103 | self.progress_instances.lock().unwrap()[index].clone()
104 | }
105 | fn update_window(&self, kilobytes_per_second: usize) {
106 | self.rolling.update(kilobytes_per_second);
107 | }
108 | }
109 |
110 | #[throttle(1, Duration::from_millis(20))]
111 | pub fn calculate_update(progress: &ProgressObject) {
112 | let last_update_time = progress
113 | .last_update_time
114 | .swap(Instant::now(), Ordering::SeqCst);
115 | let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis();
116 |
117 | let current_bytes_downloaded = progress.sum();
118 | let max = progress.get_max();
119 | let bytes_at_last_update = progress
120 | .bytes_last_update
121 | .swap(current_bytes_downloaded, Ordering::Relaxed);
122 |
123 | let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
124 |
125 | let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
126 |
127 | let bytes_remaining = max - current_bytes_downloaded; // bytes
128 |
129 | progress.update_window(kilobytes_per_second);
130 | push_update(progress, bytes_remaining);
131 | }
132 |
133 | #[throttle(1, Duration::from_millis(500))]
134 | pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
135 | let average_speed = progress.rolling.get_average();
136 | let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
137 |
138 | update_ui(progress, average_speed, time_remaining);
139 | update_queue(progress);
140 | }
141 |
142 | fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
143 | progress_object
144 | .sender
145 | .send(DownloadManagerSignal::UpdateUIStats(
146 | kilobytes_per_second,
147 | time_remaining,
148 | ))
149 | .unwrap();
150 | }
151 |
152 | fn update_queue(progress: &ProgressObject) {
153 | progress
154 | .sender
155 | .send(DownloadManagerSignal::UpdateUIQueue)
156 | .unwrap();
157 | }
158 |
--------------------------------------------------------------------------------
/src-tauri/src/download_manager/util/queue.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::VecDeque,
3 | sync::{Arc, Mutex, MutexGuard},
4 | };
5 |
6 | use crate::database::models::data::DownloadableMetadata;
7 |
8 | #[derive(Clone)]
9 | pub struct Queue {
10 | inner: Arc>>,
11 | }
12 |
13 | #[allow(dead_code)]
14 | impl Default for Queue {
15 | fn default() -> Self {
16 | Self::new()
17 | }
18 | }
19 |
20 | impl Queue {
21 | pub fn new() -> Self {
22 | Self {
23 | inner: Arc::new(Mutex::new(VecDeque::new())),
24 | }
25 | }
26 | pub fn read(&self) -> VecDeque {
27 | self.inner.lock().unwrap().clone()
28 | }
29 | pub fn edit(&self) -> MutexGuard<'_, VecDeque> {
30 | self.inner.lock().unwrap()
31 | }
32 | pub fn pop_front(&self) -> Option {
33 | self.edit().pop_front()
34 | }
35 | pub fn is_empty(&self) -> bool {
36 | self.inner.lock().unwrap().len() == 0
37 | }
38 | pub fn exists(&self, meta: DownloadableMetadata) -> bool {
39 | self.read().contains(&meta)
40 | }
41 | /// Either inserts `interface` at the specified index, or appends to
42 | /// the back of the deque if index is greater than the length of the deque
43 | pub fn insert(&self, interface: DownloadableMetadata, index: usize) {
44 | if self.read().len() > index {
45 | self.append(interface);
46 | } else {
47 | self.edit().insert(index, interface);
48 | }
49 | }
50 | pub fn append(&self, interface: DownloadableMetadata) {
51 | self.edit().push_back(interface);
52 | }
53 | pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option {
54 | let mut queue = self.edit();
55 | let front = queue.front()?;
56 | if front == meta {
57 | return queue.pop_front();
58 | }
59 | None
60 | }
61 | pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option {
62 | self.read().iter().position(|data| data == meta)
63 | }
64 | pub fn move_to_index_by_meta(
65 | &self,
66 | meta: &DownloadableMetadata,
67 | new_index: usize,
68 | ) -> Result<(), ()> {
69 | let index = match self.get_by_meta(meta) {
70 | Some(index) => index,
71 | None => return Err(()),
72 | };
73 | let existing = match self.edit().remove(index) {
74 | Some(existing) => existing,
75 | None => return Err(()),
76 | };
77 | self.edit().insert(new_index, existing);
78 | Ok(())
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src-tauri/src/download_manager/util/rolling_progress_updates.rs:
--------------------------------------------------------------------------------
1 | use std::sync::{
2 | atomic::{AtomicUsize, Ordering},
3 | Arc,
4 | };
5 |
6 | #[derive(Clone)]
7 | pub struct RollingProgressWindow {
8 | window: Arc<[AtomicUsize; S]>,
9 | current: Arc,
10 | }
11 | impl RollingProgressWindow {
12 | pub fn new() -> Self {
13 | Self {
14 | window: Arc::new([(); S].map(|_| AtomicUsize::new(0))),
15 | current: Arc::new(AtomicUsize::new(0)),
16 | }
17 | }
18 | pub fn update(&self, kilobytes_per_second: usize) {
19 | let index = self.current.fetch_add(1, Ordering::SeqCst);
20 | let current = &self.window[index % S];
21 | current.store(kilobytes_per_second, Ordering::SeqCst);
22 | }
23 | pub fn get_average(&self) -> usize {
24 | let current = self.current.load(Ordering::SeqCst);
25 | self.window
26 | .iter()
27 | .enumerate()
28 | .filter(|(i, _)| i < ¤t)
29 | .map(|(_, x)| x.load(Ordering::Relaxed))
30 | .sum::()
31 | / S
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src-tauri/src/error/application_download_error.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt::{Display, Formatter},
3 | io,
4 | };
5 |
6 | use serde_with::SerializeDisplay;
7 |
8 | use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
9 |
10 | // TODO: Rename / separate from downloads
11 | #[derive(Debug, SerializeDisplay)]
12 | pub enum ApplicationDownloadError {
13 | Communication(RemoteAccessError),
14 | Checksum,
15 | Setup(SetupError),
16 | Lock,
17 | IoError(io::ErrorKind),
18 | DownloadError,
19 | }
20 |
21 | impl Display for ApplicationDownloadError {
22 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
23 | match self {
24 | ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
25 | ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", error),
26 | ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
27 | ApplicationDownloadError::Checksum => write!(f, "checksum failed to validate for download"),
28 | ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
29 | ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src-tauri/src/error/backup_error.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | use serde_with::SerializeDisplay;
4 |
5 | #[derive(Debug, SerializeDisplay, Clone, Copy)]
6 | pub enum BackupError {
7 | InvalidSystem,
8 | NotFound,
9 | ParseError
10 | }
11 |
12 | impl Display for BackupError {
13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14 | let s = match self {
15 | BackupError::InvalidSystem => "Attempted to generate path for invalid system",
16 | BackupError::NotFound => "Could not generate or find path",
17 | BackupError::ParseError => "Failed to parse path",
18 | };
19 | write!(f, "{}", s)
20 | }
21 | }
--------------------------------------------------------------------------------
/src-tauri/src/error/download_manager_error.rs:
--------------------------------------------------------------------------------
1 | use std::{fmt::Display, io, sync::mpsc::SendError};
2 |
3 | use serde_with::SerializeDisplay;
4 |
5 | #[derive(SerializeDisplay)]
6 | pub enum DownloadManagerError {
7 | IOError(io::Error),
8 | SignalError(SendError),
9 | }
10 | impl Display for DownloadManagerError {
11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12 | match self {
13 | DownloadManagerError::IOError(error) => write!(f, "{}", error),
14 | DownloadManagerError::SignalError(send_error) => write!(f, "{}", send_error),
15 | }
16 | }
17 | }
18 | impl From> for DownloadManagerError {
19 | fn from(value: SendError) -> Self {
20 | DownloadManagerError::SignalError(value)
21 | }
22 | }
23 | impl From for DownloadManagerError {
24 | fn from(value: io::Error) -> Self {
25 | DownloadManagerError::IOError(value)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src-tauri/src/error/drop_server_error.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Deserialize, Debug, Clone)]
4 | #[serde(rename_all = "camelCase")]
5 | pub struct DropServerError {
6 | pub status_code: usize,
7 | pub status_message: String,
8 | pub message: String,
9 | pub url: String,
10 | }
11 |
--------------------------------------------------------------------------------
/src-tauri/src/error/library_error.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | use serde_with::SerializeDisplay;
4 |
5 | #[derive(SerializeDisplay)]
6 | pub enum LibraryError {
7 | MetaNotFound(String),
8 | }
9 | impl Display for LibraryError {
10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11 | match self {
12 | LibraryError::MetaNotFound(id) => write!(
13 | f,
14 | "Could not locate any installed version of game ID {} in the database",
15 | id
16 | ),
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src-tauri/src/error/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod application_download_error;
2 | pub mod drop_server_error;
3 | pub mod download_manager_error;
4 | pub mod library_error;
5 | pub mod process_error;
6 | pub mod remote_access_error;
7 | pub mod setup_error;
8 | pub mod backup_error;
--------------------------------------------------------------------------------
/src-tauri/src/error/process_error.rs:
--------------------------------------------------------------------------------
1 | use std::{fmt::Display, io::Error};
2 |
3 | use serde_with::SerializeDisplay;
4 |
5 | #[derive(SerializeDisplay)]
6 | pub enum ProcessError {
7 | SetupRequired,
8 | NotInstalled,
9 | AlreadyRunning,
10 | NotDownloaded,
11 | InvalidID,
12 | InvalidVersion,
13 | IOError(Error),
14 | FormatError(String), // String errors supremacy
15 | InvalidPlatform,
16 | }
17 |
18 | impl Display for ProcessError {
19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 | let s = match self {
21 | ProcessError::SetupRequired => "Game not set up",
22 | ProcessError::NotInstalled => "Game not installed",
23 | ProcessError::AlreadyRunning => "Game already running",
24 | ProcessError::NotDownloaded => "Game not downloaded",
25 | ProcessError::InvalidID => "Invalid Game ID",
26 | ProcessError::InvalidVersion => "Invalid Game version",
27 | ProcessError::IOError(error) => &error.to_string(),
28 | ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
29 | ProcessError::FormatError(e) => &format!("Failed to format template: {}", e),
30 | };
31 | write!(f, "{}", s)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src-tauri/src/error/remote_access_error.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | error::Error,
3 | fmt::{Display, Formatter},
4 | sync::Arc,
5 | };
6 |
7 | use http::StatusCode;
8 | use serde_with::SerializeDisplay;
9 | use url::ParseError;
10 |
11 | use super::drop_server_error::DropServerError;
12 |
13 | #[derive(Debug, SerializeDisplay)]
14 | pub enum RemoteAccessError {
15 | FetchError(Arc),
16 | ParsingError(ParseError),
17 | InvalidEndpoint,
18 | HandshakeFailed(String),
19 | GameNotFound(String),
20 | InvalidResponse(DropServerError),
21 | InvalidRedirect,
22 | ManifestDownloadFailed(StatusCode, String),
23 | OutOfSync,
24 | Cache(cacache::Error),
25 | }
26 |
27 | impl Display for RemoteAccessError {
28 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29 | match self {
30 | RemoteAccessError::FetchError(error) => {
31 | if error.is_connect() {
32 | return write!(f, "Failed to connect to Drop server. Check if you access Drop through a browser, and then try again.");
33 | }
34 |
35 | write!(
36 | f,
37 | "{}: {}",
38 | error,
39 | error
40 | .source()
41 | .map(|e| e.to_string())
42 | .or_else(|| Some("Unknown error".to_string()))
43 | .unwrap()
44 | )
45 | },
46 | RemoteAccessError::ParsingError(parse_error) => {
47 | write!(f, "{}", parse_error)
48 | }
49 | RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
50 | RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message),
51 | RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {}", id),
52 | RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message),
53 | RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"),
54 | RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
55 | f,
56 | "failed to download game manifest: {} {}",
57 | status, response
58 | ),
59 | RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
60 | RemoteAccessError::Cache(error) => write!(f, "Cache Error: {}", error),
61 | }
62 | }
63 | }
64 |
65 | impl From for RemoteAccessError {
66 | fn from(err: reqwest::Error) -> Self {
67 | RemoteAccessError::FetchError(Arc::new(err))
68 | }
69 | }
70 | impl From for RemoteAccessError {
71 | fn from(err: ParseError) -> Self {
72 | RemoteAccessError::ParsingError(err)
73 | }
74 | }
75 | impl std::error::Error for RemoteAccessError {}
76 |
--------------------------------------------------------------------------------
/src-tauri/src/error/setup_error.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::{Display, Formatter};
2 |
3 | #[derive(Debug, Clone)]
4 | pub enum SetupError {
5 | Context,
6 | }
7 |
8 | impl Display for SetupError {
9 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
10 | match self {
11 | SetupError::Context => write!(f, "failed to generate contexts for download"),
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src-tauri/src/games/collections/collection.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | use crate::games::library::Game;
4 |
5 | pub type Collections = Vec;
6 |
7 | #[derive(Serialize, Deserialize, Debug, Clone, Default)]
8 | #[serde(rename_all = "camelCase")]
9 | pub struct Collection {
10 | id: String,
11 | name: String,
12 | is_default: bool,
13 | user_id: String,
14 | entries: Vec,
15 | }
16 |
17 | #[derive(Serialize, Deserialize, Debug, Clone, Default)]
18 | #[serde(rename_all = "camelCase")]
19 | pub struct CollectionObject {
20 | collection_id: String,
21 | game_id: String,
22 | game: Game,
23 | }
24 |
--------------------------------------------------------------------------------
/src-tauri/src/games/collections/commands.rs:
--------------------------------------------------------------------------------
1 | use reqwest::blocking::Client;
2 | use serde_json::json;
3 | use url::Url;
4 |
5 | use crate::{
6 | database::db::DatabaseImpls,
7 | error::remote_access_error::RemoteAccessError,
8 | remote::{auth::generate_authorization_header, requests::make_request},
9 | DB,
10 | };
11 |
12 | use super::collection::{Collection, Collections};
13 |
14 | #[tauri::command]
15 | pub fn fetch_collections() -> Result {
16 | let client = Client::new();
17 | let response = make_request(&client, &["/api/v1/client/collection"], &[], |r| {
18 | r.header("Authorization", generate_authorization_header())
19 | })?
20 | .send()?;
21 |
22 | Ok(response.json()?)
23 | }
24 |
25 | #[tauri::command]
26 | pub fn fetch_collection(collection_id: String) -> Result {
27 | let client = Client::new();
28 | let response = make_request(
29 | &client,
30 | &["/api/v1/client/collection/", &collection_id],
31 | &[],
32 | |r| r.header("Authorization", generate_authorization_header()),
33 | )?
34 | .send()?;
35 |
36 | Ok(response.json()?)
37 | }
38 |
39 | #[tauri::command]
40 | pub fn create_collection(name: String) -> Result {
41 | let client = Client::new();
42 | let base_url = DB.fetch_base_url();
43 |
44 | let base_url = Url::parse(&format!("{}api/v1/client/collection/", base_url))?;
45 |
46 | let response = client
47 | .post(base_url)
48 | .header("Authorization", generate_authorization_header())
49 | .json(&json!({"name": name}))
50 | .send()?;
51 |
52 | Ok(response.json()?)
53 | }
54 |
55 | #[tauri::command]
56 | pub fn add_game_to_collection(
57 | collection_id: String,
58 | game_id: String,
59 | ) -> Result<(), RemoteAccessError> {
60 | let client = Client::new();
61 | let url = Url::parse(&format!(
62 | "{}api/v1/client/collection/{}/entry/",
63 | DB.fetch_base_url(),
64 | collection_id
65 | ))?;
66 |
67 | client
68 | .post(url)
69 | .header("Authorization", generate_authorization_header())
70 | .json(&json!({"id": game_id}))
71 | .send()?;
72 | Ok(())
73 | }
74 |
75 | #[tauri::command]
76 | pub fn delete_collection(collection_id: String) -> Result {
77 | let client = Client::new();
78 | let base_url = Url::parse(&format!(
79 | "{}api/v1/client/collection/{}",
80 | DB.fetch_base_url(),
81 | collection_id
82 | ))?;
83 |
84 | let response = client
85 | .delete(base_url)
86 | .header("Authorization", generate_authorization_header())
87 | .send()?;
88 |
89 | Ok(response.json()?)
90 | }
91 | #[tauri::command]
92 | pub fn delete_game_in_collection(
93 | collection_id: String,
94 | game_id: String,
95 | ) -> Result<(), RemoteAccessError> {
96 | let client = Client::new();
97 | let base_url = Url::parse(&format!(
98 | "{}api/v1/client/collection/{}/entry",
99 | DB.fetch_base_url(),
100 | collection_id
101 | ))?;
102 |
103 | client
104 | .delete(base_url)
105 | .header("Authorization", generate_authorization_header())
106 | .json(&json!({"id": game_id}))
107 | .send()?;
108 |
109 | Ok(())
110 | }
111 |
--------------------------------------------------------------------------------
/src-tauri/src/games/collections/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod collection;
2 | pub mod commands;
3 |
--------------------------------------------------------------------------------
/src-tauri/src/games/commands.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Mutex;
2 |
3 | use tauri::AppHandle;
4 |
5 | use crate::{
6 | database::models::data::GameVersion,
7 | error::{library_error::LibraryError, remote_access_error::RemoteAccessError},
8 | games::library::{
9 | fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
10 | uninstall_game_logic,
11 | },
12 | offline, AppState,
13 | };
14 |
15 | use super::{
16 | library::{
17 | fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
18 | Game,
19 | },
20 | state::{GameStatusManager, GameStatusWithTransient},
21 | };
22 |
23 | #[tauri::command]
24 | pub fn fetch_library(
25 | state: tauri::State<'_, Mutex>,
26 | ) -> Result, RemoteAccessError> {
27 | offline!(
28 | state,
29 | fetch_library_logic,
30 | fetch_library_logic_offline,
31 | state
32 | )
33 | }
34 |
35 | #[tauri::command]
36 | pub fn fetch_game(
37 | game_id: String,
38 | state: tauri::State<'_, Mutex>,
39 | ) -> Result {
40 | offline!(
41 | state,
42 | fetch_game_logic,
43 | fetch_game_logic_offline,
44 | game_id,
45 | state
46 | )
47 | }
48 |
49 | #[tauri::command]
50 | pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
51 | GameStatusManager::fetch_state(&id)
52 | }
53 |
54 | #[tauri::command]
55 | pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
56 | let meta = match get_current_meta(&game_id) {
57 | Some(data) => data,
58 | None => return Err(LibraryError::MetaNotFound(game_id)),
59 | };
60 | uninstall_game_logic(meta, &app_handle);
61 |
62 | Ok(())
63 | }
64 |
65 | #[tauri::command]
66 | pub fn fetch_game_verion_options(
67 | game_id: String,
68 | state: tauri::State<'_, Mutex>,
69 | ) -> Result, RemoteAccessError> {
70 | fetch_game_verion_options_logic(game_id, state)
71 | }
72 |
--------------------------------------------------------------------------------
/src-tauri/src/games/downloads/commands.rs:
--------------------------------------------------------------------------------
1 | use std::sync::{Arc, Mutex};
2 |
3 | use crate::{
4 | download_manager::{
5 | download_manager::DownloadManagerSignal, downloadable::Downloadable,
6 | }, error::download_manager_error::DownloadManagerError, AppState
7 | };
8 |
9 | use super::download_agent::GameDownloadAgent;
10 |
11 | #[tauri::command]
12 | pub fn download_game(
13 | game_id: String,
14 | game_version: String,
15 | install_dir: usize,
16 | state: tauri::State<'_, Mutex>,
17 | ) -> Result<(), DownloadManagerError> {
18 | let sender = state.lock().unwrap().download_manager.get_sender();
19 | let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
20 | game_id,
21 | game_version,
22 | install_dir,
23 | sender,
24 | )) as Box);
25 | Ok(state
26 | .lock()
27 | .unwrap()
28 | .download_manager
29 | .queue_download(game_download_agent)?)
30 | }
31 |
--------------------------------------------------------------------------------
/src-tauri/src/games/downloads/download_logic.rs:
--------------------------------------------------------------------------------
1 | use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
2 | use crate::download_manager::util::progress_object::ProgressHandle;
3 | use crate::error::application_download_error::ApplicationDownloadError;
4 | use crate::error::remote_access_error::RemoteAccessError;
5 | use crate::games::downloads::manifest::DropDownloadContext;
6 | use log::warn;
7 | use md5::{Context, Digest};
8 | use reqwest::blocking::{RequestBuilder, Response};
9 |
10 | use std::fs::{set_permissions, Permissions};
11 | use std::io::{ErrorKind, Read};
12 | #[cfg(unix)]
13 | use std::os::unix::fs::PermissionsExt;
14 | use std::{
15 | fs::{File, OpenOptions},
16 | io::{self, BufWriter, Seek, SeekFrom, Write},
17 | path::PathBuf,
18 | };
19 |
20 | pub struct DropWriter {
21 | hasher: Context,
22 | destination: W,
23 | }
24 | impl DropWriter {
25 | fn new(path: PathBuf) -> Self {
26 | Self {
27 | destination: OpenOptions::new().write(true).open(path).unwrap(),
28 | hasher: Context::new(),
29 | }
30 | }
31 |
32 | fn finish(mut self) -> io::Result {
33 | self.flush().unwrap();
34 | Ok(self.hasher.compute())
35 | }
36 | }
37 | // Write automatically pushes to file and hasher
38 | impl Write for DropWriter {
39 | fn write(&mut self, buf: &[u8]) -> io::Result {
40 | self.hasher.write_all(buf).map_err(|e| {
41 | io::Error::new(
42 | ErrorKind::Other,
43 | format!("Unable to write to hasher: {}", e),
44 | )
45 | })?;
46 | self.destination.write(buf)
47 | }
48 |
49 | fn flush(&mut self) -> io::Result<()> {
50 | self.hasher.flush()?;
51 | self.destination.flush()
52 | }
53 | }
54 | // Seek moves around destination output
55 | impl Seek for DropWriter {
56 | fn seek(&mut self, pos: SeekFrom) -> io::Result {
57 | self.destination.seek(pos)
58 | }
59 | }
60 |
61 | pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
62 | pub source: R,
63 | pub destination: DropWriter,
64 | pub control_flag: &'a DownloadThreadControl,
65 | pub progress: ProgressHandle,
66 | pub size: usize,
67 | }
68 | impl<'a> DropDownloadPipeline<'a, Response, File> {
69 | fn new(
70 | source: Response,
71 | destination: DropWriter,
72 | control_flag: &'a DownloadThreadControl,
73 | progress: ProgressHandle,
74 | size: usize,
75 | ) -> Self {
76 | Self {
77 | source,
78 | destination,
79 | control_flag,
80 | progress,
81 | size,
82 | }
83 | }
84 |
85 | fn copy(&mut self) -> Result {
86 | let copy_buf_size = 512;
87 | let mut copy_buf = vec![0; copy_buf_size];
88 | let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
89 |
90 | let mut current_size = 0;
91 | loop {
92 | if self.control_flag.get() == DownloadThreadControlFlag::Stop {
93 | return Ok(false);
94 | }
95 |
96 | let bytes_read = self.source.read(&mut copy_buf)?;
97 | current_size += bytes_read;
98 |
99 | buf_writer.write_all(©_buf[0..bytes_read])?;
100 | self.progress.add(bytes_read);
101 |
102 | if current_size == self.size {
103 | break;
104 | }
105 | }
106 |
107 | Ok(true)
108 | }
109 |
110 | fn finish(self) -> Result {
111 | let checksum = self.destination.finish()?;
112 | Ok(checksum)
113 | }
114 | }
115 |
116 | pub fn download_game_chunk(
117 | ctx: &DropDownloadContext,
118 | control_flag: &DownloadThreadControl,
119 | progress: ProgressHandle,
120 | request: RequestBuilder,
121 | ) -> Result {
122 | // If we're paused
123 | if control_flag.get() == DownloadThreadControlFlag::Stop {
124 | progress.set(0);
125 | return Ok(false);
126 | }
127 |
128 | let response = request
129 | .send()
130 | .map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
131 |
132 | if response.status() != 200 {
133 | let err = response.json().unwrap();
134 | return Err(ApplicationDownloadError::Communication(
135 | RemoteAccessError::InvalidResponse(err),
136 | ));
137 | }
138 |
139 | let mut destination = DropWriter::new(ctx.path.clone());
140 |
141 | if ctx.offset != 0 {
142 | destination
143 | .seek(SeekFrom::Start(ctx.offset))
144 | .expect("Failed to seek to file offset");
145 | }
146 |
147 | let content_length = response.content_length();
148 | if content_length.is_none() {
149 | warn!("recieved 0 length content from server");
150 | return Err(ApplicationDownloadError::Communication(
151 | RemoteAccessError::InvalidResponse(response.json().unwrap()),
152 | ));
153 | }
154 |
155 | let mut pipeline = DropDownloadPipeline::new(
156 | response,
157 | destination,
158 | control_flag,
159 | progress,
160 | content_length.unwrap().try_into().unwrap(),
161 | );
162 |
163 | let completed = pipeline
164 | .copy()
165 | .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
166 | if !completed {
167 | return Ok(false);
168 | };
169 |
170 | // If we complete the file, set the permissions (if on Linux)
171 | #[cfg(unix)]
172 | {
173 | let permissions = Permissions::from_mode(ctx.permissions);
174 | set_permissions(ctx.path.clone(), permissions).unwrap();
175 | }
176 |
177 | let checksum = pipeline
178 | .finish()
179 | .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
180 |
181 | let res = hex::encode(checksum.0);
182 | if res != ctx.checksum {
183 | return Err(ApplicationDownloadError::Checksum);
184 | }
185 |
186 | Ok(true)
187 | }
188 |
--------------------------------------------------------------------------------
/src-tauri/src/games/downloads/manifest.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use std::collections::HashMap;
3 | use std::path::PathBuf;
4 |
5 | pub type DropManifest = HashMap;
6 | #[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
7 | #[serde(rename_all = "camelCase")]
8 | pub struct DropChunk {
9 | pub permissions: u32,
10 | pub ids: Vec,
11 | pub checksums: Vec,
12 | pub lengths: Vec,
13 | pub version_name: String,
14 | }
15 |
16 | #[derive(Serialize, Deserialize, Debug, Clone)]
17 | pub struct DropDownloadContext {
18 | pub file_name: String,
19 | pub version: String,
20 | pub index: usize,
21 | pub offset: u64,
22 | pub game_id: String,
23 | pub path: PathBuf,
24 | pub checksum: String,
25 | pub length: usize,
26 | pub permissions: u32,
27 | }
28 |
--------------------------------------------------------------------------------
/src-tauri/src/games/downloads/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod commands;
2 | pub mod download_agent;
3 | mod download_logic;
4 | mod manifest;
5 | mod stored_manifest;
6 |
--------------------------------------------------------------------------------
/src-tauri/src/games/downloads/stored_manifest.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::File,
3 | io::{Read, Write},
4 | path::PathBuf,
5 | sync::Mutex,
6 | };
7 |
8 | use log::{error, warn};
9 | use serde::{Deserialize, Serialize};
10 | use serde_binary::binary_stream::Endian;
11 |
12 | #[derive(Serialize, Deserialize, Debug)]
13 | pub struct StoredManifest {
14 | game_id: String,
15 | game_version: String,
16 | pub completed_contexts: Mutex>,
17 | pub base_path: PathBuf,
18 | }
19 |
20 | static DROP_DATA_PATH: &str = ".dropdata";
21 |
22 | impl StoredManifest {
23 | pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
24 | Self {
25 | base_path,
26 | game_id,
27 | game_version,
28 | completed_contexts: Mutex::new(Vec::new()),
29 | }
30 | }
31 | pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
32 | let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
33 | Ok(file) => file,
34 | Err(_) => return StoredManifest::new(game_id, game_version, base_path),
35 | };
36 |
37 | let mut s = Vec::new();
38 | match file.read_to_end(&mut s) {
39 | Ok(_) => {}
40 | Err(e) => {
41 | error!("{}", e);
42 | return StoredManifest::new(game_id, game_version, base_path);
43 | }
44 | };
45 |
46 | match serde_binary::from_vec::(s, Endian::Little) {
47 | Ok(manifest) => manifest,
48 | Err(e) => {
49 | warn!("{}", e);
50 | StoredManifest::new(game_id, game_version, base_path)
51 | }
52 | }
53 | }
54 | pub fn write(&self) {
55 | let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
56 | Ok(json) => json,
57 | Err(_) => return,
58 | };
59 |
60 | let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
61 | Ok(file) => file,
62 | Err(e) => {
63 | error!("{}", e);
64 | return;
65 | }
66 | };
67 |
68 | match file.write_all(&manifest_raw) {
69 | Ok(_) => {}
70 | Err(e) => error!("{}", e),
71 | };
72 | }
73 | pub fn set_completed_contexts(&self, completed_contexts: &[usize]) {
74 | *self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
75 | }
76 | pub fn get_completed_contexts(&self) -> Vec {
77 | self.completed_contexts.lock().unwrap().clone()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src-tauri/src/games/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod collections;
2 | pub mod commands;
3 | pub mod downloads;
4 | pub mod library;
5 | pub mod state;
6 |
--------------------------------------------------------------------------------
/src-tauri/src/games/state.rs:
--------------------------------------------------------------------------------
1 | use crate::database::{
2 | db::borrow_db_checked,
3 | models::data::{ApplicationTransientStatus, GameDownloadStatus},
4 | };
5 |
6 | pub type GameStatusWithTransient = (
7 | Option,
8 | Option,
9 | );
10 | pub struct GameStatusManager {}
11 |
12 | impl GameStatusManager {
13 | pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
14 | let db_lock = borrow_db_checked();
15 | let online_state = match db_lock.applications.installed_game_version.get(game_id) {
16 | Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
17 | None => None,
18 | };
19 | let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
20 | drop(db_lock);
21 |
22 | if online_state.is_some() {
23 | return (None, online_state);
24 | }
25 |
26 | if offline_state.is_some() {
27 | return (offline_state, None);
28 | }
29 |
30 | (None, None)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | drop_app_lib::run()
6 | }
7 |
--------------------------------------------------------------------------------
/src-tauri/src/process/commands.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Mutex;
2 |
3 | use crate::{error::process_error::ProcessError, AppState};
4 |
5 | #[tauri::command]
6 | pub fn launch_game(
7 | id: String,
8 | state: tauri::State<'_, Mutex>,
9 | ) -> Result<(), ProcessError> {
10 | let state_lock = state.lock().unwrap();
11 | let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
12 |
13 | //let meta = DownloadableMetadata {
14 | // id,
15 | // version: Some(version),
16 | // download_type: DownloadType::Game,
17 | //};
18 |
19 | match process_manager_lock.launch_process(id) {
20 | Ok(_) => {}
21 | Err(e) => return Err(e),
22 | };
23 |
24 | drop(process_manager_lock);
25 | drop(state_lock);
26 |
27 | Ok(())
28 | }
29 |
30 | #[tauri::command]
31 | pub fn kill_game(
32 | game_id: String,
33 | state: tauri::State<'_, Mutex>,
34 | ) -> Result<(), ProcessError> {
35 | let state_lock = state.lock().unwrap();
36 | let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
37 | process_manager_lock
38 | .kill_game(game_id)
39 | .map_err(ProcessError::IOError)
40 | }
41 |
--------------------------------------------------------------------------------
/src-tauri/src/process/compat.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src-tauri/src/process/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod commands;
2 | #[cfg(target_os = "linux")]
3 | pub mod compat;
4 | pub mod process_manager;
5 |
--------------------------------------------------------------------------------
/src-tauri/src/remote/cache.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | database::{db::borrow_db_checked, models::data::Database},
3 | error::remote_access_error::RemoteAccessError,
4 | };
5 | use cacache::Integrity;
6 | use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
7 | use serde::{de::DeserializeOwned, Deserialize, Serialize};
8 | use serde_binary::binary_stream::Endian;
9 |
10 | #[macro_export]
11 | macro_rules! offline {
12 | ($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
13 |
14 | if crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == crate::AppStatus::Offline {
15 | $func2( $( $arg ), *)
16 | } else {
17 | $func1( $( $arg ), *)
18 | }
19 | }
20 | }
21 |
22 | pub fn cache_object<'a, K: AsRef, D: Serialize + DeserializeOwned>(
23 | key: K,
24 | data: &D,
25 | ) -> Result {
26 | let bytes = serde_binary::to_vec(data, Endian::Little).unwrap();
27 | cacache::write_sync(&borrow_db_checked().cache_dir, key, bytes)
28 | .map_err(|e| RemoteAccessError::Cache(e))
29 | }
30 | pub fn get_cached_object<'a, K: AsRef, D: Serialize + DeserializeOwned>(
31 | key: K,
32 | ) -> Result {
33 | get_cached_object_db::(key, &borrow_db_checked())
34 | }
35 | pub fn get_cached_object_db<'a, K: AsRef, D: Serialize + DeserializeOwned>(
36 | key: K,
37 | db: &Database,
38 | ) -> Result {
39 | let bytes = cacache::read_sync(&db.cache_dir, key).map_err(|e| RemoteAccessError::Cache(e))?;
40 | let data = serde_binary::from_slice::(&bytes, Endian::Little).unwrap();
41 | Ok(data)
42 | }
43 | #[derive(Serialize, Deserialize)]
44 | pub struct ObjectCache {
45 | content_type: String,
46 | body: Vec,
47 | }
48 |
49 | impl From>> for ObjectCache {
50 | fn from(value: Response>) -> Self {
51 | ObjectCache {
52 | content_type: value
53 | .headers()
54 | .get(CONTENT_TYPE)
55 | .unwrap()
56 | .to_str()
57 | .unwrap()
58 | .to_owned(),
59 | body: value.body().clone(),
60 | }
61 | }
62 | }
63 | impl From for Response> {
64 | fn from(value: ObjectCache) -> Self {
65 | let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type);
66 | resp_builder.body(value.body).unwrap()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src-tauri/src/remote/commands.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Mutex;
2 |
3 | use log::debug;
4 | use reqwest::blocking::Client;
5 | use tauri::{AppHandle, Emitter, Manager};
6 | use url::Url;
7 |
8 | use crate::{
9 | database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
10 | error::remote_access_error::RemoteAccessError,
11 | remote::{auth::generate_authorization_header, requests::make_request},
12 | AppState, AppStatus,
13 | };
14 |
15 | use super::{
16 | auth::{auth_initiate_logic, recieve_handshake, setup},
17 | cache::{cache_object, get_cached_object},
18 | remote::use_remote_logic,
19 | };
20 |
21 | #[tauri::command]
22 | pub fn use_remote(
23 | url: String,
24 | state: tauri::State<'_, Mutex>>,
25 | ) -> Result<(), RemoteAccessError> {
26 | use_remote_logic(url, state)
27 | }
28 |
29 | #[tauri::command]
30 | pub fn gen_drop_url(path: String) -> Result {
31 | let base_url = {
32 | let handle = borrow_db_checked();
33 |
34 | Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
35 | };
36 |
37 | let url = base_url.join(&path).unwrap();
38 |
39 | Ok(url.to_string())
40 | }
41 |
42 | #[tauri::command]
43 | pub fn fetch_drop_object(path: String) -> Result, RemoteAccessError> {
44 | let _drop_url = gen_drop_url(path.clone())?;
45 | let req = make_request(&Client::new(), &[&path], &[], |r| {
46 | r.header("Authorization", generate_authorization_header())
47 | })?
48 | .send();
49 |
50 | match req {
51 | Ok(data) => {
52 | let data = data.bytes()?.to_vec();
53 | cache_object(&path, &data)?;
54 | Ok(data)
55 | }
56 | Err(e) => {
57 | debug!("{}", e);
58 | get_cached_object::<&str, Vec>(&path)
59 | }
60 | }
61 | }
62 | #[tauri::command]
63 | pub fn sign_out(app: AppHandle) {
64 | // Clear auth from database
65 | {
66 | let mut handle = borrow_db_mut_checked();
67 | handle.auth = None;
68 | drop(handle);
69 | save_db();
70 | }
71 |
72 | // Update app state
73 | {
74 | let app_state = app.state::>();
75 | let mut app_state_handle = app_state.lock().unwrap();
76 | app_state_handle.status = AppStatus::SignedOut;
77 | app_state_handle.user = None;
78 | }
79 |
80 | // Emit event for frontend
81 | app.emit("auth/signedout", ()).unwrap();
82 | }
83 |
84 | #[tauri::command]
85 | pub fn retry_connect(state: tauri::State<'_, Mutex>) {
86 | let (app_status, user) = setup();
87 |
88 | let mut guard = state.lock().unwrap();
89 | guard.status = app_status;
90 | guard.user = user;
91 | drop(guard);
92 | }
93 |
94 | #[tauri::command]
95 | pub fn auth_initiate() -> Result<(), RemoteAccessError> {
96 | auth_initiate_logic()
97 | }
98 |
99 | #[tauri::command]
100 | pub fn manual_recieve_handshake(app: AppHandle, token: String) {
101 | recieve_handshake(app, format!("handshake/{}", token));
102 | }
103 |
--------------------------------------------------------------------------------
/src-tauri/src/remote/fetch_object.rs:
--------------------------------------------------------------------------------
1 | use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
2 | use log::warn;
3 | use tauri::UriSchemeResponder;
4 |
5 | use super::{
6 | auth::generate_authorization_header,
7 | cache::{cache_object, get_cached_object, ObjectCache},
8 | requests::make_request,
9 | };
10 |
11 | pub fn fetch_object(request: http::Request>, responder: UriSchemeResponder) {
12 | // Drop leading /
13 | let object_id = &request.uri().path()[1..];
14 |
15 | let header = generate_authorization_header();
16 | let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
17 | let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
18 | f.header("Authorization", header)
19 | })
20 | .unwrap()
21 | .send();
22 | if response.is_err() {
23 | let data = get_cached_object::<&str, ObjectCache>(object_id);
24 |
25 | match data {
26 | Ok(data) => responder.respond(data.into()),
27 | Err(e) => {
28 | warn!("{}", e)
29 | }
30 | }
31 | return;
32 | }
33 | let response = response.unwrap();
34 |
35 | let resp_builder = ResponseBuilder::new().header(
36 | CONTENT_TYPE,
37 | response.headers().get("Content-Type").unwrap(),
38 | );
39 | let data = Vec::from(response.bytes().unwrap());
40 | let resp = resp_builder.body(data).unwrap();
41 | cache_object::<&str, ObjectCache>(object_id, &resp.clone().into()).unwrap();
42 |
43 | responder.respond(resp);
44 | }
45 | pub fn fetch_object_offline(request: http::Request>, responder: UriSchemeResponder) {
46 | let object_id = &request.uri().path()[1..];
47 | let data = get_cached_object::<&str, ObjectCache>(object_id);
48 |
49 | match data {
50 | Ok(data) => responder.respond(data.into()),
51 | Err(e) => warn!("{}", e),
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src-tauri/src/remote/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod auth;
2 | #[macro_use]
3 | pub mod cache;
4 | pub mod commands;
5 | pub mod fetch_object;
6 | pub mod remote;
7 | pub mod requests;
8 | pub mod server_proto;
9 |
--------------------------------------------------------------------------------
/src-tauri/src/remote/remote.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Mutex;
2 |
3 | use log::{debug, warn};
4 | use serde::Deserialize;
5 | use url::Url;
6 |
7 | use crate::{
8 | database::db::{borrow_db_mut_checked, save_db},
9 | error::remote_access_error::RemoteAccessError,
10 | AppState, AppStatus,
11 | };
12 |
13 | #[derive(Deserialize)]
14 | #[serde(rename_all = "camelCase")]
15 | struct DropHealthcheck {
16 | app_name: String,
17 | }
18 |
19 | pub fn use_remote_logic(
20 | url: String,
21 | state: tauri::State<'_, Mutex>>,
22 | ) -> Result<(), RemoteAccessError> {
23 | debug!("connecting to url {}", url);
24 | let base_url = Url::parse(&url)?;
25 |
26 | // Test Drop url
27 | let test_endpoint = base_url.join("/api/v1")?;
28 | let response = reqwest::blocking::get(test_endpoint.to_string())?;
29 |
30 | let result: DropHealthcheck = response.json()?;
31 |
32 | if result.app_name != "Drop" {
33 | warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
34 | return Err(RemoteAccessError::InvalidEndpoint);
35 | }
36 |
37 | let mut app_state = state.lock().unwrap();
38 | app_state.status = AppStatus::SignedOut;
39 | drop(app_state);
40 |
41 | let mut db_state = borrow_db_mut_checked();
42 | db_state.base_url = base_url.to_string();
43 | drop(db_state);
44 |
45 | save_db();
46 |
47 | Ok(())
48 | }
49 |
--------------------------------------------------------------------------------
/src-tauri/src/remote/requests.rs:
--------------------------------------------------------------------------------
1 | use reqwest::blocking::{Client, RequestBuilder};
2 |
3 | use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
4 |
5 | pub fn make_request, F: FnOnce(RequestBuilder) -> RequestBuilder>(
6 | client: &Client,
7 | path_components: &[T],
8 | query: &[(T, T)],
9 | f: F,
10 | ) -> Result {
11 | let mut base_url = DB.fetch_base_url();
12 | for endpoint in path_components {
13 | base_url = base_url.join(endpoint.as_ref())?;
14 | }
15 | {
16 | let mut queries = base_url.query_pairs_mut();
17 | for (param, val) in query {
18 | queries.append_pair(param.as_ref(), val.as_ref());
19 | }
20 | }
21 | let response = client.get(base_url);
22 | Ok(f(response))
23 | }
24 |
--------------------------------------------------------------------------------
/src-tauri/src/remote/server_proto.rs:
--------------------------------------------------------------------------------
1 | use std::str::FromStr;
2 |
3 | use http::{
4 | uri::PathAndQuery,
5 | Request, Response, StatusCode, Uri,
6 | };
7 | use reqwest::blocking::Client;
8 | use tauri::UriSchemeResponder;
9 |
10 | use crate::database::db::borrow_db_checked;
11 |
12 | pub fn handle_server_proto_offline(_request: Request>, responder: UriSchemeResponder) {
13 | let four_oh_four = Response::builder()
14 | .status(StatusCode::NOT_FOUND)
15 | .body(Vec::new())
16 | .unwrap();
17 | responder.respond(four_oh_four);
18 | }
19 |
20 | pub fn handle_server_proto(request: Request>, responder: UriSchemeResponder) {
21 | let db_handle = borrow_db_checked();
22 | let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
23 | Some(e) => e,
24 | None => return,
25 | };
26 | let remote_uri = db_handle.base_url.parse::().unwrap();
27 |
28 | let path = request.uri().path();
29 |
30 | let mut new_uri = request.uri().clone().into_parts();
31 | new_uri.path_and_query =
32 | Some(PathAndQuery::from_str(&format!("{}?noWrapper=true", path)).unwrap());
33 | new_uri.authority = remote_uri.authority().cloned();
34 | new_uri.scheme = remote_uri.scheme().cloned();
35 | let new_uri = Uri::from_parts(new_uri).unwrap();
36 |
37 | let whitelist_prefix = vec!["/store", "/api", "/_", "/fonts"];
38 |
39 | if whitelist_prefix
40 | .iter()
41 | .map(|f| !path.starts_with(f))
42 | .all(|f| f)
43 | {
44 | webbrowser::open(&new_uri.to_string()).unwrap();
45 | return;
46 | }
47 |
48 | let client = Client::new();
49 | let response = client
50 | .request(request.method().clone(), new_uri.to_string())
51 | .header("Authorization", format!("Bearer {}", web_token))
52 | .headers(request.headers().clone())
53 | .send()
54 | .unwrap();
55 |
56 | let response_status = response.status();
57 | let response_body = response.bytes().unwrap();
58 |
59 | let http_response = Response::builder()
60 | .status(response_status)
61 | .body(response_body.to_vec())
62 | .unwrap();
63 |
64 | responder.respond(http_response);
65 | }
66 |
--------------------------------------------------------------------------------
/src-tauri/tailscale/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/src-tauri/tailscale/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tailscale"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [build-dependencies]
7 | bindgen = "*"
8 | abs-file-macro = "0.1.2"
9 |
10 | [dependencies]
11 | libc = "0.2.172"
12 |
--------------------------------------------------------------------------------
/src-tauri/tailscale/build.rs:
--------------------------------------------------------------------------------
1 | extern crate bindgen;
2 |
3 | use abs_file_macro::abs_file;
4 | use std::path::PathBuf;
5 | use std::process::Command;
6 |
7 | fn main() {
8 | let build_folder = PathBuf::from(abs_file!());
9 | let build_folder = build_folder.parent().unwrap();
10 |
11 | let in_path = build_folder.join("libtailscale");
12 | let out_path = build_folder.join("src/");
13 |
14 | let mut make_cmd = Command::new("make");
15 | make_cmd.arg("c-archive");
16 | make_cmd.current_dir(in_path.clone());
17 |
18 | make_cmd.status().expect("Make build failed");
19 |
20 | let bindings = bindgen::Builder::default()
21 | .header(in_path.join("libtailscale.h").to_str().unwrap())
22 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
23 | .generate()
24 | .expect("Unable to generate bindings");
25 |
26 | bindings
27 | .write_to_file(out_path.join("bindings.rs"))
28 | .expect("Couldn't write bindings!");
29 |
30 | println!("cargo:rerun-if-changed=libtailscale/tailscale.go");
31 | println!(
32 | "cargo:rustc-link-search=native={}",
33 | in_path.to_str().unwrap()
34 | );
35 | println!("cargo:rustc-link-lib=static={}", "tailscale");
36 | }
37 |
--------------------------------------------------------------------------------
/src-tauri/tailscale/src/test.rs:
--------------------------------------------------------------------------------
1 | use std::error::Error;
2 |
3 | use crate::{Tailscale, TailscaleError};
4 |
5 | #[test]
6 | fn start_listener() -> Result<(), TailscaleError> {
7 | println!("Creating server");
8 | // Create a new server
9 | let ts = Tailscale::new();
10 |
11 | // Configure it
12 | println!("Configuring directory");
13 | ts.set_dir("/tmp/tailscale-rust-test")?;
14 | println!("Configuring hostname");
15 | ts.set_hostname("my-rust-node")?;
16 | println!("Setting ephemeral");
17 | //ts.set_authkey("tskey-...")?; // Set authkey if needed for auto-registration
18 | ts.set_ephemeral(true)?;
19 |
20 | // Bring the server up
21 | println!("Starting Tailscale...");
22 | ts.up()?;
23 | println!("Tailscale started!");
24 |
25 | // Get IPs
26 | let mut ip_buf = [0u8; 256];
27 | let ips = ts.get_ips(&mut ip_buf)?;
28 | println!("Tailscale IPs: {}", ips);
29 | Ok(())
30 | }
31 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.tauri.app/config/2.0.0",
3 | "productName": "Drop Desktop Client",
4 | "version": "0.3.0-rc-2",
5 | "identifier": "dev.drop.app",
6 | "build": {
7 | "beforeDevCommand": "yarn dev --port 1432",
8 | "devUrl": "http://localhost:1432",
9 | "beforeBuildCommand": "yarn generate",
10 | "frontendDist": "../.output/public"
11 | },
12 | "app": {
13 | "security": {
14 | "csp": null
15 | }
16 | },
17 | "plugins": {
18 | "deep-link": {
19 | "desktop": {
20 | "schemes": ["drop"]
21 | }
22 | }
23 | },
24 | "bundle": {
25 | "active": true,
26 | "targets": ["nsis", "deb", "rpm", "dmg", "appimage"],
27 | "windows": {
28 | "nsis": {
29 | "installMode": "both"
30 | },
31 | "webviewInstallMode": {
32 | "silent": true,
33 | "type": "embedBootstrapper"
34 | },
35 | "wix": null
36 | },
37 | "icon": [
38 | "icons/32x32.png",
39 | "icons/128x128.png",
40 | "icons/128x128@2x.png",
41 | "icons/icon.icns",
42 | "icons/icon.ico"
43 | ],
44 | "externalBin": []
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./components/**/*.{js,vue,ts}",
5 | "./layouts/**/*.vue",
6 | "./pages/**/*.vue",
7 | "./plugins/**/*.{js,ts}",
8 | "./app.vue",
9 | "./error.vue",
10 | ],
11 | theme: {
12 | extend: {
13 | fontFamily: {
14 | sans: ["Inter"],
15 | display: ["Motiva Sans"],
16 | },
17 | },
18 | },
19 | plugins: [require("@tailwindcss/forms"), require('@tailwindcss/typography')],
20 | };
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://nuxt.com/docs/guide/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json"
4 | }
5 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import type { Component } from "vue";
2 |
3 | export type NavigationItem = {
4 | prefix: string;
5 | route: string;
6 | label: string;
7 | };
8 |
9 | export type QuickActionNav = {
10 | icon: Component;
11 | notifications?: number;
12 | action: () => Promise;
13 | };
14 |
15 | export type User = {
16 | id: string;
17 | username: string;
18 | admin: boolean;
19 | displayName: string;
20 | profilePictureObjectId: string;
21 | };
22 |
23 | export type AppState = {
24 | status: AppStatus;
25 | user?: User;
26 | };
27 |
28 | export type Game = {
29 | id: string;
30 | mName: string;
31 | mShortDescription: string;
32 | mDescription: string;
33 | mIconObjectId: string;
34 | mBannerObjectId: string;
35 | mCoverObjectId: string;
36 | mImageLibraryObjectIds: string[];
37 | mImageCarouselObjectIds: string[];
38 | };
39 |
40 | export type GameVersion = {
41 | launchCommandTemplate: string;
42 | };
43 |
44 | export enum AppStatus {
45 | NotConfigured = "NotConfigured",
46 | Offline = "Offline",
47 | SignedOut = "SignedOut",
48 | SignedIn = "SignedIn",
49 | SignedInNeedsReauth = "SignedInNeedsReauth",
50 | ServerUnavailable = "ServerUnavailable",
51 | }
52 |
53 | export enum GameStatusEnum {
54 | Remote = "Remote",
55 | Queued = "Queued",
56 | Downloading = "Downloading",
57 | Installed = "Installed",
58 | Updating = "Updating",
59 | Uninstalling = "Uninstalling",
60 | SetupRequired = "SetupRequired",
61 | Running = "Running",
62 | }
63 |
64 | export type GameStatus = {
65 | type: GameStatusEnum;
66 | version_name?: string;
67 | };
68 |
69 | export enum DownloadableType {
70 | Game = "Game",
71 | Tool = "Tool",
72 | DLC = "DLC",
73 | Mod = "Mod",
74 | }
75 |
76 | export type DownloadableMetadata = {
77 | id: string;
78 | version: string;
79 | downloadType: DownloadableType;
80 | };
81 |
82 | export type Settings = {
83 | autostart: boolean;
84 | maxDownloadThreads: number;
85 | forceOffline: boolean;
86 | };
87 |
--------------------------------------------------------------------------------