Compare commits

..

3 Commits

Author SHA1 Message Date
DecDuck
43b56462d6 feat: more refactoring (broken) 2025-09-16 15:09:43 +10:00
DecDuck
ab219670dc fix: cleanup dependencies 2025-09-14 09:27:49 +10:00
DecDuck
c1beef380e refactor: into rust workspaces 2025-09-14 09:19:03 +10:00
177 changed files with 14696 additions and 27247 deletions

View File

@@ -1,4 +1,4 @@
name: "publish" name: 'publish'
on: on:
workflow_dispatch: {} workflow_dispatch: {}
@@ -18,16 +18,16 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- platform: "macos-14" # for Arm based macs (M1 and above). - platform: 'macos-latest' # for Arm based macs (M1 and above).
args: "--target aarch64-apple-darwin" args: '--target aarch64-apple-darwin'
- platform: "macos-14" # for Intel based macs. - platform: 'macos-latest' # for Intel based macs.
args: "--target x86_64-apple-darwin" args: '--target x86_64-apple-darwin'
- platform: "ubuntu-22.04" # for Tauri v1 you could replace this with ubuntu-20.04. - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: "" args: ''
- platform: "ubuntu-22.04-arm" - platform: 'ubuntu-22.04-arm'
args: "--target aarch64-unknown-linux-gnu" args: '--target aarch64-unknown-linux-gnu'
- platform: "windows-latest" - platform: 'windows-latest'
args: "" args: ''
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
@@ -36,29 +36,16 @@ jobs:
submodules: true submodules: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: setup node - name: setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/* node-version: lts/*
cache: pnpm
- name: install Rust nightly - name: install Rust nightly
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@nightly
with: with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: install dependencies (ubuntu only) - name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above. if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
@@ -67,8 +54,9 @@ jobs:
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
- name: Import Apple Developer Certificate - name: Import Apple Developer Certificate
if: matrix.platform == 'macos-14' if: matrix.platform == 'macos-latest'
env: env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@@ -79,30 +67,18 @@ jobs:
security default-keychain -s build.keychain security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain security set-keychain-settings -t 3600 -u build.keychain
curl https://droposs.org/drop.crt --output drop.pem
echo "Created keychain" sudo security authorizationdb write com.apple.trust-settings.user allow
security add-trusted-cert -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
curl https://droposs.org/drop.der --output drop.der sudo security authorizationdb remove com.apple.trust-settings.user
# swiftc libs/appletrust/add-certificate.swift
# ./add-certificate drop.der
# rm add-certificate
# echo "Added certificate to keychain using swift util"
## Script is equivalent to:
sudo security authorizationdb write com.apple.trust-settings.admin allow
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.der
sudo security authorizationdb remove com.apple.trust-settings.admin
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
echo "Imported certificate"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain security find-identity -v -p codesigning build.keychain
- name: Verify Certificate - name: Verify Certificate
if: matrix.platform == 'macos-14' if: matrix.platform == 'macos-latest'
run: | run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS") CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}') CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
@@ -110,19 +86,19 @@ jobs:
echo "Certificate imported. Using identity: $CERT_ID" echo "Certificate imported. Using identity: $CERT_ID"
- name: install frontend dependencies - name: install frontend dependencies
run: pnpm install # change this to npm, pnpm or bun depending on which one you use. run: yarn install # change this to npm, pnpm or bun depending on which one you use.
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
NO_STRIP: true NO_STRIP: true
with: with:
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version. tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
releaseName: "Auto-release v__VERSION__" releaseName: 'Auto-release v__VERSION__'
releaseBody: "See the assets to download this version and install. This release was created automatically." releaseBody: 'See the assets to download this version and install. This release was created automatically.'
releaseDraft: false releaseDraft: false
prerelease: true prerelease: true
args: ${{ matrix.args }} args: ${{ matrix.args }}

4
.gitignore vendored
View File

@@ -29,6 +29,4 @@ src-tauri/flamegraph.svg
src-tauri/perf* src-tauri/perf*
/*.AppImage /*.AppImage
/squashfs-root /squashfs-root
/target/

2
.gitlab-ci-local/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -1,21 +1,29 @@
# Drop Desktop Client # Drop App
The Drop Desktop Client is the companion app for [Drop](https://github.com/Drop-OSS/drop). It is the official & intended way to download and play games on your Drop server. Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
## Internals ## Running
Before setting up the drop app, be sure that you have a server set up.
The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart)
It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI. ## Current features
Currently supported are the following features:
- Signin (with custom server)
- Database registering & recovery
- Dynamic library fetching from server
- Installing & uninstalling games
- Download progress monitoring
- Launching / playing games
## Development ## Development
Before setting up a development environemnt, be sure that you have a server set up. The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart).
Then, install dependencies with `yarn`. This'll install the custom builder's dependencies. Then, check everything works properly with `yarn tauri build`. Install dependencies with `yarn`
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh` Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`: To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
e.g. `RUST_LOG=debug yarn tauri dev` e.g. `RUST_LOG=debug yarn tauri dev`
## Contributing ## Contributing
Check out the contributing guide on our Developer Docs: [Drop Developer Docs - Contributing](https://developer.droposs.org/contributing). Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines.

View File

@@ -40,10 +40,10 @@ for (const view of views) {
process.chdir(`./${view}`); process.chdir(`./${view}`);
loggerChild.info(`Install deps for "${view}"`); loggerChild.info(`Install deps for "${view}"`);
await spawn("pnpm install"); await spawn("yarn");
loggerChild.info(`Building "${view}"`); loggerChild.info(`Building "${view}"`);
await spawn("pnpm run build", { await spawn("yarn build", {
env: { ...process.env, NUXT_APP_BASE_URL: `/${view}/` }, env: { ...process.env, NUXT_APP_BASE_URL: `/${view}/` },
}); });

View File

@@ -1,72 +0,0 @@
import Foundation
import Security
enum SecurityError: Error {
case generalError
}
func deleteCertificateFromKeyChain(_ certificateLabel: String) -> Bool {
let delQuery: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrLabel: certificateLabel,
]
let delStatus: OSStatus = SecItemDelete(delQuery as CFDictionary)
return delStatus == errSecSuccess
}
func saveCertificateToKeyChain(_ certificate: SecCertificate, certificateLabel: String) throws {
SecKeychainSetPreferenceDomain(SecPreferencesDomain.system)
deleteCertificateFromKeyChain(certificateLabel)
let setQuery: [NSString: AnyObject] = [
kSecClass: kSecClassCertificate,
kSecValueRef: certificate,
kSecAttrLabel: certificateLabel as AnyObject,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
kSecAttrCanSign: true as AnyObject,
]
let addStatus: OSStatus = SecItemAdd(setQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
throw SecurityError.generalError
}
var status = SecTrustSettingsSetTrustSettings(certificate, SecTrustSettingsDomain.admin, nil)
}
func getCertificateFromString(stringData: String) throws -> SecCertificate {
if let data = NSData(base64Encoded: stringData, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) {
if let certificate = SecCertificateCreateWithData(kCFAllocatorDefault, data) {
return certificate
}
}
throw SecurityError.generalError
}
if CommandLine.arguments.count != 2 {
print("Usage: \(CommandLine.arguments[0]) [cert.file]")
print("Usage: \(CommandLine.arguments[0]) --version")
exit(1)
}
if (CommandLine.arguments[1] == "--version") {
let version = "dev"
print(version)
exit(0)
} else {
let fileURL = URL(fileURLWithPath: CommandLine.arguments[1])
do {
let certData = try Data(contentsOf: fileURL)
let certificate = SecCertificateCreateWithData(nil, certData as CFData)
if certificate != nil {
try? saveCertificateToKeyChain(certificate!, certificateLabel: "DropOSS")
exit(0)
} else {
print("ERROR: Unknown error while reading the \(CommandLine.arguments[1]) file.")
}
} catch {
print("ERROR: Unexpected error while reading the \(CommandLine.arguments[1]) file. \(error)")
}
}
exit(1)

View File

@@ -1,5 +1,5 @@
<template> <template>
<NuxtLoadingIndicator color="#2563eb" /> <NuxtLoadingIndicator color="#2563eb" />
<NuxtLayout class="select-none w-screen h-screen"> <NuxtLayout class="select-none w-screen h-screen">
<NuxtPage /> <NuxtPage />
<ModalStack /> <ModalStack />
@@ -15,8 +15,6 @@ import {
initialNavigation, initialNavigation,
setupHooks, setupHooks,
} from "./composables/state-navigation.js"; } from "./composables/state-navigation.js";
import { listen } from "@tauri-apps/api/event";
import type { AppState } from "./types.js";
const router = useRouter(); const router = useRouter();
@@ -38,8 +36,9 @@ async function fetchState() {
} }
await fetchState(); await fetchState();
listen("update_state", (event) => { // This is inefficient but apparently we do it lol
state.value = event.payload as AppState; router.beforeEach(async () => {
await fetchState();
}); });
setupHooks(); setupHooks();

View File

@@ -6,7 +6,6 @@ html,
body { body {
-ms-overflow-style: none; /* IE and Edge / -ms-overflow-style: none; /* IE and Edge /
scrollbar-width: none; / Firefox */ scrollbar-width: none; / Firefox */
overscroll-behavior: none;
} }
/* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */

View File

@@ -1,106 +0,0 @@
<template>
<ModalTemplate :model-value="true">
<template #default
><div class="flex items-start gap-x-3">
<img :src="useObject(game.mIconObjectId)" class="size-12" />
<div class="mt-3 text-center sm:mt-0 sm:text-left">
<h3 class="text-base font-semibold text-zinc-100">
Missing required dependency "{{ game.mName }}"
</h3>
<div class="mt-2">
<p class="text-sm text-zinc-400">
To launch this game, you need to have "{{ game.mName }}" ({{
version.displayName ?? version.versionPath
}}) installed.
</p>
</div>
</div>
</div>
<InstallDirectorySelector
:install-dirs="installDirs"
v-model="installDir"
/>
<div v-if="installError" class="mt-1 rounded-md bg-red-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-600">
{{ installError }}
</h3>
</div>
</div>
</div>
</template>
<template #buttons>
<LoadingButton
@click="() => install()"
:loading="installLoading"
:disabled="installLoading"
type="submit"
class="ml-2 w-full sm:w-fit"
>
Install
</LoadingButton>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="cancel"
ref="cancelButtonRef"
>
Cancel
</button>
</template>
</ModalTemplate>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { XCircleIcon } from "@heroicons/vue/24/solid";
const model = defineModel<{ gameId: string; versionId: string }>({
required: true,
});
const { game, status } = await useGame(model.value.gameId);
const versionOptions = await invoke<Array<VersionOption>>(
"fetch_game_version_options",
{
gameId: game.id,
}
);
const version = versionOptions.find(
(v) => v.versionId === model.value.versionId
)!;
const installDirs = await invoke<string[]>("fetch_download_dir_stats");
const installDir = ref(0);
function cancel() {
// @ts-expect-error
model.value = undefined;
}
const installError = ref<string | undefined>();
const installLoading = ref(false);
async function install() {
try {
installLoading.value = true;
await invoke("download_game", {
gameId: game.id,
versionId: model.value.versionId,
installDir: installDir.value,
targetPlatform: version.platform,
});
cancel();
} catch (error) {
installError.value = (error as string).toString();
}
installLoading.value = false;
}
</script>

View File

@@ -1,87 +0,0 @@
<template>
<Listbox as="div" v-model="installDir">
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
>Install to</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
>
<span class="block truncate">{{ installDirs[installDir] }}</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="(dir, dirIdx) in installDirs"
:key="dir"
:value="dirIdx"
v-slot="{ active, selected }"
>
<li
:class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
>
<span
:class="[
selected ? 'font-semibold text-zinc-100' : 'font-normal',
'block truncate',
]"
>{{ dir }}</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
<div class="text-zinc-400 text-sm mt-2">
Add more install directories in
<PageWidget to="/settings/downloads">
<WrenchIcon class="size-3" />
Settings
</PageWidget>
</div>
</Listbox>
</template>
<script setup lang="ts">
import {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
} from "@headlessui/vue";
import {
CheckIcon,
ChevronUpDownIcon,
WrenchIcon,
} from "@heroicons/vue/20/solid";
const installDir = defineModel<number>({ required: true });
const { installDirs } = defineProps<{ installDirs: string[] }>();
</script>

View File

@@ -27,12 +27,12 @@
</button> </button>
</div> </div>
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5 h-full"> <TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
<Disclosure <Disclosure
as="div" as="div"
v-for="(nav, navIndex) in filteredNavigation" v-for="(nav, navIndex) in filteredNavigation"
:key="nav.id" :key="nav.id"
:class="['first:pt-0 last:pb-0', nav.tools ? 'mt-auto' : '']" class="first:pt-0 last:pb-0"
v-slot="{ open }" v-slot="{ open }"
:default-open="nav.deft" :default-open="nav.deft"
> >
@@ -43,12 +43,9 @@
<span class="text-sm font-semibold font-display">{{ <span class="text-sm font-semibold font-display">{{
nav.name nav.name
}}</span> }}</span>
<span class="ml-6 relative flex size-4"> <span class="ml-6 flex h-7 items-center">
<MinusIcon class="absolute inset-0 size-4" aria-hidden="true" /> <PlusSmallIcon v-if="!open" class="size-6" aria-hidden="true" />
<MinusIcon <MinusSmallIcon v-else class="size-6" aria-hidden="true" />
:class="[ !open ? 'rotate-90' : 'rotate-0', 'transition-all absolute inset-0 size-4']"
aria-hidden="true"
/>
</span> </span>
</DisclosureButton> </DisclosureButton>
</dt> </dt>
@@ -61,8 +58,8 @@
currentNavigation == item.id currentNavigation == item.id
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20' ? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: item.isInstalled.value : item.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200' ? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300', : 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]" ]"
:href="item.route" :href="item.route"
> >
@@ -72,12 +69,14 @@
> >
<img <img
class="size-6 object-cover bg-zinc-900 rounded transition-all duration-300 shadow-sm" class="size-6 object-cover bg-zinc-900 rounded transition-all duration-300 shadow-sm"
:src="useObject(item.icon)" :src="icons[item.id]"
alt="" alt=""
/> />
</div> </div>
<div class="truncate inline-flex items-center gap-x-2"> <div class="inline-flex items-center gap-x-2">
<p class="text-sm whitespace-nowrap font-display font-semibold"> <p
class="text-sm whitespace-nowrap font-display font-semibold"
>
{{ item.label }} {{ item.label }}
</p> </p>
<p <p
@@ -126,8 +125,8 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
import { import {
ArrowPathIcon, ArrowPathIcon,
MagnifyingGlassIcon, MagnifyingGlassIcon,
MinusIcon, MinusSmallIcon,
PlusIcon, PlusSmallIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { import {
@@ -144,7 +143,7 @@ const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Installed]: "text-green-500", [GameStatusEnum.Installed]: "text-green-500",
[GameStatusEnum.Downloading]: "text-zinc-400", [GameStatusEnum.Downloading]: "text-zinc-400",
[GameStatusEnum.Validating]: "text-blue-300", [GameStatusEnum.Validating]: "text-blue-300",
[GameStatusEnum.Running]: "text-blue-500", [GameStatusEnum.Running]: "text-green-500",
[GameStatusEnum.Remote]: "text-zinc-700", [GameStatusEnum.Remote]: "text-zinc-700",
[GameStatusEnum.Queued]: "text-zinc-400", [GameStatusEnum.Queued]: "text-zinc-400",
[GameStatusEnum.Updating]: "text-zinc-400", [GameStatusEnum.Updating]: "text-zinc-400",
@@ -173,76 +172,49 @@ const loading = ref(false);
const games: { const games: {
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> }; [key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
} = {}; } = {};
const icons: { [key: string]: string } = {};
const collections: Ref<Collection[]> = ref([]); const collections: Ref<Collection[]> = ref([]);
async function calculateGames(clearAll = false, forceRefresh = false) { async function calculateGames(clearAll = false, forceRefresh = false) {
try {
await calculateGamesLogic(clearAll, forceRefresh);
} catch (e) {
createModal(
ModalType.Notification,
{
title: "Failed to fetch library",
description: `Drop encountered an error while fetching your library: ${e}`,
},
(_, c) => c(),
);
}
loading.value = false;
}
type FetchLibraryResponse = {
library: Game[];
collections: Collection[];
other: Game[];
};
async function calculateGamesLogic(clearAll = false, forceRefresh = false) {
if (clearAll) { if (clearAll) {
collections.value = []; collections.value = [];
loading.value = true; loading.value = true;
} }
// If we update immediately, the navigation gets re-rendered before we // If we update immediately, the navigation gets re-rendered before we
// add all the necessary state, and it freaks tf out // add all the necessary state, and it freaks tf out
const library = await invoke<FetchLibraryResponse>("fetch_library", { const newGames = await invoke<Game[]>("fetch_library", {
hardRefresh: forceRefresh,
});
const otherCollections = await invoke<Collection[]>("fetch_collections", {
hardRefresh: forceRefresh, hardRefresh: forceRefresh,
}); });
const allGames = [ const allGames = [
...library.library, ...newGames,
...library.collections ...otherCollections
.map((e) => e.entries) .map((e) => e.entries)
.flat() .flat()
.map((e) => e.game), .map((e) => e.game),
...library.other,
].filter((v, i, a) => a.indexOf(v) === i); ].filter((v, i, a) => a.indexOf(v) === i);
for (const game of allGames) { for (const game of allGames) {
if (games[game.id]) continue; if (games[game.id]) continue;
games[game.id] = await useGame(game.id); games[game.id] = await useGame(game.id);
} }
for (const game of allGames) {
if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconObjectId);
}
const libraryCollection = { const libraryCollection = {
id: "library", id: "library",
name: "Library", name: "Library",
isDefault: true, isDefault: true,
entries: library.library.map((e) => ({ gameId: e.id, game: e })), entries: newGames.map((e) => ({ gameId: e.id, game: e })),
} satisfies Collection;
const otherCollection = {
id: "other",
name: "Tools & Launchers",
isDefault: false,
isTools: true,
entries: library.other.map((v) => ({ gameId: v.id, game: v })),
} satisfies Collection; } satisfies Collection;
loading.value = false; loading.value = false;
collections.value = [ collections.value = [libraryCollection, ...otherCollections];
libraryCollection,
...library.collections,
...(library.other.length > 0 ? [otherCollection] : []),
];
} }
// Wait up to 300 ms for the library to load, otherwise // Wait up to 300 ms for the library to load, otherwise
@@ -263,17 +235,15 @@ const navigation = computed(() =>
const status = games[game.id].status; const status = games[game.id].status;
const isInstalled = computed( const isInstalled = computed(
() => status.value.type != GameStatusEnum.Remote, () => status.value.type != GameStatusEnum.Remote
); );
const item = { const item = {
label: game.mName, label: game.mName,
route: `/library/${game.id}`, route: `/library/${game.id}`,
prefix: `/library/${game.id}`, prefix: `/library/${game.id}`,
icon: game.mIconObjectId,
isInstalled, isInstalled,
id: game.id, id: game.id,
type: game.type,
}; };
return item; return item;
}); });
@@ -282,10 +252,9 @@ const navigation = computed(() =>
id: collection.id, id: collection.id,
name: collection.name, name: collection.name,
deft: collection.isDefault, deft: collection.isDefault,
tools: collection.isTools ?? false,
items, items,
}; };
}), })
); );
const route = useRoute(); const route = useRoute();
@@ -308,7 +277,7 @@ const filteredNavigation = computed(() => {
listen("update_library", async (event) => { listen("update_library", async (event) => {
console.log("Updating library"); console.log("Updating library");
let oldNavigation = currentNavigation.value; let oldNavigation = currentNavigation.value;
await calculateGames(false, true); await calculateGames();
if (oldNavigation !== currentNavigation.value) { if (oldNavigation !== currentNavigation.value) {
router.push("/library"); router.push("/library");
} }

View File

@@ -1,15 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowDownTrayIcon, CloudIcon } from "@heroicons/vue/20/solid"; import { ArrowDownTrayIcon, CloudIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
async function checkOffline() {
const isOffline = await invoke("check_online");
}
</script> </script>
<template> <template>
<button <div
@click="checkOffline"
class="transition inline-flex items-center rounded-sm px-4 py-1.5 bg-zinc-900 text-sm text-zinc-400 gap-x-2" class="transition inline-flex items-center rounded-sm px-4 py-1.5 bg-zinc-900 text-sm text-zinc-400 gap-x-2"
> >
<div class="relative"> <div class="relative">
@@ -19,5 +13,5 @@ async function checkOffline() {
/> />
</div> </div>
Offline Offline
</button> </div>
</template> </template>

View File

@@ -33,18 +33,4 @@ listen("update_stats", (event) => {
stats.value = event.payload as StatsState; stats.value = event.payload as StatsState;
}); });
export const useDownloadHistory = () => useState<Array<number>>('history', () => []); export const useDownloadHistory = () => useState<Array<number>>('history', () => []);
export function formatKilobytes(bytes: number): string {
const units = ["K", "M", "G", "T", "P"];
let value = bytes;
let unitIndex = 0;
const scalar = 1000;
while (value >= scalar && unitIndex < units.length - 1) {
value /= scalar;
unitIndex++;
}
return `${value.toFixed(1)} ${units[unitIndex]}`;
}

View File

@@ -43,18 +43,19 @@ export const useGame = async (gameId: string) => {
gameStatusRegistry[gameId] = ref(parseStatus(data.status)); gameStatusRegistry[gameId] = ref(parseStatus(data.status));
listen(`update_game/${gameId}`, (event) => { listen(`update_game/${gameId}`, (event) => {
console.log(event);
const payload: { const payload: {
status: SerializedGameStatus; status: SerializedGameStatus;
version?: GameVersion; version?: GameVersion;
} = event.payload as any; } = event.payload as any;
gameStatusRegistry[gameId].value = parseStatus(payload.status); gameStatusRegistry[gameId].value = parseStatus(payload.status);
/** /**
* I am not super happy about this. * I am not super happy about this.
* *
* This will mean that we will still have a version assigned if we have a game installed then uninstall it. * This will mean that we will still have a version assigned if we have a game installed then uninstall it.
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called * It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
* on transient state updates. * on transient state updates.
*/ */
if (payload.version) { if (payload.version) {
gameRegistry[gameId].version = payload.version; gameRegistry[gameId].version = payload.version;
@@ -71,23 +72,3 @@ export const useGame = async (gameId: string) => {
export type FrontendGameConfiguration = { export type FrontendGameConfiguration = {
launchString: string; launchString: string;
}; };
export type LaunchResult =
| { result: "Success" }
| { result: "InstallRequired"; data: [string, string] };
export type VersionOption = {
versionId: string;
displayName?: string;
versionPath: string;
platform: string;
size: number;
requiredContent: Array<{
gameId: string;
versionId: string;
name: string;
iconObjectId: string;
shortDescription: string;
size: number;
}>;
};

View File

@@ -1,5 +1,5 @@
import { convertFileSrc } from "@tauri-apps/api/core"; import { convertFileSrc } from "@tauri-apps/api/core";
export const useObject = (id: string) => { export const useObject = async (id: string) => {
return convertFileSrc(id, "object"); return convertFileSrc(id, "object");
}; };

View File

@@ -12,7 +12,6 @@ export default defineNuxtConfig({
css: ["~/assets/main.scss"], css: ["~/assets/main.scss"],
ssr: false, ssr: false,
devtools: false,
extends: [["../libs/drop-base"]], extends: [["../libs/drop-base"]],

View File

@@ -1,7 +1,7 @@
{ {
"name": "view", "name": "view",
"private": true, "private": true,
"version": "0.3.4", "version": "0.3.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt generate", "build": "nuxt generate",
@@ -13,9 +13,7 @@
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5", "@heroicons/vue": "^2.1.5",
"@nuxtjs/tailwindcss": "^6.12.2", "@nuxtjs/tailwindcss": "^6.12.2",
"@tauri-apps/api": "^2.9.1", "@tauri-apps/api": "^2.7.0",
"@tauri-apps/plugin-os": "^2.3.2",
"@tauri-apps/plugin-shell": "^2.3.3",
"koa": "^2.16.1", "koa": "^2.16.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"micromark": "^4.0.1", "micromark": "^4.0.1",
@@ -34,5 +32,6 @@
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.13",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vue-tsc": "^2.2.10" "vue-tsc": "^2.2.10"
} },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@@ -170,8 +170,8 @@
</div> </div>
</div> </div>
<div class="space-y-6"> <form class="space-y-6">
<div v-if="versionOptions && versionOptions.length > 0 && currentVersionOption"> <div v-if="versionOptions && versionOptions.length > 0">
<Listbox as="div" v-model="installVersionIndex"> <Listbox as="div" v-model="installVersionIndex">
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100" <ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
>Version</ListboxLabel >Version</ListboxLabel
@@ -181,16 +181,9 @@
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6" class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
> >
<span class="block truncate" <span class="block truncate"
>{{ >{{ versionOptions[installVersionIndex].versionName }}
currentVersionOption.displayName ||
currentVersionOption.versionPath
}}
on on
{{ currentVersionOption.platform }} ({{ {{ versionOptions[installVersionIndex].platform }}</span
formatKilobytes(
currentVersionOption.size / 1024
)
}}B)</span
> >
<span <span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2" class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
@@ -213,7 +206,7 @@
<ListboxOption <ListboxOption
as="template" as="template"
v-for="(version, versionIdx) in versionOptions" v-for="(version, versionIdx) in versionOptions"
:key="version.versionId" :key="version.versionName"
:value="versionIdx" :value="versionIdx"
v-slot="{ active, selected }" v-slot="{ active, selected }"
> >
@@ -230,12 +223,8 @@
: 'font-normal', : 'font-normal',
'block truncate', 'block truncate',
]" ]"
>{{ version.displayName || version.versionPath }} on >{{ version.versionName }} on
{{ version.platform }} ({{ {{ version.platform }}</span
formatKilobytes(
versionOptions[installVersionIndex].size / 1024
)
}}B)</span
> >
<span <span
@@ -292,86 +281,82 @@
</div> </div>
</div> </div>
<div v-if="installDirs"> <div v-if="installDirs">
<InstallDirectorySelector <Listbox as="div" v-model="installDir">
:install-dirs="installDirs" <ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
v-model="installDir" >Install to</ListboxLabel
/>
</div>
<div
v-if="
currentVersionOption?.requiredContent &&
currentVersionOption.requiredContent.length > 0
"
>
<div class="border-b border-white/10 py-2">
<h3 class="text-sm font-semibold text-white">
Install additional dependencies?
</h3>
<p class="mt-1 text-xs text-gray-400">
This game requires additional content to run. Click the components
to automatically queue for download.
</p>
</div>
<ul role="list" class="mt-2 divide-y divide-white/5">
<li
v-for="content in currentVersionOption
.requiredContent"
:key="content.versionId"
:class="[
!installDepsDisabled[content.versionId]
? 'bg-zinc-950 ring-2 ring-zinc-800'
: '',
'rounded-lg relative flex justify-between px-2 py-3',
]"
> >
<div class="flex min-w-0 gap-x-2"> <div class="relative mt-2">
<img <ListboxButton
class="size-12 flex-none" class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
:src="useObject(content.iconObjectId)" >
alt="" <span class="block truncate">{{
/> installDirs[installDir]
<div class="min-w-0 flex-auto"> }}</span>
<p class="text-sm/6 font-semibold text-white"> <span
<button class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
@click=" >
() => <ChevronUpDownIcon
(installDepsDisabled[content.versionId] = class="h-5 w-5 text-gray-400"
!installDepsDisabled[content.versionId]) aria-hidden="true"
" />
> </span>
<span class="absolute inset-x-0 -top-px bottom-0"></span> </ListboxButton>
{{ content.name }}
</button> <transition
</p> leave-active-class="transition ease-in duration-100"
<p class="mt-1 flex text-xs/5 text-gray-400"> leave-from-class="opacity-100"
{{ content.shortDescription }} leave-to-class="opacity-0"
</p> >
</div> <ListboxOptions
</div> class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
<div class="flex shrink-0 items-center gap-x-2"> >
<div class="hidden sm:flex sm:flex-col sm:items-end"> <ListboxOption
<p as="template"
class="inline-flex items-center gap-x-1 text-xs/5 text-gray-400" v-for="(dir, dirIdx) in installDirs"
:key="dir"
:value="dirIdx"
v-slot="{ active, selected }"
> >
{{ formatKilobytes(content.size / 1024) }}B <li
<ServerIcon class="size-3" /> :class="[
</p> active ? 'bg-blue-600 text-white' : 'text-zinc-300',
</div> 'relative cursor-default select-none py-2 pl-3 pr-9',
<CheckIcon ]"
v-if="!installDepsDisabled[content.versionId]" >
class="size-5 flex-none text-green-500" <span
aria-hidden="true" :class="[
/> selected
<MinusIcon ? 'font-semibold text-zinc-100'
v-else : 'font-normal',
class="size-5 flex-none text-gray-500" 'block truncate',
aria-hidden="true" ]"
/> >{{ dir }}</span
</div> >
</li>
</ul> <span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
<div class="text-zinc-400 text-sm mt-2">
Add more install directories in
<PageWidget to="/settings/downloads">
<WrenchIcon class="size-3" />
Settings
</PageWidget>
</div>
</Listbox>
</div> </div>
</div> </form>
<div v-if="installError" class="mt-1 rounded-md bg-red-600/10 p-4"> <div v-if="installError" class="mt-1 rounded-md bg-red-600/10 p-4">
<div class="flex"> <div class="flex">
@@ -407,48 +392,6 @@
</template> </template>
</ModalTemplate> </ModalTemplate>
<ModalTemplate :model-value="launchOptionsOpen">
<template #default>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left">
<h3 class="text-base font-semibold text-zinc-100">
Launch {{ game.mName }}
</h3>
<div class="mt-2">
<p class="text-sm text-zinc-400">
The instance admin has configured multiple ways to start this
game. Select an option to start.
</p>
</div>
</div>
</div>
<ol class="space-y-2">
<li v-for="(launchData, launchIdx) in launchOptions!">
<button
class="transition w-full rounded-sm bg-zinc-800 inline-flex items-center text-sm py-2 px-3 gap-x-2 text-zinc-100 hover:text-zinc-300 hover:bg-zinc-700"
@click="() => launchIndex(launchIdx)"
>
<PlayIcon class="size-4" />
<span>
{{ launchData.name }}
</span>
</button>
</li>
</ol>
</template>
<template #buttons>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="launchOptions = undefined"
ref="cancelButtonRef"
>
Cancel
</button>
</template>
</ModalTemplate>
<!-- <!--
Dear future DecDuck, Dear future DecDuck,
This v-if is necessary for Vue rendering reasons This v-if is necessary for Vue rendering reasons
@@ -527,11 +470,6 @@
</div> </div>
</div> </div>
</Transition> </Transition>
<DependencyRequiredModal
v-if="dependencyRequiredModal"
v-model="dependencyRequiredModal"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -551,10 +489,9 @@ import {
XMarkIcon, XMarkIcon,
ArrowsPointingOutIcon, ArrowsPointingOutIcon,
PhotoIcon, PhotoIcon,
PlayIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline"; import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { MinusIcon, ServerIcon, XCircleIcon } from "@heroicons/vue/24/solid"; import { XCircleIcon } from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { micromark } from "micromark"; import { micromark } from "micromark";
import { GameStatusEnum } from "~/types"; import { GameStatusEnum } from "~/types";
@@ -583,7 +520,9 @@ const mediaUrls = await Promise.all(
const htmlDescription = micromark(game.value.mDescription); const htmlDescription = micromark(game.value.mDescription);
const installFlowOpen = ref(false); const installFlowOpen = ref(false);
const versionOptions = ref<undefined | Array<VersionOption>>(); const versionOptions = ref<
undefined | Array<{ versionName: string; platform: string }>
>();
const installDirs = ref<undefined | Array<string>>(); const installDirs = ref<undefined | Array<string>>();
const currentImageIndex = ref(0); const currentImageIndex = ref(0);
@@ -609,31 +548,15 @@ const installLoading = ref(false);
const installError = ref<string | undefined>(); const installError = ref<string | undefined>();
const installVersionIndex = ref(0); const installVersionIndex = ref(0);
const installDir = ref(0); const installDir = ref(0);
const installDepsDisabled = ref<{ [key: string]: boolean }>({});
const currentVersionOption = computed(() => versionOptions.value?.[installVersionIndex.value]);
async function install() { async function install() {
try { try {
if (!versionOptions.value) throw new Error("Versions have not been loaded"); if (!versionOptions.value) throw new Error("Versions have not been loaded");
installLoading.value = true; installLoading.value = true;
const versionOption = versionOptions.value[installVersionIndex.value]; await invoke("download_game", {
gameId: game.value.id,
const games = [ gameVersion: versionOptions.value[installVersionIndex.value].versionName,
{ gameId: game.value.id, versionId: versionOption.versionId }, installDir: installDir.value,
...versionOption.requiredContent });
.filter((v) => !installDepsDisabled.value[v.versionId])
.map((v) => ({ gameId: v.gameId, versionId: v.versionId })),
];
for (const game of games) {
await invoke("download_game", {
gameId: game.gameId,
versionId: game.versionId,
installDir: installDir.value,
targetPlatform: versionOption.platform,
});
}
installFlowOpen.value = false; installFlowOpen.value = false;
} catch (error) { } catch (error) {
installError.value = (error as string).toString(); installError.value = (error as string).toString();
@@ -650,24 +573,9 @@ async function resumeDownload() {
} }
} }
const launchOptions = ref<Array<{ name: string }> | undefined>(undefined);
const launchOptionsOpen = computed(() => launchOptions.value !== undefined);
async function launch() { async function launch() {
if (status.value.type == GameStatusEnum.SetupRequired) {
await launchIndex(0);
return;
}
try { try {
const fetchedLaunchOptions = await invoke<Array<{ name: string }>>( await invoke("launch_game", { id: game.value.id });
"get_launch_options",
{ id: game.value.id }
);
if (fetchedLaunchOptions.length == 1) {
await launchIndex(0);
return;
}
launchOptions.value = fetchedLaunchOptions;
} catch (e) { } catch (e) {
createModal( createModal(
ModalType.Notification, ModalType.Notification,
@@ -682,36 +590,6 @@ async function launch() {
} }
} }
const dependencyRequiredModal = ref<
{ gameId: string; versionId: string } | undefined
>(undefined);
async function launchIndex(index: number) {
launchOptions.value = undefined;
try {
const result = await invoke<LaunchResult>("launch_game", {
id: game.value.id,
index,
});
if (result.result == "InstallRequired") {
dependencyRequiredModal.value = {
gameId: result.data[0],
versionId: result.data[1],
};
}
} catch (e) {
createModal(
ModalType.Notification,
{
title: `Couldn't run "${game.value.mName}"`,
description: `Drop failed to launch "${game.value.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c()
);
}
}
async function queue() { async function queue() {
router.push("/queue"); router.push("/queue");
} }

View File

@@ -7,7 +7,7 @@
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2" class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2"
> >
<span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span> <span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span>
<span class="text-xs text-zinc-400" <span v-if="stats.time > 0" class="text-xs text-zinc-400"
>{{ formatTime(stats.time) }} left</span >{{ formatTime(stats.time) }} left</span
> >
</div> </div>
@@ -184,10 +184,21 @@ async function cancelGame(meta: DownloadableMetadata) {
await invoke("cancel_game", { meta }); await invoke("cancel_game", { meta });
} }
function formatTime(seconds: number): string { function formatKilobytes(bytes: number): string {
if (seconds == 0) { const units = ["K", "M", "G", "T", "P"];
return `0s`; let value = bytes;
let unitIndex = 0;
const scalar = 1000;
while (value >= scalar && unitIndex < units.length - 1) {
value /= scalar;
unitIndex++;
} }
return `${value.toFixed(1)} ${units[unitIndex]}`;
}
function formatTime(seconds: number): string {
if (seconds < 60) { if (seconds < 60) {
return `${Math.round(seconds)}s`; return `${Math.round(seconds)}s`;
} }

View File

@@ -116,7 +116,7 @@ platformInfo.value = currentPlatform;
async function openDataDir() { async function openDataDir() {
if (!dataDir.value) return; if (!dataDir.value) return;
try { try {
await invoke("open_fs", { path: dataDir.value }); await open(dataDir.value);
} catch (error) { } catch (error) {
console.error("Failed to open data dir:", error); console.error("Failed to open data dir:", error);
} }
@@ -126,7 +126,7 @@ async function openLogFile() {
if (!dataDir.value) return; if (!dataDir.value) return;
try { try {
const logPath = `${dataDir.value}/drop.log`; const logPath = `${dataDir.value}/drop.log`;
await invoke("open_fs", { path: logPath }); await open(logPath);
} catch (error) { } catch (error) {
console.error("Failed to open log file:", error); console.error("Failed to open log file:", error);
} }

View File

@@ -1,12 +1,37 @@
<template> <template>
<iframe :src="convertedStoreUrl" class="grow w-full h-full" /> <div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<BuildingStorefrontIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Store not supported in client
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
Currently, Drop requires you to view the store in your browser.
Please click the button below to open it in your default browser.
</p>
<NuxtLink
:href="storeUrl"
target="_blank"
class="mt-6 transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
Open Store <ArrowTopRightOnSquareIcon class="size-4" />
</NuxtLink>
</div>
</div>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import {
ArrowTopRightOnSquareIcon, ArrowTopRightOnSquareIcon,
BuildingStorefrontIcon, BuildingStorefrontIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
import { convertFileSrc, invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
const convertedStoreUrl = convertFileSrc("store", "server"); const storeUrl = await invoke<string>("gen_drop_url", { path: "/store" });
</script> </script>

8217
main/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +0,0 @@
onlyBuiltDependencies:
- '@parcel/watcher'
- esbuild

View File

@@ -27,7 +27,6 @@ export type AppState = {
export type Game = { export type Game = {
id: string; id: string;
type: "Game" | "Executor" | "Redist";
mName: string; mName: string;
mShortDescription: string; mShortDescription: string;
mDescription: string; mDescription: string;
@@ -42,7 +41,6 @@ export type Collection = {
id: string; id: string;
name: string; name: string;
isDefault: boolean; isDefault: boolean;
isTools?: boolean;
entries: Array<{ gameId: string; game: Game }>; entries: Array<{ gameId: string; game: Game }>;
}; };

8091
main/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
GSK_RENDERER=ngl pnpm tauri dev WEBKIT_DISABLE_DMABUF_RENDERER=1 yarn tauri dev

View File

@@ -3,7 +3,7 @@
ARCH=$(uname -m) ARCH=$(uname -m)
# build tauri apps # build tauri apps
# NO_STRIP=true pnpm tauri build -- --verbose # NO_STRIP=true yarn tauri build -- --verbose
# unpack appimage # unpack appimage
APPIMAGE=$(ls ./src-tauri/target/release/bundle/appimage/*.AppImage) APPIMAGE=$(ls ./src-tauri/target/release/bundle/appimage/*.AppImage)

View File

@@ -7,11 +7,16 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.7.0",
"@tauri-apps/plugin-deep-link": "^2.4.1",
"@tauri-apps/plugin-dialog": "^2.3.2",
"@tauri-apps/plugin-opener": "^2.4.0",
"@tauri-apps/plugin-os": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0",
"pino": "^9.7.0", "pino": "^9.7.0",
"pino-pretty": "^13.1.1", "pino-pretty": "^13.1.1"
"tauri": "^0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.9.4" "@tauri-apps/cli": "^2.7.1"
} }
} }

5583
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
onlyBuiltDependencies:
- sharp
overrides:
cross-spawn@<6.0.6: '>=6.0.6'
cross-spawn@>=7.0.0 <7.0.5: '>=7.0.5'
form-data@<2.5.4: '>=2.5.4'
got@<11.8.5: '>=11.8.5'
http-cache-semantics@<4.1.1: '>=4.1.1'
lodash@<4.17.21: '>=4.17.21'
lodash@>=4.0.0 <4.17.21: '>=4.17.21'
minimist@>=1.0.0 <1.2.6: '>=1.2.6'
nth-check@<2.0.1: '>=2.0.1'
semver-regex@<3.1.3: '>=3.1.3'
semver-regex@<3.1.4: '>=3.1.4'
semver@>=7.0.0 <7.5.2: '>=7.5.2'
sharp@<0.30.5: '>=0.30.5'
sharp@<0.32.6: '>=0.32.6'
tmp@<=0.2.3: '>=0.2.4'
tough-cookie@<4.1.3: '>=4.1.3'
trim-newlines@<3.0.1: '>=3.0.1'

3848
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +1,72 @@
[package] [package]
name = "drop-app" name = "drop-app"
version = "0.4.0" version = "0.3.3"
description = "The client application for the open-source, self-hosted game distribution platform Drop" # authors = ["Drop OSS"]
authors = ["Drop OSS"]
edition = "2024" edition = "2024"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
[workspace] [workspace]
resolver = "3" resolver = "3"
members = [ members = ["drop-consts",
"client", "drop-database",
"cloud_saves", "drop-downloads",
"database", "drop-errors", "drop-library",
"download_manager", "drop-native-library",
"games", "drop-process",
"process", "drop-remote",
"remote",
"utils",
] ]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]
crate-type = ["cdylib", "rlib", "staticlib"] crate-type = ["cdylib", "rlib", "staticlib"]
# The `_lib` suffix may seem redundant but it is necessary # The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name. # to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "drop_app_lib" name = "drop_app_lib"
rustflags = ["-C", "target-feature=+aes,+sse2"] # rustflags = ["-C", "target-feature=+aes,+sse2"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
atomic-instant-full = "0.1.0"
bitcode = "0.6.6"
boxcar = "0.2.7" boxcar = "0.2.7"
bytes = "1.10.1"
cacache = "13.1.0"
chrono = "0.4.38"
client = { path = "./client", version = "0.1.0" } # client
database = { path = "./database" } # database
deranged = "=0.4.0"
dirs = "6.0.0" dirs = "6.0.0"
download_manager = { path = "./download_manager", version = "0.1.0" } # download manager drop-database = { path = "./drop-database" }
droplet-rs = "0.7.3" drop-downloads = { path = "./drop-downloads" }
filetime = "0.2.25" drop-errors = { path = "./drop-errors" }
futures-core = "0.3.31" drop-native-library = { path = "./drop-native-library" }
drop-process = { path = "./drop-process" }
drop-remote = { path = "./drop-remote" }
futures-lite = "2.6.0" futures-lite = "2.6.0"
games = { path = "./games", version = "0.1.0" } # games
gethostname = "1.0.1"
hex = "0.4.3" hex = "0.4.3"
http = "1.1.0" http = "1.1.0"
http-serde = "2.1.1"
humansize = "2.1.3"
known-folders = "1.2.0" known-folders = "1.2.0"
log = "0.4.22" log = "0.4.22"
md5 = "0.7.0" md5 = "0.7.0"
native_model = { git = "https://github.com/Drop-OSS/native_model.git", version = "0.6.4", features = [
"rmp_serde_1_3",
] }
page_size = "0.6.0"
parking_lot = "0.12.3"
process = { path = "./process" } # process
rand = "0.9.1"
rayon = "1.10.0" rayon = "1.10.0"
regex = "1.11.1" regex = "1.11.1"
remote = { path = "./remote", version = "0.1.0" } # remote reqwest-websocket = "0.5.0"
reqwest = { version = "0.12.28", default-features = false, features = [ serde_json = "1"
tar = "0.4.44"
tauri = { version = "2.7.0", features = ["protocol-asset", "tray-icon"] }
tauri-plugin-autostart = "2.0.0"
tauri-plugin-deep-link = "2"
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2.4.0"
tauri-plugin-os = "2"
tauri-plugin-shell = "2.2.1"
tempfile = "3.19.1"
url = "2.5.2"
webbrowser = "1.0.2"
whoami = "1.6.0"
zstd = "0.13.3"
[dependencies.log4rs]
version = "1.3.0"
features = ["console_appender", "file_appender"]
[dependencies.reqwest]
version = "0.12.22"
default-features = false
features = [
"blocking", "blocking",
"http2", "http2",
"json", "json",
@@ -71,44 +74,7 @@ reqwest = { version = "0.12.28", default-features = false, features = [
"rustls-tls", "rustls-tls",
"rustls-tls-native-roots", "rustls-tls-native-roots",
"stream", "stream",
] } ]
reqwest-middleware = "0.4.0"
reqwest-middleware-cache = "0.1.1"
reqwest-websocket = "0.5.0"
schemars = "0.8.22"
serde_json = "1"
serde_with = "3.12.0"
sha1 = "0.10.6"
shared_child = "1.0.1"
slice-deque = "0.3.0"
sysinfo = "0.36.1"
tar = "0.4.44"
tauri-plugin-autostart = "*"
tauri-plugin-deep-link = "*"
tauri-plugin-dialog = "*"
tauri-plugin-opener = "*"
tauri-plugin-os = "*"
tauri-plugin-shell = "*"
tempfile = "3.19.1"
throttle_my_fn = "0.2.6"
tokio-util = { version = "0.7.16", features = ["io"] }
umu-wrapper-lib = "0.1.0"
url = "2.5.2"
urlencoding = "2.1.3"
utils = { path = "./utils" } # utils
walkdir = "2.5.0"
webbrowser = "1.0.2"
whoami = "1.6.0"
wry = { version = "*", features = [] }
zstd = "0.13.3"
[dependencies.log4rs]
version = "1.3.0"
features = ["console_appender", "file_appender"]
[dependencies.rustbreak]
version = "2"
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
[dependencies.rustix] [dependencies.rustix]
version = "0.38.37" version = "0.38.37"
@@ -118,20 +84,12 @@ features = ["fs"]
version = "1" version = "1"
features = ["derive", "rc"] features = ["derive", "rc"]
[dependencies.tauri]
version = "2.9.5"
features = ["protocol-asset", "tray-icon", "unstable"]
[dependencies.tokio]
version = "1.40.0"
features = ["rt", "signal", "tokio-macros"]
[dependencies.uuid] [dependencies.uuid]
version = "1.10.0" version = "1.10.0"
features = ["fast-rng", "macro-diagnostics", "v4"] features = ["fast-rng", "macro-diagnostics", "v4"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "*", features = [] } tauri-build = { version = "2.0.0", features = [] }
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies] [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] } tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
@@ -141,10 +99,3 @@ lto = true
panic = "abort" panic = "abort"
codegen-units = 1 codegen-units = 1
[profile.dev.package."*"]
# Set the default for dependencies in Development mode.
opt-level = 3
[profile.dev]
# Turn on a small amount of optimisation in Development mode.
opt-level = 1

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
[package]
name = "client"
version = "0.1.0"
edition = "2024"
[dependencies]
bitcode = "*"
database = { version = "*", path = "../database" }
log = "*"
serde = { version = "*", features = ["derive"] }
tauri = "*"
tauri-plugin-autostart = "*"

View File

@@ -1,10 +0,0 @@
use serde::Serialize;
use crate::{app_status::AppStatus, user::User};
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppState {
pub status: AppStatus,
pub user: Option<User>
}

View File

@@ -1,12 +0,0 @@
use serde::Serialize;
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
pub enum AppStatus {
NotConfigured,
Offline,
ServerError,
SignedOut,
SignedIn,
SignedInNeedsReauth,
ServerUnavailable,
}

View File

@@ -1,26 +0,0 @@
use database::borrow_db_checked;
use log::debug;
use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt;
// New function to sync state on startup
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
let db_handle = borrow_db_checked();
let should_be_enabled = db_handle.settings.autostart;
drop(db_handle);
let manager = app.autolaunch();
let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
if current_state != should_be_enabled {
if should_be_enabled {
manager.enable().map_err(|e| e.to_string())?;
debug!("synced autostart: enabled");
} else {
manager.disable().map_err(|e| e.to_string())?;
debug!("synced autostart: disabled");
}
}
Ok(())
}

View File

@@ -1,52 +0,0 @@
use std::{
ffi::OsStr,
path::PathBuf,
process::{Command, Stdio},
sync::LazyLock,
};
use log::info;
pub static COMPAT_INFO: LazyLock<Option<CompatInfo>> = LazyLock::new(create_new_compat_info);
pub static UMU_LAUNCHER_EXECUTABLE: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
let x = get_umu_executable();
info!("{:?}", &x);
x
});
#[derive(Clone)]
pub struct CompatInfo {
pub umu_installed: bool,
}
fn create_new_compat_info() -> Option<CompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = UMU_LAUNCHER_EXECUTABLE.is_some();
Some(CompatInfo {
umu_installed: has_umu_installed,
})
}
const UMU_BASE_LAUNCHER_EXECUTABLE: &str = "umu-run";
const UMU_INSTALL_DIRS: [&str; 4] = ["/app/share", "/use/local/share", "/usr/share", "/opt"];
fn get_umu_executable() -> Option<PathBuf> {
if check_executable_exists(UMU_BASE_LAUNCHER_EXECUTABLE) {
return Some(PathBuf::from(UMU_BASE_LAUNCHER_EXECUTABLE));
}
for dir in UMU_INSTALL_DIRS {
let p = PathBuf::from(dir).join(UMU_BASE_LAUNCHER_EXECUTABLE);
if check_executable_exists(&p) {
return Some(p);
}
}
None
}
fn check_executable_exists<P: AsRef<OsStr>>(exec: P) -> bool {
let has_umu_installed = Command::new(exec).stdout(Stdio::null()).output();
has_umu_installed.is_ok()
}

View File

@@ -1,5 +0,0 @@
pub mod app_status;
pub mod autostart;
pub mod compat;
pub mod user;
pub mod app_state;

View File

@@ -1,12 +0,0 @@
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct User {
id: String,
username: String,
admin: bool,
display_name: String,
profile_picture_object_id: String,
}

View File

@@ -1,19 +0,0 @@
[package]
name = "cloud_saves"
version = "0.1.0"
edition = "2024"
[dependencies]
database = { version = "0.1.0", path = "../database" }
dirs = "6.0.0"
log = "0.4.28"
regex = "1.11.3"
rustix = "1.1.2"
serde = "1.0.228"
serde_json = "1.0.145"
serde_with = "3.15.0"
tar = "0.4.44"
tempfile = "3.23.0"
uuid = "1.18.1"
whoami = "1.6.1"
zstd = "0.13.3"

View File

@@ -1,234 +0,0 @@
use std::{collections::HashMap, path::PathBuf, str::FromStr};
#[cfg(target_os = "linux")]
use database::platform::Platform;
use database::{GameVersion, db::DATA_ROOT_DIR};
use log::warn;
use crate::error::BackupError;
use super::path::CommonPath;
pub struct BackupManager<'a> {
pub current_platform: Platform,
pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>,
}
impl Default for BackupManager<'_> {
fn default() -> Self {
Self::new()
}
}
impl BackupManager<'_> {
pub fn new() -> Self {
BackupManager {
#[cfg(target_os = "windows")]
current_platform: Platform::Windows,
#[cfg(target_os = "macos")]
current_platform: Platform::macOS,
#[cfg(target_os = "linux")]
current_platform: Platform::Linux,
sources: HashMap::from([
// Current platform to target platform
(
(Platform::Windows, Platform::Windows),
&WindowsBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
(
(Platform::Linux, Platform::Linux),
&LinuxBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
(
(Platform::macOS, Platform::macOS),
&MacBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
]),
}
}
}
pub trait BackupHandler: Send + Sync {
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(DATA_ROOT_DIR.join("games"))
}
fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(PathBuf::from_str(&game.game_id).unwrap())
}
fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(self
.root_translate(path, game)?
.join(self.game_translate(path, game)?))
}
fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
let c = CommonPath::Home.get().ok_or(BackupError::NotFound);
println!("{:?}", c);
c
}
fn store_user_id_translate(
&self,
_path: &PathBuf,
game: &GameVersion,
) -> Result<PathBuf, BackupError> {
PathBuf::from_str(&game.game_id).map_err(|_| BackupError::ParseError)
}
fn os_user_name_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
Ok(PathBuf::from_str(&whoami::username()).unwrap())
}
fn win_app_data_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected Windows Reference in Backup <winAppData>");
Err(BackupError::InvalidSystem)
}
fn win_local_app_data_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected Windows Reference in Backup <winLocalAppData>");
Err(BackupError::InvalidSystem)
}
fn win_local_app_data_low_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected Windows Reference in Backup <winLocalAppDataLow>");
Err(BackupError::InvalidSystem)
}
fn win_documents_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected Windows Reference in Backup <winDocuments>");
Err(BackupError::InvalidSystem)
}
fn win_public_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected Windows Reference in Backup <winPublic>");
Err(BackupError::InvalidSystem)
}
fn win_program_data_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected Windows Reference in Backup <winProgramData>");
Err(BackupError::InvalidSystem)
}
fn win_dir_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected Windows Reference in Backup <winDir>");
Err(BackupError::InvalidSystem)
}
fn xdg_data_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected XDG Reference in Backup <xdgData>");
Err(BackupError::InvalidSystem)
}
fn xdg_config_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
warn!("Unexpected XDG Reference in Backup <xdgConfig>");
Err(BackupError::InvalidSystem)
}
fn skip_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(PathBuf::new())
}
}
pub struct LinuxBackupManager {}
impl BackupHandler for LinuxBackupManager {
fn xdg_config_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
CommonPath::Data.get().ok_or(BackupError::NotFound)
}
fn xdg_data_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
CommonPath::Config.get().ok_or(BackupError::NotFound)
}
}
pub struct WindowsBackupManager {}
impl BackupHandler for WindowsBackupManager {
fn win_app_data_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
CommonPath::Config.get().ok_or(BackupError::NotFound)
}
fn win_local_app_data_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
CommonPath::DataLocal.get().ok_or(BackupError::NotFound)
}
fn win_local_app_data_low_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
CommonPath::DataLocalLow
.get()
.ok_or(BackupError::NotFound)
}
fn win_dir_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
Ok(PathBuf::from_str("C:/Windows").unwrap())
}
fn win_documents_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
CommonPath::Document.get().ok_or(BackupError::NotFound)
}
fn win_program_data_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
Ok(PathBuf::from_str("C:/ProgramData").unwrap())
}
fn win_public_translate(
&self,
_path: &PathBuf,
_game: &GameVersion,
) -> Result<PathBuf, BackupError> {
CommonPath::Public.get().ok_or(BackupError::NotFound)
}
}
pub struct MacBackupManager {}
impl BackupHandler for MacBackupManager {}

View File

@@ -1,27 +0,0 @@
use std::fmt::Display;
use serde_with::SerializeDisplay;
#[derive(Debug, SerializeDisplay, Clone, Copy)]
pub enum BackupError {
InvalidSystem,
NotFound,
ParseError,
}
impl Display for BackupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
BackupError::InvalidSystem => "Attempted to generate path for invalid system",
BackupError::NotFound => "Could not generate or find path",
BackupError::ParseError => "Failed to parse path",
};
write!(f, "{}", s)
}
}

View File

@@ -1,8 +0,0 @@
pub mod backup_manager;
pub mod conditions;
pub mod error;
pub mod metadata;
pub mod normalise;
pub mod path;
pub mod placeholder;
pub mod resolver;

View File

@@ -1,19 +0,0 @@
[package]
name = "database"
version = "0.1.0"
edition = "2024"
[dependencies]
aes = "0.8.4"
anyhow = "*"
chrono = "0.4.42"
ctr = "0.9.2"
dirs = "6.0.0"
keyring = { version = "3.6.3", features = ["apple-native", "crypto-rust", "linux-native-sync-persistent", "windows-native"] }
log = "0.4.28"
rand = "0.9.2"
ron = "0.12.0"
serde = "1.0.228"
serde_with = "3.15.0"
url = "2.5.7"
whoami = "1.6.1"

View File

@@ -1,37 +0,0 @@
use std::{
path::PathBuf,
sync::{Arc, LazyLock},
};
use keyring::Entry;
use log::info;
use crate::interface::{DatabaseInterface};
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
#[cfg(not(debug_assertions))]
static DATA_ROOT_PREFIX: &str = "drop";
#[cfg(debug_assertions)]
static DATA_ROOT_PREFIX: &str = "drop-debug";
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = LazyLock::new(|| {
Arc::new(
dirs::data_dir()
.expect("Failed to get data dir")
.join(DATA_ROOT_PREFIX),
)
});
pub(crate) static KEY_IV: LazyLock<([u8; 16], [u8; 16])> = LazyLock::new(|| {
let entry = Entry::new("drop", "database_key").expect("failed to open keyring");
let mut key = entry.get_secret().unwrap_or_else(|_| {
let mut buffer = [0u8; 32];
rand::fill(&mut buffer);
entry.set_secret(&buffer).expect("failed to save key");
info!("created new database key");
buffer.to_vec()
});
let new = key.split_off(16);
(new.try_into().expect("failed to extract key"), key.try_into().expect("failed to extract iv"))
});

View File

@@ -1,249 +0,0 @@
use std::{
fs::{self, create_dir_all},
mem::ManuallyDrop,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard},
};
use aes::cipher::{KeyIvInit as _, StreamCipher as _};
use anyhow::Error;
use chrono::Utc;
use log::{debug, error, info, warn};
use url::Url;
use crate::{
db::{DATA_ROOT_DIR, DB, KEY_IV},
models::{
self,
data::{Database, DatabaseVersionSerializable},
},
};
type Aes128Ctr64LE = ctr::Ctr64LE<aes::Aes128>;
pub struct DatabaseInterface {
data: RwLock<models::data::Database>,
path: PathBuf,
}
impl DatabaseInterface {
pub fn set_up_database() -> Self {
let db_path = DATA_ROOT_DIR.join("drop.db");
let games_base_dir = DATA_ROOT_DIR.join("games");
let logs_root_dir = DATA_ROOT_DIR.join("logs");
let cache_dir = DATA_ROOT_DIR.join("cache");
let pfx_dir = DATA_ROOT_DIR.join("pfx");
debug!("creating data directory at {DATA_ROOT_DIR:?}");
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
DATA_ROOT_DIR.display(),
e
)
});
create_dir_all(&games_base_dir).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
games_base_dir.display(),
e
)
});
create_dir_all(&logs_root_dir).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
logs_root_dir.display(),
e
)
});
create_dir_all(&cache_dir).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
cache_dir.display(),
e
)
});
create_dir_all(&pfx_dir).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
pfx_dir.display(),
e
)
});
let exists = fs::exists(db_path.clone()).unwrap_or_else(|e| {
panic!(
"Failed to find if {} exists with error {}",
db_path.display(),
e
)
});
if exists {
match DatabaseInterface::open_at_path(&db_path) {
Ok(db) => db.unwrap(),
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir)
.expect("failed to recover from failed database"),
}
} else {
let default = Database::new(games_base_dir, None, cache_dir);
debug!("Creating database at path {}", db_path.display());
DatabaseInterface::create_at_path(&db_path, default)
.expect("Database could not be created")
}
}
pub fn open_at_path(db_path: &Path) -> Result<Option<DatabaseInterface>, Error> {
if !db_path.exists() {
return Ok(None);
};
let mut database_data = std::fs::read(db_path)?;
let (key, iv) = *KEY_IV;
let mut cipher = Aes128Ctr64LE::new(&key.into(), &iv.into());
cipher.apply_keystream(&mut database_data);
let database_data = String::from_utf8(database_data)?;
let database_data: DatabaseVersionSerializable = ron::from_str(&database_data)?;
Ok(Some(DatabaseInterface {
data: RwLock::new(database_data.0),
path: db_path.to_path_buf(),
}))
}
pub fn create_at_path(db_path: &Path, database: Database) -> Result<DatabaseInterface, Error> {
let database = DatabaseVersionSerializable(database);
let mut database_data = ron::to_string(&database)?.into_bytes();
let (key, iv) = *KEY_IV;
let mut cipher = Aes128Ctr64LE::new(&key.into(), &iv.into());
cipher.apply_keystream(&mut database_data);
std::fs::write(db_path, database_data)?;
Ok(DatabaseInterface {
data: RwLock::new(database.0),
path: db_path.to_path_buf(),
})
}
pub fn database_is_set_up(&self) -> bool {
!borrow_db_checked().base_url.is_empty()
}
pub fn fetch_base_url(&self) -> Url {
let handle = borrow_db_checked();
Url::parse(&handle.base_url)
.unwrap_or_else(|_| panic!("Failed to parse base url {}", handle.base_url))
}
fn save(&self) -> Result<(), Error> {
let lock = self.data.read().expect("failed to lock database to save");
DatabaseInterface::create_at_path(&self.path, lock.clone())?;
Ok(())
}
fn borrow_data(
&self,
) -> Result<
std::sync::RwLockReadGuard<'_, Database>,
PoisonError<std::sync::RwLockReadGuard<'_, Database>>,
> {
self.data.read()
}
fn borrow_data_mut(
&self,
) -> Result<
std::sync::RwLockWriteGuard<'_, Database>,
PoisonError<std::sync::RwLockWriteGuard<'_, Database>>,
> {
self.data.write()
}
}
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
fn handle_invalid_database(
error: Error,
db_path: PathBuf,
games_base_dir: PathBuf,
cache_dir: PathBuf,
) -> Result<DatabaseInterface, Error> {
warn!("{error:?}");
let new_path = {
let time = Utc::now().timestamp();
let mut base = db_path.clone();
base.set_file_name(format!("drop.db.backup-{time}"));
base
};
info!("old database stored at: {}", new_path.to_string_lossy());
fs::rename(&db_path, &new_path).unwrap_or_else(|e| {
panic!(
"Could not rename database {} to {} with error {}",
db_path.display(),
new_path.display(),
e
)
});
let db = Database::new(games_base_dir, Some(new_path), cache_dir);
Ok(DatabaseInterface::create_at_path(&db_path, db).expect("Database could not be created"))
}
// To automatically save the database upon drop
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Drop for DBWrite<'_> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(()) => {}
Err(e) => {
error!("database failed to save with error {e}");
panic!("database failed to save with error {e}")
}
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() {
Ok(data) => DBRead(data),
Err(e) => {
error!("database borrow failed with error {e}");
panic!("database borrow failed with error {e}");
}
}
}
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
match DB.borrow_data_mut() {
Ok(data) => DBWrite(ManuallyDrop::new(data)),
Err(e) => {
error!("database borrow mut failed with error {e}");
panic!("database borrow mut failed with error {e}");
}
}
}

View File

@@ -1,14 +0,0 @@
#![feature(nonpoison_rwlock)]
pub mod db;
pub mod debug;
pub mod interface;
pub mod models;
pub mod platform;
pub use db::DB;
pub use interface::{borrow_db_checked, borrow_db_mut_checked};
pub use models::data::{
ApplicationTransientStatus, Database, DatabaseApplications, DatabaseAuth, DownloadType,
DownloadableMetadata, GameDownloadStatus, GameVersion, Settings,
};

View File

@@ -1,284 +0,0 @@
pub mod data {
use std::{hash::Hash, path::PathBuf};
use serde::{Deserialize, Serialize};
// NOTE: Within each version, you should NEVER use these types.
// Declare it using the actual version that it is from, i.e. v1::Settings rather than just Settings from here
pub type Database = v1::Database;
pub type GameVersion = v1::GameVersion;
pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v1::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
/**
* Need to be universally accessible by the ID, and the version is just a couple sprinkles on top
*/
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v1::DatabaseApplications;
use std::collections::HashMap;
impl PartialEq for DownloadableMetadata {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.download_type == other.download_type
}
}
impl Hash for DownloadableMetadata {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.download_type.hash(state);
}
}
#[derive(Serialize, Deserialize)]
enum DatabaseVersionEnum {
V1 { database: v1::Database },
}
pub struct DatabaseVersionSerializable(pub(crate) Database);
impl Serialize for DatabaseVersionSerializable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// Always serialize to latest version
DatabaseVersionEnum::V1 {
database: self.0.clone(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for DatabaseVersionSerializable {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match DatabaseVersionEnum::deserialize(deserializer)? {
DatabaseVersionEnum::V1 { database } => DatabaseVersionSerializable(database),
})
}
}
mod v1 {
use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf};
use crate::platform::Platform;
use super::{Deserialize, Serialize};
fn default_template() -> String {
"{}".to_owned()
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct GameVersion {
pub game_id: String,
pub version_id: String,
pub display_name: Option<String>,
pub version_path: String,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
#[serde(default = "default_template")]
pub launch_template: String,
pub launches: Vec<LaunchConfiguration>,
pub setups: Vec<SetupConfiguration>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LaunchConfiguration {
pub launch_id: String,
pub name: String,
pub command: String,
pub platform: Platform,
pub umu_id_override: Option<String>,
pub executor: Option<LaunchConfigurationExecutor>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
/**
* This is intended to be used to look up the actual launch configuration that we store elsewhere
*/
pub struct LaunchConfigurationExecutor {
pub launch_id: String,
pub game_id: String,
pub version_id: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SetupConfiguration {
pub command: String,
pub platform: Platform,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
pub force_offline: bool, // ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
force_offline: false,
}
}
}
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
PartiallyInstalled {
version_name: String,
install_dir: String,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum ApplicationTransientStatus {
Queued { version_id: String },
Downloading { version_id: String },
Uninstalling {},
Updating { version_id: String },
Validating { version_id: String },
Running {},
}
#[derive(serde::Serialize, Clone, Deserialize)]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
pub client_id: String,
pub web_token: Option<String>,
}
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy,
)]
pub enum DownloadType {
Game,
Tool,
Dlc,
Mod,
}
#[derive(Debug, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata {
pub id: String,
pub version: String,
pub target_platform: Platform,
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(
id: String,
version: String,
target_platform: Platform,
download_type: DownloadType,
) -> Self {
Self {
id,
version,
target_platform,
download_type,
}
}
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, GameVersion>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
}
}
impl Database {
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: String::new(),
auth: None,
settings: Settings::default(),
cache_dir,
}
}
}
impl DatabaseAuth {
pub fn new(
private: String,
cert: String,
client_id: String,
web_token: Option<String>,
) -> Self {
Self {
private,
cert,
client_id,
web_token,
}
}
}
}

View File

@@ -1,20 +0,0 @@
[package]
name = "download_manager"
version = "0.1.0"
edition = "2024"
[dependencies]
async-trait = "0.1.89"
atomic-instant-full = "0.1"
database = { version = "0.1.0", path = "../database" }
futures-util = "0.3.31"
humansize = "2.1.3"
log = "0.4.28"
parking_lot = "0.12.5"
remote = { version = "0.1.0", path = "../remote" }
serde = "1.0.228"
serde_with = "3.15.0"
tauri = "*"
throttle_my_fn = "0.2.6"
tokio = { version = "1.48.0", features = ["sync"] }
utils = { version = "0.1.0", path = "../utils" }

View File

@@ -1,144 +0,0 @@
use std::{
collections::HashMap,
sync::RwLock,
time::{Duration, Instant},
};
use futures_util::StreamExt;
use remote::{
error::RemoteAccessError,
requests::{generate_url, make_authenticated_get},
utils::DROP_CLIENT_ASYNC,
};
use serde::Deserialize;
use tauri::Url;
use crate::util::semaphore::{SyncSemaphore, SyncSemaphorePermit};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DepotManifestContent {
version_id: String,
//compression: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DepotManifest {
content: HashMap<String, Vec<DepotManifestContent>>,
}
struct Depot {
endpoint: String,
manifest: Option<DepotManifest>,
latest_speed: Option<usize>, // bytes per second
current_downloads: SyncSemaphore,
}
pub struct DepotManager {
depots: RwLock<Vec<Depot>>,
}
#[derive(Deserialize)]
struct ServersideDepot {
endpoint: String,
}
const SPEEDTEST_TIMEOUT: Duration = Duration::from_secs(4);
impl DepotManager {
pub fn new() -> Self {
Self {
depots: RwLock::new(Vec::new()),
}
}
pub async fn sync_depots(&self) -> Result<(), RemoteAccessError> {
let depots = make_authenticated_get(generate_url(&["/api/v1/client/depots"], &[])?).await?;
let depots: Vec<ServersideDepot> = depots.json().await?;
let mut new_depots = depots
.into_iter()
.map(|depot| Depot {
endpoint: if depot.endpoint.ends_with("/") {
depot.endpoint
} else {
format!("{}/", depot.endpoint)
},
manifest: None,
latest_speed: None,
current_downloads: SyncSemaphore::new(),
})
.collect::<Vec<Depot>>();
for depot in &mut new_depots {
let manifest_url = Url::parse(&depot.endpoint)?.join("manifest.json")?;
let manifest = DROP_CLIENT_ASYNC.get(manifest_url).send().await?;
let manifest: DepotManifest = manifest.json().await?;
depot.manifest.replace(manifest);
let speedtest_url = Url::parse(&depot.endpoint)?.join("speedtest")?;
let speedtest = DROP_CLIENT_ASYNC.get(speedtest_url).send().await?;
let mut stream = speedtest.bytes_stream();
let start = Instant::now();
let mut total_length = 0;
while let Some(chunk) = stream.next().await {
let length = chunk?.len();
total_length += length;
if SPEEDTEST_TIMEOUT <= start.elapsed() {
break;
}
}
let elapsed = start.elapsed().as_millis() as usize;
let speed = if elapsed == 0 { usize::MAX } else { (total_length / elapsed) * 1000 };
depot.latest_speed.replace(speed);
}
let mut depot_lock = self.depots.write().unwrap();
*depot_lock = new_depots;
Ok(())
}
pub fn next_depot(
&self,
game_id: &str,
version_id: &str,
) -> Result<(String, SyncSemaphorePermit), RemoteAccessError> {
let lock = self.depots.read().unwrap();
let best_depot = lock
.iter()
.filter(|v| {
let manifest = match &v.manifest {
Some(v) => v,
None => return false,
};
let versions = match manifest.content.get(game_id) {
Some(v) => v,
None => return false,
};
let _version = match versions.iter().find(|v| v.version_id == version_id) {
Some(v) => v,
None => return false,
};
true
})
.max_by(|x, y| {
let x_speed = x.latest_speed.unwrap_or(0) / x.current_downloads.permits();
let y_speed = y.latest_speed.unwrap_or(0) / y.current_downloads.permits();
x_speed.cmp(&y_speed)
})
.ok_or(RemoteAccessError::NoDepots)?;
Ok((
best_depot.endpoint.clone(),
best_depot.current_downloads.acquire(),
))
}
}

View File

@@ -1,82 +0,0 @@
use humansize::{BINARY, format_size};
use std::{
fmt::{Display, Formatter},
io,
sync::{Arc, mpsc::SendError},
};
use remote::error::RemoteAccessError;
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum DownloadManagerError<T> {
IOError(io::Error),
SignalError(SendError<T>),
}
impl<T> Display for DownloadManagerError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DownloadManagerError::IOError(error) => write!(f, "{error}"),
DownloadManagerError::SignalError(send_error) => write!(f, "{send_error}"),
}
}
}
impl<T> From<SendError<T>> for DownloadManagerError<T> {
fn from(value: SendError<T>) -> Self {
DownloadManagerError::SignalError(value)
}
}
impl<T> From<io::Error> for DownloadManagerError<T> {
fn from(value: io::Error) -> Self {
DownloadManagerError::IOError(value)
}
}
// TODO: Rename / separate from downloads
#[derive(Debug, SerializeDisplay)]
pub enum ApplicationDownloadError {
NotInitialized,
Communication(RemoteAccessError),
DiskFull(u64, u64),
#[allow(dead_code)]
Checksum,
Lock,
IoError(Arc<io::Error>),
DownloadError(RemoteAccessError),
InvalidCommand,
}
impl Display for ApplicationDownloadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApplicationDownloadError::NotInitialized => {
write!(f, "Download not initalized, did something go wrong?")
}
ApplicationDownloadError::DiskFull(required, available) => write!(
f,
"Game requires {}, {} remaining left on disk.",
format_size(*required, BINARY),
format_size(*available, BINARY),
),
ApplicationDownloadError::Communication(error) => write!(f, "{error}"),
ApplicationDownloadError::Lock => write!(
f,
"failed to acquire lock. Something has gone very wrong internally. Please restart the application"
),
ApplicationDownloadError::Checksum => {
write!(f, "checksum failed to validate for download")
}
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
ApplicationDownloadError::DownloadError(error) => {
write!(f, "Download failed with error {error:?}")
}
ApplicationDownloadError::InvalidCommand => write!(f, "Invalid command state"),
}
}
}
impl From<io::Error> for ApplicationDownloadError {
fn from(value: io::Error) -> Self {
ApplicationDownloadError::IoError(Arc::new(value))
}
}

View File

@@ -1,45 +0,0 @@
#![feature(duration_millis_float)]
#![feature(nonpoison_mutex)]
#![feature(sync_nonpoison)]
use std::{ops::Deref, sync::OnceLock};
use tauri::AppHandle;
use crate::{
download_manager_builder::DownloadManagerBuilder, download_manager_frontend::DownloadManager,
};
pub mod download_manager_builder;
pub mod download_manager_frontend;
pub mod downloadable;
pub mod error;
pub mod frontend_updates;
pub mod util;
pub mod depot_manager;
pub static DOWNLOAD_MANAGER: DownloadManagerWrapper = DownloadManagerWrapper::new();
pub struct DownloadManagerWrapper(OnceLock<DownloadManager>);
impl DownloadManagerWrapper {
const fn new() -> Self {
DownloadManagerWrapper(OnceLock::new())
}
pub fn init(app_handle: AppHandle) {
DOWNLOAD_MANAGER
.0
.set(DownloadManagerBuilder::build(app_handle))
.expect("failed to initialise download manager");
}
}
impl Deref for DownloadManagerWrapper {
type Target = DownloadManager;
fn deref(&self) -> &Self::Target {
match self.0.get() {
Some(download_manager) => download_manager,
None => unreachable!("Download manager should always be initialised"),
}
}
}

View File

@@ -1,28 +0,0 @@
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
pub struct SyncSemaphore {
inner: Arc<AtomicUsize>
}
impl SyncSemaphore {
pub fn new() -> Self {
Self { inner: Arc::new(AtomicUsize::new(0)) }
}
pub fn acquire(&self) -> SyncSemaphorePermit {
self.inner.fetch_add(1, Ordering::Relaxed);
SyncSemaphorePermit(self.inner.clone())
}
pub fn permits(&self) -> usize {
self.inner.fetch_add(0, Ordering::Relaxed)
}
}
pub struct SyncSemaphorePermit(Arc<AtomicUsize>);
impl Drop for SyncSemaphorePermit {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::Relaxed);
}
}

View File

@@ -1,8 +1,7 @@
[package] [package]
name = "utils" name = "drop-consts"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
log = "0.4.28" dirs = "6.0.0"
webbrowser = "1.0.5"

View File

@@ -0,0 +1,15 @@
use std::{
path::PathBuf,
sync::{Arc, LazyLock},
};
#[cfg(not(debug_assertions))]
static DATA_ROOT_PREFIX: &'static str = "drop";
#[cfg(debug_assertions)]
static DATA_ROOT_PREFIX: &str = "drop-debug";
pub static DATA_ROOT_DIR: LazyLock<&'static PathBuf> =
LazyLock::new(|| Box::leak(Box::new(dirs::data_dir().unwrap().join(DATA_ROOT_PREFIX))));
pub static CACHE_DIR: LazyLock<&'static PathBuf> =
LazyLock::new(|| Box::leak(Box::new(DATA_ROOT_DIR.join("cache"))));

View File

@@ -0,0 +1,21 @@
[package]
name = "drop-database"
version = "0.1.0"
edition = "2024"
[dependencies]
bitcode = "0.6.7"
chrono = "0.4.42"
drop-consts = { path = "../drop-consts" }
drop-library = { path = "../drop-library" }
drop-native-library = { path = "../drop-native-library" }
log = "0.4.28"
native_model = { git = "https://github.com/Drop-OSS/native_model.git", version = "0.6.4", features = [
"rmp_serde_1_3",
] }
rustbreak = "2.0.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_with = "3.14.0"
url = "2.5.7"
whoami = "1.6.1"

View File

@@ -0,0 +1,140 @@
use std::{
fs::{self, create_dir_all},
mem::ManuallyDrop,
ops::{Deref, DerefMut},
path::PathBuf,
sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard},
};
use chrono::Utc;
use drop_consts::DATA_ROOT_DIR;
use log::{debug, error, info, warn};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{Serialize, de::DeserializeOwned};
use crate::DB;
use super::models::data::Database;
// Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)]
pub struct DropDatabaseSerializer;
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer
{
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
}
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
let mut buf = Vec::new();
s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Internal(e.to_string()))?;
let (val, _version) =
native_model::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val)
}
}
pub type DatabaseInterface =
rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer>;
pub trait DatabaseImpls {
fn set_up_database() -> DatabaseInterface;
}
impl DatabaseImpls for DatabaseInterface {
fn set_up_database() -> DatabaseInterface {
let db_path = DATA_ROOT_DIR.join("drop.db");
let games_base_dir = DATA_ROOT_DIR.join("games");
let logs_root_dir = DATA_ROOT_DIR.join("logs");
let cache_dir = DATA_ROOT_DIR.join("cache");
let pfx_dir = DATA_ROOT_DIR.join("pfx");
debug!("creating data directory at {DATA_ROOT_DIR:?}");
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap();
create_dir_all(&games_base_dir).unwrap();
create_dir_all(&logs_root_dir).unwrap();
create_dir_all(&cache_dir).unwrap();
create_dir_all(&pfx_dir).unwrap();
let exists = fs::exists(db_path.clone()).unwrap();
if exists {
match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
}
} else {
let default = Database::new(games_base_dir, None);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
}
}
}
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
fn handle_invalid_database(
_e: RustbreakError,
db_path: PathBuf,
games_base_dir: PathBuf,
cache_dir: PathBuf,
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
warn!("{_e}");
let new_path = {
let time = Utc::now().timestamp();
let mut base = db_path.clone();
base.set_file_name(format!("drop.db.backup-{time}"));
base
};
info!("old database stored at: {}", new_path.to_string_lossy());
fs::rename(&db_path, &new_path).unwrap();
let db = Database::new(
games_base_dir.into_os_string().into_string().unwrap(),
Some(new_path),
);
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
}
// To automatically save the database upon drop
pub struct DBRead<'a>(pub(crate) RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(pub(crate) ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Drop for DBWrite<'_> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(()) => {}
Err(e) => {
error!("database failed to save with error {e}");
panic!("database failed to save with error {e}")
}
}
}
}

View File

@@ -1,23 +1,17 @@
use std::{ use std::{
collections::HashMap, collections::HashMap, fs::File, io::{self, Read, Write}, path::{Path, PathBuf}
fs::File,
io::{self, Read, Write},
path::{Path, PathBuf},
}; };
use database::platform::Platform;
use log::error; use log::error;
use native_model::{Decode, Encode}; use native_model::{Decode, Encode};
use utils::lock;
pub type DropData = v1::DropData; pub type DropData = v1::DropData;
pub static DROPDATA_PATH: &str = ".dropdata"; pub static DROP_DATA_PATH: &str = ".dropdata";
pub mod v1 { mod v1 {
use std::{collections::HashMap, path::PathBuf, sync::Mutex}; use std::{collections::HashMap, path::PathBuf, sync::Mutex};
use database::platform::Platform;
use native_model::native_model; use native_model::native_model;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -26,18 +20,16 @@ pub mod v1 {
pub struct DropData { pub struct DropData {
pub game_id: String, pub game_id: String,
pub game_version: String, pub game_version: String,
pub target_platform: Platform,
pub contexts: Mutex<HashMap<String, bool>>, pub contexts: Mutex<HashMap<String, bool>>,
pub base_path: PathBuf, pub base_path: PathBuf,
} }
impl DropData { impl DropData {
pub fn new(game_id: String, game_version: String, target_platform: Platform, base_path: PathBuf) -> Self { pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
Self { Self {
base_path, base_path,
game_id, game_id,
game_version, game_version,
target_platform,
contexts: Mutex::new(HashMap::new()), contexts: Mutex::new(HashMap::new()),
} }
} }
@@ -45,24 +37,19 @@ pub mod v1 {
} }
impl DropData { impl DropData {
pub fn generate(game_id: String, game_version: String, target_platform: Platform, base_path: PathBuf) -> Self { pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
match DropData::read(&base_path) { match DropData::read(&base_path) {
Ok(v) => v, Ok(v) => v,
Err(_) => DropData::new(game_id, game_version, target_platform, base_path), Err(_) => DropData::new(game_id, game_version, base_path),
} }
} }
pub fn read(base_path: &Path) -> Result<Self, io::Error> { pub fn read(base_path: &Path) -> Result<Self, io::Error> {
let mut file = File::open(base_path.join(DROPDATA_PATH))?; let mut file = File::open(base_path.join(DROP_DATA_PATH))?;
let mut s = Vec::new(); let mut s = Vec::new();
file.read_to_end(&mut s)?; file.read_to_end(&mut s)?;
native_model::rmp_serde_1_3::RmpSerde::decode(s).map_err(|e| { Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap())
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to decode drop data: {e}"),
)
})
} }
pub fn write(&self) { pub fn write(&self) {
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) { let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
@@ -70,7 +57,7 @@ impl DropData {
Err(_) => return, Err(_) => return,
}; };
let mut file = match File::create(self.base_path.join(DROPDATA_PATH)) { let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
Ok(file) => file, Ok(file) => file,
Err(e) => { Err(e) => {
error!("{e}"); error!("{e}");
@@ -84,15 +71,12 @@ impl DropData {
} }
} }
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) { pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
*lock!(self.contexts) = completed_contexts *self.contexts.lock().unwrap() = completed_contexts.iter().map(|s| (s.0.clone(), s.1)).collect();
.iter()
.map(|s| (s.0.clone(), s.1))
.collect();
} }
pub fn set_context(&self, context: String, state: bool) { pub fn set_context(&self, context: String, state: bool) {
lock!(self.contexts).entry(context).insert_entry(state); self.contexts.lock().unwrap().entry(context).insert_entry(state);
} }
pub fn get_contexts(&self) -> HashMap<String, bool> { pub fn get_contexts(&self) -> HashMap<String, bool> {
lock!(self.contexts).clone() self.contexts.lock().unwrap().clone()
} }
} }

View File

@@ -0,0 +1,34 @@
use std::{mem::ManuallyDrop, sync::LazyLock};
use log::error;
use crate::db::{DBRead, DBWrite, DatabaseImpls, DatabaseInterface};
pub mod db;
pub mod debug;
pub mod models;
pub mod process;
pub mod runtime_models;
pub mod drop_data;
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() {
Ok(data) => DBRead(data),
Err(e) => {
error!("database borrow failed with error {e}");
panic!("database borrow failed with error {e}");
}
}
}
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
match DB.borrow_data_mut() {
Ok(data) => DBWrite(ManuallyDrop::new(data)),
Err(e) => {
error!("database borrow mut failed with error {e}");
panic!("database borrow mut failed with error {e}");
}
}
}

View File

@@ -0,0 +1,404 @@
pub mod data {
use std::{hash::Hash, path::PathBuf};
use native_model::native_model;
use serde::{Deserialize, Serialize};
// NOTE: Within each version, you should NEVER use these types.
// Declare it using the actual version that it is from, i.e. v1::Settings rather than just Settings from here
pub type GameVersion = v1::GameVersion;
pub type Database = v4::Database;
pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v2::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
/**
* Need to be universally accessible by the ID, and the version is just a couple sprinkles on top
*/
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v4::DatabaseApplications;
// pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::collections::HashMap;
impl PartialEq for DownloadableMetadata {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.download_type == other.download_type
}
}
impl Hash for DownloadableMetadata {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.download_type.hash(state);
}
}
mod v1 {
use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf};
use crate::process::Platform;
use super::{Deserialize, Serialize, native_model};
fn default_template() -> String {
"{}".to_owned()
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 2, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct GameVersion {
pub game_id: String,
pub version_name: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
#[serde(default = "default_template")]
pub launch_command_template: String,
pub setup_command: String,
pub setup_args: Vec<String>,
#[serde(default = "default_template")]
pub setup_command_template: String,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 4, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
pub force_offline: bool, // ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
force_offline: false,
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum ApplicationTransientStatus {
Queued { version_name: String },
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Validating { version_name: String },
Running {},
}
#[derive(serde::Serialize, Clone, Deserialize)]
#[native_model(id = 6, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
pub client_id: String,
pub web_token: Option<String>,
}
#[native_model(id = 8, version = 1)]
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy,
)]
pub enum DownloadType {
Game,
Tool,
Dlc,
Mod,
}
#[native_model(id = 7, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Debug, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata {
pub id: String,
pub version: Option<String>,
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self {
id,
version,
download_type,
}
}
}
#[native_model(id = 1, version = 1)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
}
}
mod v2 {
use std::{collections::HashMap, path::PathBuf};
use serde_with::serde_as;
use crate::runtime_models::Game;
use super::{Deserialize, Serialize, native_model, v1};
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::Database)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: v1::Settings,
pub auth: Option<v1::DatabaseAuth>,
pub base_url: String,
pub applications: v1::DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
pub umu_installed: bool,
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications,
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: None,
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::GameDownloadStatus)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
PartiallyInstalled {
version_name: String,
install_dir: String,
},
}
impl From<v1::GameDownloadStatus> for GameDownloadStatus {
fn from(value: v1::GameDownloadStatus) -> Self {
match value {
v1::GameDownloadStatus::Remote {} => Self::Remote {},
v1::GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Self::SetupRequired {
version_name,
install_dir,
},
v1::GameDownloadStatus::Installed {
version_name,
install_dir,
} => Self::Installed {
version_name,
install_dir,
},
}
}
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from=v1::DatabaseApplications)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, v1::GameVersion>>,
pub installed_game_version: HashMap<String, v1::DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses:
HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
}
impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self {
Self {
game_statuses: value
.game_statuses
.into_iter()
.map(|x| (x.0, x.1.into()))
.collect::<HashMap<String, GameDownloadStatus>>(),
install_dirs: value.install_dirs,
game_versions: value.game_versions,
installed_game_version: value.installed_game_version,
transient_statuses: value.transient_statuses,
}
}
}
}
mod v3 {
use std::path::PathBuf;
use super::{Deserialize, Serialize, native_model, v1, v2};
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde, from = v2::Database)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: v1::Settings,
pub auth: Option<v1::DatabaseAuth>,
pub base_url: String,
pub applications: v2::DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<v2::DatabaseCompatInfo>,
}
impl From<v2::Database> for Database {
fn from(value: v2::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications.into(),
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: None,
}
}
}
}
mod v4 {
use std::{collections::HashMap, path::PathBuf};
use drop_library::libraries::LibraryProviderIdentifier;
use drop_native_library::impls::DropNativeLibraryProvider;
use serde_with::serde_as;
use crate::models::data::v3;
use super::{Deserialize, Serialize, native_model, v1, v2};
#[derive(Serialize, Deserialize, Clone)]
pub enum Library {
NativeLibrary(DropNativeLibraryProvider),
}
#[serde_as]
#[derive(Serialize, Deserialize, Default, Clone)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 4, with = native_model::rmp_serde_1_3::RmpSerde, from=v2::DatabaseApplications)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
pub libraries: HashMap<LibraryProviderIdentifier, Library>,
#[serde(skip)]
pub transient_statuses:
HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
}
impl From<v2::DatabaseApplications> for DatabaseApplications {
fn from(value: v2::DatabaseApplications) -> Self {
todo!()
}
}
#[native_model(id = 1, version = 4, with = native_model::rmp_serde_1_3::RmpSerde, from = v3::Database)]
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct Database {
#[serde(default)]
pub settings: v1::Settings,
pub drop_applications: DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
}
impl From<v3::Database> for Database {
fn from(value: v3::Database) -> Self {
Database {
settings: value.settings,
drop_applications: value.applications.into(),
prev_database: value.prev_database,
}
}
}
}
impl Database {
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
) -> Self {
Self {
drop_applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
libraries: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
settings: Settings::default(),
}
}
}
}

View File

@@ -1,24 +1,23 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, Ord)] #[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
pub enum Platform { pub enum Platform {
Windows, Windows,
Linux, Linux,
#[allow(non_camel_case_types)] MacOs,
macOS,
} }
impl Platform { impl Platform {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub const HOST: Platform = Self::Windows; pub const HOST: Platform = Self::Windows;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub const HOST: Platform = Self::macOS; pub const HOST: Platform = Self::MacOs;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub const HOST: Platform = Self::Linux; pub const HOST: Platform = Self::Linux;
pub fn is_case_sensitive(&self) -> bool { pub fn is_case_sensitive(&self) -> bool {
match self { match self {
Self::Windows | Self::macOS => false, Self::Windows | Self::MacOs => false,
Self::Linux => true, Self::Linux => true,
} }
} }
@@ -29,7 +28,7 @@ impl From<&str> for Platform {
match value.to_lowercase().trim() { match value.to_lowercase().trim() {
"windows" => Self::Windows, "windows" => Self::Windows,
"linux" => Self::Linux, "linux" => Self::Linux,
"mac" | "macos" => Self::macOS, "mac" | "macos" => Self::MacOs,
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
@@ -40,8 +39,8 @@ impl From<whoami::Platform> for Platform {
match value { match value {
whoami::Platform::Windows => Platform::Windows, whoami::Platform::Windows => Platform::Windows,
whoami::Platform::Linux => Platform::Linux, whoami::Platform::Linux => Platform::Linux,
whoami::Platform::MacOS => Platform::macOS, whoami::Platform::MacOS => Platform::MacOs,
platform => unimplemented!("Playform {} is not supported", platform), _ => unimplemented!(),
} }
} }
} }

View File

@@ -0,0 +1,28 @@
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct Game {
pub id: String,
m_name: String,
m_short_description: String,
m_description: String,
// mDevelopers
// mPublishers
m_icon_object_id: String,
m_banner_object_id: String,
m_cover_object_id: String,
m_image_library_object_ids: Vec<String>,
m_image_carousel_object_ids: Vec<String>,
}
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct User {
id: String,
username: String,
admin: bool,
display_name: String,
profile_picture_object_id: String,
}

View File

@@ -0,0 +1,16 @@
[package]
name = "drop-downloads"
version = "0.1.0"
edition = "2024"
[dependencies]
atomic-instant-full = "0.1.0"
drop-database = { path = "../drop-database" }
drop-errors = { path = "../drop-errors" }
# can't depend, cycle
# drop-native-library = { path = "../drop-native-library" }
log = "0.4.22"
parking_lot = "0.12.4"
serde = "1.0.219"
tauri = { version = "2.7.0" }
throttle_my_fn = "0.2.6"

View File

@@ -1,22 +1,19 @@
use core::panic;
use std::{ use std::{
collections::HashMap, collections::HashMap,
sync::{Arc, Mutex}, sync::{
time::Duration, Arc, Mutex,
mpsc::{Receiver, Sender, channel},
},
thread::{JoinHandle, spawn},
}; };
use database::DownloadableMetadata; use drop_database::models::data::DownloadableMetadata;
use drop_errors::application_download_error::ApplicationDownloadError;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use tauri::{AppHandle, async_runtime::JoinHandle}; use tauri::{AppHandle, Emitter};
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::{sync::mpsc, time::timeout};
use utils::{app_emit, lock, send};
use crate::{ use crate::{
depot_manager::DepotManager, download_manager_frontend::DownloadStatus, events::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}
download_manager_frontend::DownloadStatus,
error::ApplicationDownloadError,
frontend_updates::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
}; };
use super::{ use super::{
@@ -32,43 +29,6 @@ use super::{
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>; pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>; pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
/*
Welcome to the download manager, the most overengineered, glorious piece of bullshit.
The download manager takes a queue of ids and their associated
DownloadAgents, and then, one-by-one, executes them. It provides an interface
to interact with the currently downloading agent, and manage the queue.
When the DownloadManager is initialised, it is designed to provide a reference
which can be used to provide some instructions (the DownloadManagerInterface),
but other than that, it runs without any sort of interruptions.
It does this by opening up two data structures. Primarily is the command_receiver,
and mpsc (multi-channel-single-producer) which allows commands to be sent from
the Interface, and queued up for the Manager to process.
These have been mapped in the DownloadManagerSignal docs.
The other way to interact with the DownloadManager is via the donwload_queue,
which is just a collection of ids which may be rearranged to suit
whichever download queue order is required.
+----------------------------------------------------------------------------+
| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
| WHICH HAS NOT BEEN ACCOUNTED FOR |
+----------------------------------------------------------------------------+
This download queue does not actually own any of the DownloadAgents. It is
simply an id-based reference system. The actual Agents are stored in the
download_agent_registry HashMap, as ordering is no issue here. This is why
appending or removing from the download_queue must be done via signals.
Behold, my madness - quexeky
*/
pub struct DownloadManagerBuilder { pub struct DownloadManagerBuilder {
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>, download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
download_queue: Queue, download_queue: Queue,
@@ -84,11 +44,10 @@ pub struct DownloadManagerBuilder {
impl DownloadManagerBuilder { impl DownloadManagerBuilder {
pub fn build(app_handle: AppHandle) -> DownloadManager { pub fn build(app_handle: AppHandle) -> DownloadManager {
let queue = Queue::new(); let queue = Queue::new();
let (command_sender, command_receiver) = mpsc::channel(1500); let (command_sender, command_receiver) = channel();
let active_progress = Arc::new(Mutex::new(None)); let active_progress = Arc::new(Mutex::new(None));
let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty)); let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
let depot_manager = Arc::new(DepotManager::new());
let manager = Self { let manager = Self {
download_agent_registry: HashMap::new(), download_agent_registry: HashMap::new(),
download_queue: queue.clone(), download_queue: queue.clone(),
@@ -102,87 +61,74 @@ impl DownloadManagerBuilder {
active_control_flag: None, active_control_flag: None,
}; };
let terminator = tauri::async_runtime::spawn(async move { let terminator = spawn(|| manager.manage_queue());
let result = manager.manage_queue().await;
info!("download manager exited with result: {:?}", result);
});
DownloadManager::new(terminator, queue, active_progress, command_sender, depot_manager) DownloadManager::new(terminator, queue, active_progress, command_sender)
} }
fn set_status(&self, status: DownloadManagerStatus) { fn set_status(&self, status: DownloadManagerStatus) {
*lock!(self.status) = status; *self.status.lock().unwrap() = status;
} }
async fn remove_and_cleanup_front_download( fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
&mut self,
meta: &DownloadableMetadata,
) -> DownloadAgent {
self.download_queue.pop_front(); self.download_queue.pop_front();
let download_agent = self.download_agent_registry.remove(meta).unwrap(); let download_agent = self.download_agent_registry.remove(meta).unwrap();
self.cleanup_current_download().await; self.cleanup_current_download();
download_agent download_agent
} }
// CAREFUL WITH THIS FUNCTION // CAREFUL WITH THIS FUNCTION
// Make sure the download thread is terminated // Make sure the download thread is terminated
async fn cleanup_current_download(&mut self) { fn cleanup_current_download(&mut self) {
self.active_control_flag = None; self.active_control_flag = None;
*lock!(self.progress) = None; *self.progress.lock().unwrap() = None;
if let Some(unfinished_thread) = { let mut download_thread_lock = self.current_download_thread.lock().unwrap();
let mut download_thread_lock = lock!(self.current_download_thread);
download_thread_lock.take() if let Some(unfinished_thread) = download_thread_lock.take()
} { && !unfinished_thread.is_finished()
let _ = unfinished_thread.await; {
unfinished_thread.join().unwrap();
} }
drop(download_thread_lock);
} }
async fn stop_and_wait_current_download(&self) -> bool { fn stop_and_wait_current_download(&self) -> bool {
self.set_status(DownloadManagerStatus::Paused); self.set_status(DownloadManagerStatus::Paused);
if let Some(current_flag) = &self.active_control_flag { if let Some(current_flag) = &self.active_control_flag {
current_flag.set(DownloadThreadControlFlag::Stop); current_flag.set(DownloadThreadControlFlag::Stop);
if let Some(current_download_thread) = {
let mut download_thread_lock = lock!(self.current_download_thread);
download_thread_lock.take()
} {
let result = timeout(Duration::from_secs(4), async {
current_download_thread.await.is_ok()
})
.await;
if let Ok(result) = result {
return result;
};
panic!("failed to cleanup download: timeout after 4 seconds");
};
} }
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
if let Some(current_download_thread) = download_thread_lock.take() {
return current_download_thread.join().is_ok();
};
true true
} }
async fn manage_queue(mut self) -> Result<(), ()> { fn manage_queue(mut self) -> Result<(), ()> {
loop { loop {
let signal = match self.command_receiver.recv().await { let signal = match self.command_receiver.recv() {
Some(signal) => signal, Ok(signal) => signal,
None => return Err(()), Err(_) => return Err(()),
}; };
match signal { match signal {
DownloadManagerSignal::Go => { DownloadManagerSignal::Go => {
self.manage_go_signal().await; self.manage_go_signal();
} }
DownloadManagerSignal::Stop => { DownloadManagerSignal::Stop => {
self.manage_stop_signal(); self.manage_stop_signal();
} }
DownloadManagerSignal::Completed(meta) => { DownloadManagerSignal::Completed(meta) => {
self.manage_completed_signal(meta).await; self.manage_completed_signal(meta);
} }
DownloadManagerSignal::Queue(download_agent) => { DownloadManagerSignal::Queue(download_agent) => {
self.manage_queue_signal(download_agent).await; self.manage_queue_signal(download_agent);
} }
DownloadManagerSignal::Error(e) => { DownloadManagerSignal::Error(e) => {
self.manage_error_signal(e).await; self.manage_error_signal(e);
} }
DownloadManagerSignal::UpdateUIQueue => { DownloadManagerSignal::UpdateUIQueue => {
self.push_ui_queue_update(); self.push_ui_queue_update();
@@ -191,16 +137,16 @@ impl DownloadManagerBuilder {
self.push_ui_stats_update(kbs, time); self.push_ui_stats_update(kbs, time);
} }
DownloadManagerSignal::Finish => { DownloadManagerSignal::Finish => {
self.stop_and_wait_current_download().await; self.stop_and_wait_current_download();
return Ok(()); return Ok(());
} }
DownloadManagerSignal::Cancel(meta) => { DownloadManagerSignal::Cancel(meta) => {
self.manage_cancel_signal(&meta).await; self.manage_cancel_signal(&meta);
} }
} }
} }
} }
async fn manage_queue_signal(&mut self, download_agent: DownloadAgent) { fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
debug!("got signal Queue"); debug!("got signal Queue");
let meta = download_agent.metadata(); let meta = download_agent.metadata();
@@ -215,10 +161,12 @@ impl DownloadManagerBuilder {
self.download_queue.append(meta.clone()); self.download_queue.append(meta.clone());
self.download_agent_registry.insert(meta, download_agent); self.download_agent_registry.insert(meta, download_agent);
send!(self.sender, DownloadManagerSignal::UpdateUIQueue); self.sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
} }
async fn manage_go_signal(&mut self) { fn manage_go_signal(&mut self) {
debug!("got signal Go"); debug!("got signal Go");
if self.download_agent_registry.is_empty() { if self.download_agent_registry.is_empty() {
debug!( debug!(
@@ -261,22 +209,21 @@ impl DownloadManagerBuilder {
let sender = self.sender.clone(); let sender = self.sender.clone();
let mut download_thread_lock = lock!(self.current_download_thread); let mut download_thread_lock = self.current_download_thread.lock().unwrap();
let app_handle = self.app_handle.clone(); let app_handle = self.app_handle.clone();
*download_thread_lock = Some(tauri::async_runtime::spawn(async move { *download_thread_lock = Some(spawn(move || {
loop { loop {
let download_result = let download_result = match download_agent.download(&app_handle) {
match download_agent.download(&app_handle).await { // Ok(true) is for completed and exited properly
// Ok(true) is for completed and exited properly Ok(v) => v,
Ok(v) => v, Err(e) => {
Err(e) => { error!("download {:?} has error {}", download_agent.metadata(), &e);
error!("download {:?} has error {}", download_agent.metadata(), &e); download_agent.on_error(&app_handle, &e);
download_agent.on_error(&app_handle, &e); sender.send(DownloadManagerSignal::Error(e)).unwrap();
send!(sender, DownloadManagerSignal::Error(e)); return;
return; }
} };
};
// If the download gets canceled // If the download gets canceled
// immediately return, on_cancelled gets called for us earlier // immediately return, on_cancelled gets called for us earlier
@@ -297,7 +244,7 @@ impl DownloadManagerBuilder {
&e &e
); );
download_agent.on_error(&app_handle, &e); download_agent.on_error(&app_handle, &e);
send!(sender, DownloadManagerSignal::Error(e)); sender.send(DownloadManagerSignal::Error(e)).unwrap();
return; return;
} }
}; };
@@ -307,12 +254,11 @@ impl DownloadManagerBuilder {
} }
if validate_result { if validate_result {
download_agent.on_complete(&app_handle).await; download_agent.on_complete(&app_handle);
send!( sender
sender, .send(DownloadManagerSignal::Completed(download_agent.metadata()))
DownloadManagerSignal::Completed(download_agent.metadata()) .unwrap();
); sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
send!(sender, DownloadManagerSignal::UpdateUIQueue);
return; return;
} }
} }
@@ -323,35 +269,40 @@ impl DownloadManagerBuilder {
active_control_flag.set(DownloadThreadControlFlag::Go); active_control_flag.set(DownloadThreadControlFlag::Go);
} }
fn manage_stop_signal(&mut self) { fn manage_stop_signal(&mut self) {
debug!("got signal Stop");
if let Some(active_control_flag) = self.active_control_flag.clone() { if let Some(active_control_flag) = self.active_control_flag.clone() {
self.set_status(DownloadManagerStatus::Paused); self.set_status(DownloadManagerStatus::Paused);
active_control_flag.set(DownloadThreadControlFlag::Stop); active_control_flag.set(DownloadThreadControlFlag::Stop);
} }
} }
async fn manage_completed_signal(&mut self, meta: DownloadableMetadata) { fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
debug!("got signal Completed");
if let Some(interface) = self.download_queue.read().front() if let Some(interface) = self.download_queue.read().front()
&& interface == &meta && interface == &meta
{ {
self.remove_and_cleanup_front_download(&meta).await; self.remove_and_cleanup_front_download(&meta);
} }
self.push_ui_queue_update(); self.push_ui_queue_update();
send!(self.sender, DownloadManagerSignal::Go); self.sender.send(DownloadManagerSignal::Go).unwrap();
} }
async fn manage_error_signal(&mut self, error: ApplicationDownloadError) { fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
info!("got signal Error"); debug!("got signal Error");
if let Some(metadata) = self.download_queue.read().front() if let Some(metadata) = self.download_queue.read().front()
&& let Some(current_agent) = self.download_agent_registry.get(metadata) && let Some(current_agent) = self.download_agent_registry.get(metadata)
{ {
current_agent.on_error(&self.app_handle, &error); current_agent.on_error(&self.app_handle, &error);
self.stop_and_wait_current_download().await; self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(metadata).await; self.remove_and_cleanup_front_download(metadata);
} }
self.push_ui_queue_update(); self.push_ui_queue_update();
self.set_status(DownloadManagerStatus::Error); self.set_status(DownloadManagerStatus::Error);
} }
async fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) { fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
debug!("got signal Cancel");
// If the current download is the one we're tryna cancel // If the current download is the one we're tryna cancel
if let Some(current_metadata) = self.download_queue.read().front() if let Some(current_metadata) = self.download_queue.read().front()
&& current_metadata == meta && current_metadata == meta
@@ -359,20 +310,20 @@ impl DownloadManagerBuilder {
{ {
self.set_status(DownloadManagerStatus::Paused); self.set_status(DownloadManagerStatus::Paused);
current_download.on_cancelled(&self.app_handle); current_download.on_cancelled(&self.app_handle);
self.stop_and_wait_current_download().await; self.stop_and_wait_current_download();
self.set_status(DownloadManagerStatus::Empty);
self.download_queue.pop_front(); self.download_queue.pop_front();
self.cleanup_current_download().await; self.cleanup_current_download();
self.download_agent_registry.remove(meta); self.download_agent_registry.remove(meta);
debug!("current download queue: {:?}", self.download_queue.read());
} }
// else just cancel it // else just cancel it
else if let Some(download_agent) = self.download_agent_registry.get(meta) { else if let Some(download_agent) = self.download_agent_registry.get(meta) {
let index = self.download_queue.get_by_meta(meta); let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index { if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle); download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index); let _ = self.download_queue.edit().remove(index).unwrap();
let removed = self.download_agent_registry.remove(meta); let removed = self.download_agent_registry.remove(meta);
debug!( debug!(
"removed {:?} from queue {:?}", "removed {:?} from queue {:?}",
@@ -381,13 +332,13 @@ impl DownloadManagerBuilder {
); );
} }
} }
self.sender.send(DownloadManagerSignal::Go).unwrap();
self.push_ui_queue_update(); self.push_ui_queue_update();
send!(self.sender, DownloadManagerSignal::Go);
} }
fn push_ui_stats_update(&self, kbs: usize, time: usize) { fn push_ui_stats_update(&self, kbs: usize, time: usize) {
let event_data = StatsUpdateEvent { speed: kbs, time }; let event_data = StatsUpdateEvent { speed: kbs, time };
app_emit!(&self.app_handle, "update_stats", event_data); self.app_handle.emit("update_stats", event_data).unwrap();
} }
fn push_ui_queue_update(&self) { fn push_ui_queue_update(&self) {
let queue = &self.download_queue.read(); let queue = &self.download_queue.read();
@@ -406,6 +357,6 @@ impl DownloadManagerBuilder {
.collect(); .collect();
let event_data = QueueUpdateEvent { queue: queue_objs }; let event_data = QueueUpdateEvent { queue: queue_objs };
app_emit!(&self.app_handle, "update_queue", event_data); self.app_handle.emit("update_queue", event_data).unwrap();
} }
} }

View File

@@ -1,25 +1,24 @@
use std::{ use std::{
any::Any,
collections::VecDeque, collections::VecDeque,
fmt::Debug, fmt::Debug,
sync::{Arc, Mutex, MutexGuard}, sync::{
mpsc::{SendError, Sender},
Mutex, MutexGuard,
},
thread::JoinHandle,
}; };
use database::DownloadableMetadata; use drop_database::models::data::DownloadableMetadata;
use drop_errors::application_download_error::ApplicationDownloadError;
use log::{debug, info}; use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use tauri::async_runtime::JoinHandle;
use tokio::sync::mpsc::Sender;
use tokio::sync::mpsc::error::SendError;
use utils::{lock, send};
use crate::{depot_manager::DepotManager, error::ApplicationDownloadError};
use super::{ use super::{
download_manager_builder::{CurrentProgressObject, DownloadAgent}, download_manager_builder::{CurrentProgressObject, DownloadAgent},
util::queue::Queue, util::queue::Queue,
}; };
#[derive(Debug)]
pub enum DownloadManagerSignal { pub enum DownloadManagerSignal {
/// Resumes (or starts) the `DownloadManager` /// Resumes (or starts) the `DownloadManager`
Go, Go,
@@ -79,46 +78,36 @@ pub enum DownloadStatus {
/// which provides raw access to the underlying queue. /// which provides raw access to the underlying queue.
/// THIS EDITING IS BLOCKING!!! /// THIS EDITING IS BLOCKING!!!
pub struct DownloadManager { pub struct DownloadManager {
terminator: Mutex<Option<JoinHandle<()>>>, terminator: Mutex<Option<JoinHandle<Result<(), ()>>>>,
download_queue: Queue, download_queue: Queue,
progress: CurrentProgressObject, progress: CurrentProgressObject,
command_sender: Sender<DownloadManagerSignal>, command_sender: Sender<DownloadManagerSignal>,
depot_manager: Arc<DepotManager>,
}
impl Debug for DownloadManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DownloadManager").finish()
}
} }
#[allow(dead_code)] #[allow(dead_code)]
impl DownloadManager { impl DownloadManager {
pub fn new( pub fn new(
terminator: JoinHandle<()>, terminator: JoinHandle<Result<(), ()>>,
download_queue: Queue, download_queue: Queue,
progress: CurrentProgressObject, progress: CurrentProgressObject,
command_sender: Sender<DownloadManagerSignal>, command_sender: Sender<DownloadManagerSignal>,
depot_manager: Arc<DepotManager>,
) -> Self { ) -> Self {
Self { Self {
terminator: Mutex::new(Some(terminator)), terminator: Mutex::new(Some(terminator)),
download_queue, download_queue,
progress, progress,
command_sender, command_sender,
depot_manager
} }
} }
pub async fn queue_download( pub fn queue_download(
&self, &self,
download: DownloadAgent, download: DownloadAgent,
) -> Result<(), SendError<DownloadManagerSignal>> { ) -> Result<(), SendError<DownloadManagerSignal>> {
info!("creating download with meta {:?}", download.metadata()); info!("creating download with meta {:?}", download.metadata());
self.command_sender self.command_sender
.send(DownloadManagerSignal::Queue(download)) .send(DownloadManagerSignal::Queue(download))?;
.await?; self.command_sender.send(DownloadManagerSignal::Go)
self.command_sender.send(DownloadManagerSignal::Go).await
} }
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> { pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
self.download_queue.edit() self.download_queue.edit()
@@ -127,63 +116,68 @@ impl DownloadManager {
self.download_queue.read() self.download_queue.read()
} }
pub fn get_current_download_progress(&self) -> Option<f64> { pub fn get_current_download_progress(&self) -> Option<f64> {
let progress_object = (*lock!(self.progress)).clone()?; let progress_object = (*self.progress.lock().unwrap()).clone()?;
Some(progress_object.get_progress()) Some(progress_object.get_progress())
} }
pub async fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) { pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
let mut queue = self.edit(); let mut queue = self.edit();
let current_index = let current_index = get_index_from_id(&mut queue, meta).unwrap();
get_index_from_id(&mut queue, meta).expect("Failed to get meta index from id"); let to_move = queue.remove(current_index).unwrap();
let to_move = queue
.remove(current_index)
.expect("Failed to remove meta at index from queue");
queue.insert(new_index, to_move); queue.insert(new_index, to_move);
send!(self.command_sender, DownloadManagerSignal::UpdateUIQueue); self.command_sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
} }
pub async fn cancel(&self, meta: DownloadableMetadata) { pub fn cancel(&self, meta: DownloadableMetadata) {
send!(self.command_sender, DownloadManagerSignal::Cancel(meta)); self.command_sender
.send(DownloadManagerSignal::Cancel(meta))
.unwrap();
} }
pub async fn rearrange(&self, current_index: usize, new_index: usize) { pub fn rearrange(&self, current_index: usize, new_index: usize) {
if current_index == new_index { if current_index == new_index {
return; return;
} }
let needs_pause = current_index == 0 || new_index == 0; let needs_pause = current_index == 0 || new_index == 0;
if needs_pause { if needs_pause {
send!(self.command_sender, DownloadManagerSignal::Stop); self.command_sender
.send(DownloadManagerSignal::Stop)
.unwrap();
} }
debug!("moving download at index {current_index} to index {new_index}"); debug!("moving download at index {current_index} to index {new_index}");
{ let mut queue = self.edit();
let mut queue = self.edit(); let to_move = queue.remove(current_index).unwrap();
let to_move = queue.remove(current_index).expect("Failed to get"); queue.insert(new_index, to_move);
queue.insert(new_index, to_move); drop(queue);
}
if needs_pause { if needs_pause {
send!(self.command_sender, DownloadManagerSignal::Go); self.command_sender.send(DownloadManagerSignal::Go).unwrap();
} }
send!(self.command_sender, DownloadManagerSignal::UpdateUIQueue); self.command_sender
send!(self.command_sender, DownloadManagerSignal::Go); .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
} }
pub async fn pause_downloads(&self) { pub fn pause_downloads(&self) {
send!(self.command_sender, DownloadManagerSignal::Stop); self.command_sender
.send(DownloadManagerSignal::Stop)
.unwrap();
} }
pub async fn resume_downloads(&self) { pub fn resume_downloads(&self) {
send!(self.command_sender, DownloadManagerSignal::Go); self.command_sender.send(DownloadManagerSignal::Go).unwrap();
} }
pub async fn ensure_terminated(&self) -> Result<(), tauri::Error> { pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
send!(self.command_sender, DownloadManagerSignal::Finish); self.command_sender
let terminator = lock!(self.terminator).take(); .send(DownloadManagerSignal::Finish)
terminator.unwrap().await .unwrap();
let terminator = self.terminator.lock().unwrap().take();
terminator.unwrap().join()
} }
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> { pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
self.command_sender.clone() self.command_sender.clone()
} }
pub fn clone_depot_manager(&self) -> Arc<DepotManager> {
self.depot_manager.clone()
}
} }
/// Takes in the locked value from .`edit()` and attempts to /// Takes in the locked value from .`edit()` and attempts to

View File

@@ -1,11 +1,9 @@
use std::{fmt::Debug, sync::Arc}; use std::sync::Arc;
use async_trait::async_trait; use drop_database::models::data::DownloadableMetadata;
use database::DownloadableMetadata; use drop_errors::application_download_error::ApplicationDownloadError;
use tauri::AppHandle; use tauri::AppHandle;
use crate::error::ApplicationDownloadError;
use super::{ use super::{
download_manager_frontend::DownloadStatus, download_manager_frontend::DownloadStatus,
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject}, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
@@ -17,9 +15,8 @@ use super::{
* *
* But the download manager manages the queue state * But the download manager manages the queue state
*/ */
#[async_trait] pub trait Downloadable: Send + Sync {
pub trait Downloadable: Send + Sync + Debug { fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
async fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>; fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>; fn progress(&self) -> Arc<ProgressObject>;
@@ -28,6 +25,6 @@ pub trait Downloadable: Send + Sync + Debug {
fn metadata(&self) -> DownloadableMetadata; fn metadata(&self) -> DownloadableMetadata;
fn on_queued(&self, app_handle: &AppHandle); fn on_queued(&self, app_handle: &AppHandle);
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError); fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
async fn on_complete(&self, app_handle: &AppHandle); fn on_complete(&self, app_handle: &AppHandle);
fn on_cancelled(&self, app_handle: &AppHandle); fn on_cancelled(&self, app_handle: &AppHandle);
} }

View File

@@ -1,4 +1,4 @@
use database::DownloadableMetadata; use drop_database::models::data::DownloadableMetadata;
use serde::Serialize; use serde::Serialize;
use crate::download_manager_frontend::DownloadStatus; use crate::download_manager_frontend::DownloadStatus;

View File

@@ -0,0 +1,7 @@
#![feature(duration_millis_float)]
pub mod download_manager_builder;
pub mod download_manager_frontend;
pub mod downloadable;
pub mod events;
pub mod util;

View File

@@ -1,6 +1,6 @@
use std::sync::{ use std::sync::{
Arc,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc,
}; };
#[derive(PartialEq, Eq, PartialOrd, Ord)] #[derive(PartialEq, Eq, PartialOrd, Ord)]
@@ -22,11 +22,7 @@ impl From<DownloadThreadControlFlag> for bool {
/// false => Stop /// false => Stop
impl From<bool> for DownloadThreadControlFlag { impl From<bool> for DownloadThreadControlFlag {
fn from(value: bool) -> Self { fn from(value: bool) -> Self {
if value { if value { DownloadThreadControlFlag::Go } else { DownloadThreadControlFlag::Stop }
DownloadThreadControlFlag::Go
} else {
DownloadThreadControlFlag::Stop
}
} }
} }

View File

@@ -2,4 +2,3 @@ pub mod download_thread_control_flag;
pub mod progress_object; pub mod progress_object;
pub mod queue; pub mod queue;
pub mod rolling_progress_updates; pub mod rolling_progress_updates;
pub mod semaphore;

View File

@@ -2,19 +2,19 @@ use std::{
sync::{ sync::{
Arc, Mutex, Arc, Mutex,
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
mpsc::Sender,
}, },
time::Instant, time::{Duration, Instant},
}; };
use atomic_instant_full::AtomicInstant; use atomic_instant_full::AtomicInstant;
use utils::{lock, send}; use throttle_my_fn::throttle;
use tokio::sync::mpsc::Sender;
use crate::download_manager_frontend::DownloadManagerSignal; use crate::download_manager_frontend::DownloadManagerSignal;
use super::rolling_progress_updates::RollingProgressWindow; use super::rolling_progress_updates::RollingProgressWindow;
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct ProgressObject { pub struct ProgressObject {
max: Arc<Mutex<usize>>, max: Arc<Mutex<usize>>,
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>, progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
@@ -45,7 +45,7 @@ impl ProgressHandle {
pub fn add(&self, amount: usize) { pub fn add(&self, amount: usize) {
self.progress self.progress
.fetch_add(amount, std::sync::atomic::Ordering::AcqRel); .fetch_add(amount, std::sync::atomic::Ordering::AcqRel);
tauri::async_runtime::spawn(calculate_update(self.progress_object.clone())); calculate_update(&self.progress_object);
} }
pub fn skip(&self, amount: usize) { pub fn skip(&self, amount: usize) {
self.progress self.progress
@@ -74,10 +74,12 @@ impl ProgressObject {
} }
pub fn set_time_now(&self) { pub fn set_time_now(&self) {
*lock!(self.start) = Instant::now(); *self.start.lock().unwrap() = Instant::now();
} }
pub fn sum(&self) -> usize { pub fn sum(&self) -> usize {
lock!(self.progress_instances) self.progress_instances
.lock()
.unwrap()
.iter() .iter()
.map(|instance| instance.load(Ordering::Acquire)) .map(|instance| instance.load(Ordering::Acquire))
.sum() .sum()
@@ -86,42 +88,39 @@ impl ProgressObject {
self.set_time_now(); self.set_time_now();
self.bytes_last_update.store(0, Ordering::Release); self.bytes_last_update.store(0, Ordering::Release);
self.rolling.reset(); self.rolling.reset();
lock!(self.progress_instances) self.progress_instances
.lock()
.unwrap()
.iter() .iter()
.for_each(|x| x.store(0, Ordering::SeqCst)); .for_each(|x| x.store(0, Ordering::SeqCst));
} }
pub fn get_max(&self) -> usize { pub fn get_max(&self) -> usize {
*lock!(self.max) *self.max.lock().unwrap()
} }
pub fn set_max(&self, new_max: usize) { pub fn set_max(&self, new_max: usize) {
*lock!(self.max) = new_max; *self.max.lock().unwrap() = new_max;
} }
pub fn set_size(&self, length: usize) { pub fn set_size(&self, length: usize) {
*lock!(self.progress_instances) = *self.progress_instances.lock().unwrap() =
(0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect(); (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
} }
pub fn get_progress(&self) -> f64 { pub fn get_progress(&self) -> f64 {
self.sum() as f64 / self.get_max() as f64 self.sum() as f64 / self.get_max() as f64
} }
pub fn get(&self, index: usize) -> Arc<AtomicUsize> { pub fn get(&self, index: usize) -> Arc<AtomicUsize> {
lock!(self.progress_instances)[index].clone() self.progress_instances.lock().unwrap()[index].clone()
} }
fn update_window(&self, kilobytes_per_second: usize) { fn update_window(&self, kilobytes_per_second: usize) {
self.rolling.update(kilobytes_per_second); self.rolling.update(kilobytes_per_second);
} }
} }
pub async fn calculate_update(progress: Arc<ProgressObject>) { #[throttle(1, Duration::from_millis(20))]
pub fn calculate_update(progress: &ProgressObject) {
let last_update_time = progress let last_update_time = progress
.last_update_time .last_update_time
.load(Ordering::SeqCst); .swap(Instant::now(), Ordering::SeqCst);
let time_since_last_update = Instant::now() let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis_f64();
.duration_since(last_update_time)
.as_millis_f64();
if time_since_last_update < 250.0 {
return;
}
progress.last_update_time.swap(Instant::now(), Ordering::SeqCst);
let current_bytes_downloaded = progress.sum(); let current_bytes_downloaded = progress.sum();
let max = progress.get_max(); let max = progress.get_max();
@@ -129,32 +128,38 @@ pub async fn calculate_update(progress: Arc<ProgressObject>) {
.bytes_last_update .bytes_last_update
.swap(current_bytes_downloaded, Ordering::Acquire); .swap(current_bytes_downloaded, Ordering::Acquire);
let bytes_since_last_update = let bytes_since_last_update = current_bytes_downloaded.saturating_sub(bytes_at_last_update) as f64;
current_bytes_downloaded.saturating_sub(bytes_at_last_update) as f64;
let kilobytes_per_second = bytes_since_last_update / time_since_last_update; let kilobytes_per_second = bytes_since_last_update / time_since_last_update;
let bytes_remaining = max.saturating_sub(current_bytes_downloaded); // bytes let bytes_remaining = max.saturating_sub(current_bytes_downloaded); // bytes
progress.update_window(kilobytes_per_second as usize); progress.update_window(kilobytes_per_second as usize);
push_update(&progress, bytes_remaining).await; push_update(progress, bytes_remaining);
} }
pub async fn push_update(progress: &ProgressObject, bytes_remaining: usize) { #[throttle(1, Duration::from_millis(250))]
pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
let average_speed = progress.rolling.get_average(); let average_speed = progress.rolling.get_average();
let time_remaining = (bytes_remaining / 1000) / average_speed.max(1); let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
update_ui(progress, average_speed, time_remaining).await; update_ui(progress, average_speed, time_remaining);
update_queue(progress).await; update_queue(progress);
} }
async fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) { fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
send!( progress_object
progress_object.sender, .sender
DownloadManagerSignal::UpdateUIStats(kilobytes_per_second, time_remaining) .send(DownloadManagerSignal::UpdateUIStats(
); kilobytes_per_second,
time_remaining,
))
.unwrap();
} }
async fn update_queue(progress: &ProgressObject) { fn update_queue(progress: &ProgressObject) {
send!(progress.sender, DownloadManagerSignal::UpdateUIQueue) progress
.sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
} }

View File

@@ -3,10 +3,9 @@ use std::{
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
}; };
use database::DownloadableMetadata; use drop_database::models::data::DownloadableMetadata;
use utils::lock;
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct Queue { pub struct Queue {
inner: Arc<Mutex<VecDeque<DownloadableMetadata>>>, inner: Arc<Mutex<VecDeque<DownloadableMetadata>>>,
} }
@@ -25,10 +24,10 @@ impl Queue {
} }
} }
pub fn read(&self) -> VecDeque<DownloadableMetadata> { pub fn read(&self) -> VecDeque<DownloadableMetadata> {
lock!(self.inner).clone() self.inner.lock().unwrap().clone()
} }
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> { pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
lock!(self.inner) self.inner.lock().unwrap()
} }
pub fn pop_front(&self) -> Option<DownloadableMetadata> { pub fn pop_front(&self) -> Option<DownloadableMetadata> {
self.edit().pop_front() self.edit().pop_front()

View File

@@ -3,17 +3,11 @@ use std::sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
}; };
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct RollingProgressWindow<const S: usize> { pub struct RollingProgressWindow<const S: usize> {
window: Arc<[AtomicUsize; S]>, window: Arc<[AtomicUsize; S]>,
current: Arc<AtomicUsize>, current: Arc<AtomicUsize>,
} }
impl<const S: usize> Default for RollingProgressWindow<S> {
fn default() -> Self {
Self::new()
}
}
impl<const S: usize> RollingProgressWindow<S> { impl<const S: usize> RollingProgressWindow<S> {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -37,7 +31,7 @@ impl<const S: usize> RollingProgressWindow<S> {
.collect::<Vec<usize>>(); .collect::<Vec<usize>>();
let amount = valid.len(); let amount = valid.len();
let sum = valid.into_iter().sum::<usize>(); let sum = valid.into_iter().sum::<usize>();
sum / amount sum / amount
} }
pub fn reset(&self) { pub fn reset(&self) {

View File

@@ -0,0 +1,14 @@
[package]
name = "drop-errors"
version = "0.1.0"
edition = "2024"
[dependencies]
http = "1.3.1"
humansize = "2.1.3"
reqwest = "0.12.23"
reqwest-websocket = "0.5.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_with = "3.14.0"
tauri-plugin-opener = "2.5.0"
url = "2.5.7"

View File

@@ -0,0 +1,49 @@
use std::{
fmt::{Display, Formatter},
io, sync::Arc,
};
use serde_with::SerializeDisplay;
use humansize::{format_size, BINARY};
use super::remote_access_error::RemoteAccessError;
// TODO: Rename / separate from downloads
#[derive(Debug, SerializeDisplay)]
pub enum ApplicationDownloadError {
NotInitialized,
Communication(RemoteAccessError),
DiskFull(u64, u64),
#[allow(dead_code)]
Checksum,
Lock,
IoError(Arc<io::Error>),
DownloadError,
}
impl Display for ApplicationDownloadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApplicationDownloadError::NotInitialized => write!(f, "Download not initalized, did something go wrong?"),
ApplicationDownloadError::DiskFull(required, available) => write!(
f,
"Game requires {}, {} remaining left on disk.",
format_size(*required, BINARY),
format_size(*available, BINARY),
),
ApplicationDownloadError::Communication(error) => write!(f, "{error}"),
ApplicationDownloadError::Lock => write!(
f,
"failed to acquire lock. Something has gone very wrong internally. Please restart the application"
),
ApplicationDownloadError::Checksum => {
write!(f, "checksum failed to validate for download")
}
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
ApplicationDownloadError::DownloadError => write!(
f,
"Download failed. See Download Manager status for specific error"
),
}
}
}

View File

@@ -0,0 +1,27 @@
use std::{fmt::Display, io, sync::mpsc::SendError};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum DownloadManagerError<T> {
IOError(io::Error),
SignalError(SendError<T>),
}
impl<T> Display for DownloadManagerError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DownloadManagerError::IOError(error) => write!(f, "{error}"),
DownloadManagerError::SignalError(send_error) => write!(f, "{send_error}"),
}
}
}
impl<T> From<SendError<T>> for DownloadManagerError<T> {
fn from(value: SendError<T>) -> Self {
DownloadManagerError::SignalError(value)
}
}
impl<T> From<io::Error> for DownloadManagerError<T> {
fn from(value: io::Error) -> Self {
DownloadManagerError::IOError(value)
}
}

View File

@@ -0,0 +1,10 @@
use serde::Deserialize;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ServerError {
pub status_code: usize,
pub status_message: String,
// pub message: String,
// pub url: String,
}

View File

@@ -0,0 +1,6 @@
pub mod application_download_error;
pub mod download_manager_error;
pub mod drop_server_error;
pub mod library_error;
pub mod process_error;
pub mod remote_access_error;

View File

@@ -0,0 +1,18 @@
use std::fmt::Display;
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum LibraryError {
MetaNotFound(String),
}
impl Display for LibraryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LibraryError::MetaNotFound(id) => write!(
f,
"Could not locate any installed version of game ID {id} in the database"
),
}
}
}

View File

@@ -0,0 +1,31 @@
use std::{fmt::Display, io::Error};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum ProcessError {
NotInstalled,
AlreadyRunning,
InvalidID,
InvalidVersion,
IOError(Error),
FormatError(String), // String errors supremacy
InvalidPlatform,
OpenerError(tauri_plugin_opener::Error)
}
impl Display for ProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
ProcessError::NotInstalled => "Game not installed",
ProcessError::AlreadyRunning => "Game already running",
ProcessError::InvalidID => "Invalid game ID",
ProcessError::InvalidVersion => "Invalid game version",
ProcessError::IOError(error) => &error.to_string(),
ProcessError::InvalidPlatform => "This game cannot be played on the current platform",
ProcessError::FormatError(e) => &format!("Failed to format template: {e}"),
ProcessError::OpenerError(error) => &format!("Failed to open directory: {error}"),
};
write!(f, "{s}")
}
}

View File

@@ -4,37 +4,26 @@ use std::{
sync::Arc, sync::Arc,
}; };
use http::{HeaderName, StatusCode, header::ToStrError}; use http::StatusCode;
use serde_with::SerializeDisplay; use serde_with::SerializeDisplay;
use url::ParseError; use url::ParseError;
use serde::Deserialize; use super::drop_server_error::ServerError;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DropServerError {
pub status_code: usize,
pub status_message: String,
pub message: String,
// pub url: String,
}
#[derive(Debug, SerializeDisplay)] #[derive(Debug, SerializeDisplay)]
pub enum RemoteAccessError { pub enum RemoteAccessError {
FetchErrorLegacy(Arc<reqwest::Error>), FetchError(Arc<reqwest::Error>),
FetchError(Arc<reqwest_middleware::Error>),
FetchErrorWS(Arc<reqwest_websocket::Error>), FetchErrorWS(Arc<reqwest_websocket::Error>),
ParsingError(ParseError), ParsingError(ParseError),
InvalidEndpoint, InvalidEndpoint,
HandshakeFailed(String), HandshakeFailed(String),
GameNotFound(String), GameNotFound(String),
InvalidResponse(DropServerError), InvalidResponse(ServerError),
UnparseableResponse(String), UnparseableResponse(String),
ManifestDownloadFailed(StatusCode, String), ManifestDownloadFailed(StatusCode, String),
OutOfSync, OutOfSync,
Cache(std::io::Error), Cache(std::io::Error),
CorruptedState, CorruptedState,
NoDepots,
} }
impl Display for RemoteAccessError { impl Display for RemoteAccessError {
@@ -55,26 +44,19 @@ impl Display for RemoteAccessError {
error error
.source() .source()
.map(std::string::ToString::to_string) .map(std::string::ToString::to_string)
.unwrap_or("Unknown error".to_string()) .or_else(|| Some("Unknown error".to_string()))
.unwrap()
) )
} }
RemoteAccessError::FetchErrorLegacy(error) => write!(
f,
"{}: {}",
error,
error
.source()
.map(|v| v.to_string())
.unwrap_or("Unknown error".to_string())
),
RemoteAccessError::FetchErrorWS(error) => write!( RemoteAccessError::FetchErrorWS(error) => write!(
f, f,
"{}: {}", "{}: {}",
error, error,
error error
.source() .source()
.map(std::string::ToString::to_string) .map(|e| e.to_string())
.unwrap_or("Unknown error".to_string()) .or_else(|| Some("Unknown error".to_string()))
.unwrap()
), ),
RemoteAccessError::ParsingError(parse_error) => { RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{parse_error}") write!(f, "{parse_error}")
@@ -87,7 +69,7 @@ impl Display for RemoteAccessError {
RemoteAccessError::InvalidResponse(error) => write!( RemoteAccessError::InvalidResponse(error) => write!(
f, f,
"server returned an invalid response: {}, {}", "server returned an invalid response: {}, {}",
error.status_code, error.message error.status_code, error.status_message
), ),
RemoteAccessError::UnparseableResponse(error) => { RemoteAccessError::UnparseableResponse(error) => {
write!(f, "server returned an invalid response: {error}") write!(f, "server returned an invalid response: {error}")
@@ -104,19 +86,13 @@ impl Display for RemoteAccessError {
f, f,
"Drop encountered a corrupted internal state. Please report this to the developers, with details of reproduction." "Drop encountered a corrupted internal state. Please report this to the developers, with details of reproduction."
), ),
RemoteAccessError::NoDepots => write!(f, "There are no download depots configured on the server. Contact your server admin."), }
}
} }
} }
impl From<reqwest::Error> for RemoteAccessError { impl From<reqwest::Error> for RemoteAccessError {
fn from(err: reqwest::Error) -> Self { fn from(err: reqwest::Error) -> Self {
RemoteAccessError::FetchErrorLegacy(Arc::new(err)) RemoteAccessError::FetchError(Arc::new(err))
}
}
impl From<reqwest_middleware::Error> for RemoteAccessError {
fn from(value: reqwest_middleware::Error) -> Self {
RemoteAccessError::FetchError(Arc::new(value))
} }
} }
impl From<reqwest_websocket::Error> for RemoteAccessError { impl From<reqwest_websocket::Error> for RemoteAccessError {
@@ -130,31 +106,3 @@ impl From<ParseError> for RemoteAccessError {
} }
} }
impl std::error::Error for RemoteAccessError {} impl std::error::Error for RemoteAccessError {}
#[derive(Debug, SerializeDisplay)]
pub enum CacheError {
HeaderNotFound(HeaderName),
ParseError(ToStrError),
Remote(RemoteAccessError),
ConstructionError(http::Error),
}
impl Display for CacheError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
CacheError::HeaderNotFound(header_name) => {
format!("Could not find header {header_name} in cache")
}
CacheError::ParseError(to_str_error) => {
format!("Could not parse cache with error {to_str_error}")
}
CacheError::Remote(remote_access_error) => {
format!("Cache got remote access error: {remote_access_error}")
}
CacheError::ConstructionError(error) => {
format!("Could not construct cache body with error {error}")
}
};
write!(f, "{s}")
}
}

View File

@@ -0,0 +1,11 @@
[package]
name = "drop-library"
version = "0.1.0"
edition = "2024"
[dependencies]
drop-errors = { path = "../drop-errors" }
http = "*"
reqwest = { version = "*", default-features = false }
serde = { version = "*", default-features = false, features = ["derive"] }
tauri = "*"

View File

@@ -0,0 +1,11 @@
pub enum DropLibraryError {
NetworkError(reqwest::Error),
ServerError(drop_errors::drop_server_error::ServerError),
Unconfigured,
}
impl From<reqwest::Error> for DropLibraryError {
fn from(value: reqwest::Error) -> Self {
DropLibraryError::NetworkError(value)
}
}

View File

@@ -0,0 +1,30 @@
use crate::libraries::LibraryProviderIdentifier;
pub struct LibraryGamePreview {
pub library: LibraryProviderIdentifier,
pub internal_id: String,
pub name: String,
pub short_description: String,
pub icon: String,
}
pub struct LibraryGame {
pub library: LibraryProviderIdentifier,
pub internal_id: String,
pub name: String,
pub short_description: String,
pub md_description: String,
pub icon: String,
}
impl From<LibraryGame> for LibraryGamePreview {
fn from(value: LibraryGame) -> Self {
LibraryGamePreview {
library: value.library,
internal_id: value.internal_id,
name: value.name,
short_description: value.short_description,
icon: value.icon,
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod libraries;
pub mod game;
pub mod errors;

View File

@@ -0,0 +1,76 @@
use std::{
fmt::Display,
hash::{DefaultHasher, Hash, Hasher},
};
use http::Request;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use tauri::UriSchemeResponder;
use crate::{
errors::DropLibraryError,
game::{LibraryGame, LibraryGamePreview},
};
#[derive(Clone, Serialize, Deserialize)]
pub struct LibraryProviderIdentifier {
internal_id: usize,
name: String,
}
impl PartialEq for LibraryProviderIdentifier {
fn eq(&self, other: &Self) -> bool {
self.internal_id == other.internal_id
}
}
impl Eq for LibraryProviderIdentifier {}
impl Hash for LibraryProviderIdentifier {
fn hash<H: Hasher>(&self, state: &mut H) {
self.internal_id.hash(state);
}
}
impl Display for LibraryProviderIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.name)
}
}
impl LibraryProviderIdentifier {
pub fn str_hash(&self) -> String {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish().to_string()
}
}
pub struct LibraryFetchConfig {
pub hard_refresh: bool,
}
pub trait DropLibraryProvider: Serialize + DeserializeOwned + Sized {
fn build(identifier: LibraryProviderIdentifier) -> Self;
fn id(&self) -> &LibraryProviderIdentifier;
fn load_object(
&self,
request: Request<Vec<u8>>,
responder: UriSchemeResponder,
) -> impl Future<Output = Result<(), DropLibraryError>> + Send;
fn fetch_library(
&self,
config: &LibraryFetchConfig,
) -> impl Future<Output = Result<Vec<LibraryGamePreview>, DropLibraryError>> + Send;
fn fetch_game(
&self,
config: &LibraryFetchConfig,
) -> impl Future<Output = Result<LibraryGame, DropLibraryError>> + Send;
fn owns_game(&self, id: &LibraryProviderIdentifier) -> bool {
self.id().internal_id == id.internal_id
}
}

View File

@@ -0,0 +1,14 @@
[package]
name = "drop-native-library"
version = "0.1.0"
edition = "2024"
[dependencies]
bitcode = "*"
drop-errors = { path = "../drop-errors" }
drop-library = { path = "../drop-library" }
drop-remote = { path = "../drop-remote" }
log = "*"
serde = { version = "*", features = ["derive"] }
tauri = "*"
url = "*"

View File

@@ -1,8 +1,7 @@
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
// use drop_database::runtime_models::Game;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::library::Game;
pub type Collections = Vec<Collection>; pub type Collections = Vec<Collection>;
#[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)]
@@ -12,13 +11,13 @@ pub struct Collection {
name: String, name: String,
is_default: bool, is_default: bool,
user_id: String, user_id: String,
pub entries: Vec<CollectionObject>, entries: Vec<CollectionObject>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CollectionObject { pub struct CollectionObject {
pub collection_id: String, collection_id: String,
pub game_id: String, game_id: String,
pub game: Game, game: Game,
} }

View File

@@ -0,0 +1,11 @@
use drop_database::models::data::{ApplicationTransientStatus, GameDownloadStatus, GameVersion};
#[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent {
pub game_id: String,
pub status: (
Option<GameDownloadStatus>,
Option<ApplicationTransientStatus>,
),
pub version: Option<GameVersion>,
}

View File

@@ -0,0 +1,50 @@
use drop_library::{
errors::DropLibraryError, game::{LibraryGame, LibraryGamePreview}, libraries::{DropLibraryProvider, LibraryFetchConfig, LibraryProviderIdentifier}
};
use drop_remote::{fetch_object::fetch_object, DropRemoteContext};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Serialize, Deserialize, Clone)]
pub struct DropNativeLibraryProvider {
identifier: LibraryProviderIdentifier,
context: Option<DropRemoteContext>,
}
impl DropNativeLibraryProvider {
pub fn configure(&mut self, base_url: Url) {
self.context = Some(DropRemoteContext::new(base_url));
}
}
impl DropLibraryProvider for DropNativeLibraryProvider {
fn build(identifier: LibraryProviderIdentifier) -> Self {
Self {
identifier,
context: None,
}
}
fn id(&self) -> &LibraryProviderIdentifier {
&self.identifier
}
async fn load_object(&self, request: tauri::http::Request<Vec<u8>>, responder: tauri::UriSchemeResponder) -> Result<(), DropLibraryError> {
let context = self.context.as_ref().ok_or(DropLibraryError::Unconfigured)?;
fetch_object(context, request, responder).await;
Ok(())
}
async fn fetch_library(
&self,
config: &LibraryFetchConfig
) -> Result<Vec<LibraryGamePreview>, DropLibraryError> {
todo!()
}
async fn fetch_game(&self, config: &LibraryFetchConfig) -> Result<LibraryGame, DropLibraryError> {
todo!()
}
}

View File

@@ -0,0 +1,5 @@
//pub mod collections;
//pub mod library;
//pub mod state;
//pub mod events;
pub mod impls;

View File

@@ -0,0 +1,493 @@
use std::fs::remove_dir_all;
use std::thread::spawn;
use drop_database::borrow_db_checked;
use drop_database::borrow_db_mut_checked;
use drop_database::models::data::ApplicationTransientStatus;
use drop_database::models::data::Database;
use drop_database::models::data::DownloadableMetadata;
use drop_database::models::data::GameDownloadStatus;
use drop_database::models::data::GameVersion;
use drop_database::runtime_models::Game;
use drop_errors::drop_server_error::ServerError;
use drop_errors::library_error::LibraryError;
use drop_errors::remote_access_error::RemoteAccessError;
use drop_remote::DropRemoteContext;
use drop_remote::auth::generate_authorization_header;
use drop_remote::cache::cache_object;
use drop_remote::cache::cache_object_db;
use drop_remote::cache::get_cached_object;
use drop_remote::cache::get_cached_object_db;
use drop_remote::requests::generate_url;
use drop_remote::utils::DROP_CLIENT_ASYNC;
use drop_remote::utils::DROP_CLIENT_SYNC;
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use tauri::Emitter as _;
use crate::events::GameUpdateEvent;
use crate::state::GameStatusManager;
use crate::state::GameStatusWithTransient;
#[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct {
game: Game,
status: GameStatusWithTransient,
version: Option<GameVersion>,
}
pub async fn fetch_library_logic(
context: &DropRemoteContext,
hard_fresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
let do_hard_refresh = hard_fresh.unwrap_or(false);
if !do_hard_refresh && let Ok(library) = get_cached_object("library") {
return Ok(library);
}
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(context, &["/api/v1/client/user/library"], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header(context))
.send()
.await?;
if response.status() != 200 {
let err = response.json().await.unwrap_or(ServerError {
status_code: 500,
status_message: "Invalid response from server.".to_owned(),
});
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let mut games: Vec<Game> = response.json().await?;
let mut db_handle = borrow_db_mut_checked();
for game in &games {
db_handle
.applications
.games
.insert(game.id.clone(), game.clone());
if !db_handle.applications.game_statuses.contains_key(&game.id) {
db_handle
.applications
.game_statuses
.insert(game.id.clone(), GameDownloadStatus::Remote {});
}
}
// Add games that are installed but no longer in library
for meta in db_handle.applications.installed_game_version.values() {
if games.iter().any(|e| e.id == meta.id) {
continue;
}
// We should always have a cache of the object
// Pass db_handle because otherwise we get a gridlock
let game = match get_cached_object_db::<Game>(&meta.id.clone()) {
Ok(game) => game,
Err(err) => {
warn!(
"{} is installed, but encountered error fetching its error: {}.",
meta.id, err
);
continue;
}
};
games.push(game);
}
drop(db_handle);
cache_object("library", &games)?;
Ok(games)
}
pub async fn fetch_library_logic_offline(
_hard_refresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
let mut games: Vec<Game> = get_cached_object("library")?;
let db_handle = borrow_db_checked();
games.retain(|game| {
matches!(
&db_handle
.applications
.game_statuses
.get(&game.id)
.unwrap_or(&GameDownloadStatus::Remote {}),
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
)
});
Ok(games)
}
pub async fn fetch_game_logic(
context: &DropRemoteContext,
id: String,
) -> Result<FetchGameStruct, RemoteAccessError> {
let version = {
let db_lock = borrow_db_checked();
let metadata_option = db_lock.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => db_lock
.applications
.game_versions
.get(&metadata.id)
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
.cloned(),
};
let game = db_lock.applications.games.get(&id);
if let Some(game) = game {
let status = GameStatusManager::fetch_state(&id, &db_lock);
let data = FetchGameStruct {
game: game.clone(),
status,
version,
};
cache_object_db(&id, game, &db_lock)?;
return Ok(data);
}
version
};
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(context, &["/api/v1/client/game/", &id], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header(context))
.send()
.await?;
if response.status() == 404 {
let offline_fetch = fetch_game_logic_offline(id.clone()).await;
if let Ok(fetch_data) = offline_fetch {
return Ok(fetch_data);
}
return Err(RemoteAccessError::GameNotFound(id));
}
if response.status() != 200 {
let err = response.json().await.unwrap();
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let game: Game = response.json().await?;
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.games
.insert(id.clone(), game.clone());
db_handle
.applications
.game_statuses
.entry(id.clone())
.or_insert(GameDownloadStatus::Remote {});
let status = GameStatusManager::fetch_state(&id, &db_handle);
drop(db_handle);
let data = FetchGameStruct {
game: game.clone(),
status,
version,
};
cache_object(&id, &game)?;
Ok(data)
}
pub async fn fetch_game_logic_offline(id: String) -> Result<FetchGameStruct, RemoteAccessError> {
let db_handle = borrow_db_checked();
let metadata_option = db_handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => db_handle
.applications
.game_versions
.get(&metadata.id)
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
.cloned(),
};
let status = GameStatusManager::fetch_state(&id, &db_handle);
let game = get_cached_object::<Game>(&id)?;
drop(db_handle);
Ok(FetchGameStruct {
game,
status,
version,
})
}
pub async fn fetch_game_version_options_logic(
context: &DropRemoteContext,
game_id: String,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(
context,
&["/api/v1/client/game/versions"],
&[("id", &game_id)],
)?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header(context))
.send()
.await?;
if response.status() != 200 {
let err = response.json().await.unwrap();
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let data: Vec<GameVersion> = response.json().await?;
Ok(data)
}
/**
* Called by:
* - on_cancel, when cancelled, for obvious reasons
* - when downloading, so if drop unexpectedly quits, we can resume the download. hidden by the "Downloading..." transient state, though
* - when scanning, to import the game
*/
pub fn set_partially_installed(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
) {
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
}
pub fn set_partially_installed_db(
db_lock: &mut Database,
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
) {
db_lock.applications.transient_statuses.remove(meta);
db_lock.applications.game_statuses.insert(
meta.id.clone(),
GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.as_ref().unwrap().clone(),
install_dir,
},
);
db_lock
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
if let Some(app_handle) = app_handle {
push_game_update(
app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, db_lock),
);
}
}
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
debug!("triggered uninstall for agent");
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.transient_statuses
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
push_game_update(
app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, &db_handle),
);
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
if previous_state.is_none() {
warn!("uninstall job doesn't have previous state, failing silently");
return;
}
let previous_state = previous_state.unwrap();
if let Some((_, install_dir)) = match previous_state {
GameDownloadStatus::Installed {
version_name,
install_dir,
} => Some((version_name, install_dir)),
GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Some((version_name, install_dir)),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => Some((version_name, install_dir)),
_ => None,
} {
db_handle
.applications
.transient_statuses
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
drop(db_handle);
let app_handle = app_handle.clone();
spawn(move || {
if let Err(e) = remove_dir_all(install_dir) {
error!("{e}");
} else {
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
.applications
.installed_game_version
.remove(&meta.id);
db_handle
.applications
.game_statuses
.insert(meta.id.clone(), GameDownloadStatus::Remote {});
let _ = db_handle.applications.transient_statuses.remove(&meta);
push_game_update(
&app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, &db_handle),
);
debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap();
}
});
} else {
warn!("invalid previous state for uninstall, failing silently.");
}
}
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
borrow_db_checked()
.applications
.installed_game_version
.get(game_id)
.cloned()
}
pub fn on_game_complete(
context: &DropRemoteContext,
meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
}
let client = DROP_CLIENT_SYNC.clone();
let response = generate_url(
context,
&["/api/v1/client/game/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
)?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header(context))
.send()?;
let game_version: GameVersion = response.json()?;
let mut handle = borrow_db_mut_checked();
handle
.applications
.game_versions
.entry(meta.id.clone())
.or_default()
.insert(meta.version.clone().unwrap(), game_version.clone());
handle
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
drop(handle);
let status = if game_version.setup_command.is_empty() {
GameDownloadStatus::Installed {
version_name: meta.version.clone().unwrap(),
install_dir,
}
} else {
GameDownloadStatus::SetupRequired {
version_name: meta.version.clone().unwrap(),
install_dir,
}
};
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(db_handle);
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(status), None),
version: Some(game_version),
},
)
.unwrap();
Ok(())
}
pub fn push_game_update(
app_handle: &AppHandle,
game_id: &String,
version: Option<GameVersion>,
status: GameStatusWithTransient,
) {
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
&status.0
&& version.is_none()
{
panic!("pushed game for installed game that doesn't have version information");
}
app_handle
.emit(
&format!("update_game/{game_id}"),
GameUpdateEvent {
game_id: game_id.clone(),
status,
version,
},
)
.unwrap();
}

View File

@@ -1,6 +1,4 @@
use database::models::data::{ // use drop_database::models::data::{ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, GameDownloadStatus};
ApplicationTransientStatus, Database, DownloadType, GameDownloadStatus,
};
pub type GameStatusWithTransient = ( pub type GameStatusWithTransient = (
Option<GameDownloadStatus>, Option<GameDownloadStatus>,
@@ -13,11 +11,13 @@ impl GameStatusManager {
let online_state = database let online_state = database
.applications .applications
.transient_statuses .transient_statuses
.iter() .get(&DownloadableMetadata {
.find(|v| v.0.id == *game_id && v.0.download_type == DownloadType::Game) id: game_id.to_string(),
.map(|v| v.1.clone()) download_type: DownloadType::Game,
.clone(); version: None,
})
.cloned();
let offline_state = database.applications.game_statuses.get(game_id).cloned(); let offline_state = database.applications.game_statuses.get(game_id).cloned();
if online_state.is_some() { if online_state.is_some() {

View File

@@ -0,0 +1,18 @@
[package]
name = "drop-process"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.42"
drop-database = { path = "../drop-database" }
drop-errors = { path = "../drop-errors" }
drop-native-library = { path = "../drop-native-library" }
dynfmt = { version = "0.1.5", features = ["curly"] }
log = "0.4.28"
page_size = "0.6.0"
shared_child = "1.1.1"
sysinfo = "0.37.0"
tauri = "2.8.5"
tauri-plugin-opener = "2.5.0"

View File

@@ -8,13 +8,7 @@ pub struct DropFormatArgs {
} }
impl DropFormatArgs { impl DropFormatArgs {
pub fn new( pub fn new(launch_string: String, working_dir: &String, executable_name: &String, absolute_executable_name: String) -> Self {
launch_string: String,
working_dir: &String,
executable_name: &String,
absolute_executable_name: String,
original: Option<String>,
) -> Self {
let mut positional = Vec::new(); let mut positional = Vec::new();
let mut map: HashMap<&'static str, String> = HashMap::new(); let mut map: HashMap<&'static str, String> = HashMap::new();
@@ -23,10 +17,6 @@ impl DropFormatArgs {
map.insert("dir", working_dir.to_string()); map.insert("dir", working_dir.to_string());
map.insert("exe", executable_name.to_string()); map.insert("exe", executable_name.to_string());
map.insert("abs_exe", absolute_executable_name); map.insert("abs_exe", absolute_executable_name);
if let Some(original) = original {
map.insert("executor", original);
}
Self { positional, map } Self { positional, map }
} }

View File

@@ -0,0 +1,4 @@
mod format;
mod process_handlers;
pub mod process_manager;
pub mod utils;

View File

@@ -0,0 +1,135 @@
use std::{
ffi::OsStr,
path::PathBuf,
process::{Command, Stdio},
sync::LazyLock,
};
use drop_database::{models::data::{Database, DownloadableMetadata, GameVersion}, process::Platform};
use log::{debug, info};
use crate::process_manager::ProcessHandler;
pub struct NativeGameLauncher;
impl ProcessHandler for NativeGameLauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
_game_version: &GameVersion,
_current_dir: &str,
) -> String {
format!("\"{}\" {}", launch_command, args.join(" "))
}
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
true
}
}
pub static UMU_LAUNCHER_EXECUTABLE: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
let x = get_umu_executable();
info!("{:?}", &x);
x
});
const UMU_BASE_LAUNCHER_EXECUTABLE: &str = "umu-run";
const UMU_INSTALL_DIRS: [&str; 4] = ["/app/share", "/use/local/share", "/usr/share", "/opt"];
fn get_umu_executable() -> Option<PathBuf> {
if check_executable_exists(UMU_BASE_LAUNCHER_EXECUTABLE) {
return Some(PathBuf::from(UMU_BASE_LAUNCHER_EXECUTABLE));
}
for dir in UMU_INSTALL_DIRS {
let p = PathBuf::from(dir).join(UMU_BASE_LAUNCHER_EXECUTABLE);
if check_executable_exists(&p) {
return Some(p);
}
}
None
}
fn check_executable_exists<P: AsRef<OsStr>>(exec: P) -> bool {
let has_umu_installed = Command::new(exec).stdout(Stdio::null()).output();
has_umu_installed.is_ok()
}
pub struct UMULauncher;
impl ProcessHandler for UMULauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
game_version: &GameVersion,
_current_dir: &str,
) -> String {
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override {
Some(game_override) => {
if game_override.is_empty() {
game_version.game_id.clone()
} else {
game_override.clone()
}
}
None => game_version.game_id.clone(),
};
format!(
"GAMEID={game_id} {umu:?} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE.as_ref().unwrap(),
launch = launch_command,
args = args.join(" ")
)
}
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
UMU_LAUNCHER_EXECUTABLE.is_some()
}
}
pub struct AsahiMuvmLauncher;
impl ProcessHandler for AsahiMuvmLauncher {
fn create_launch_process(
&self,
meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
game_version: &GameVersion,
current_dir: &str,
) -> String {
let umu_launcher = UMULauncher {};
let umu_string = umu_launcher.create_launch_process(
meta,
launch_command,
args,
game_version,
current_dir,
);
let mut args_cmd = umu_string
.split("umu-run")
.collect::<Vec<&str>>()
.into_iter();
let args = args_cmd.next().unwrap().trim();
let cmd = format!("umu-run{}", args_cmd.next().unwrap());
format!("{args} muvm -- {cmd}")
}
#[allow(unreachable_code)]
#[allow(unused_variables)]
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
#[cfg(not(target_os = "linux"))]
return false;
#[cfg(not(target_arch = "aarch64"))]
return false;
let page_size = page_size::get();
if page_size != 16384 {
return false;
}
UMU_LAUNCHER_EXECUTABLE.is_some()
}
}

Some files were not shown because too many files have changed in this diff Show More