Compare commits
7 commits
better-err
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fab0af4e8 | ||
|
|
3968a61786 | ||
|
|
8861fe4e3d | ||
|
|
f5bd12b43a | ||
|
|
6e4ac4ad83 | ||
|
|
40c4e8a71c | ||
|
|
4ad688da14 |
57 changed files with 1128 additions and 671 deletions
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Arch Linux, Windows]
|
||||
- App Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
|
@ -3,7 +3,7 @@ stages:
|
|||
|
||||
build-linux:
|
||||
stage: build
|
||||
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rust:1.81.0-bookworm
|
||||
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rustlang/rust:nightly
|
||||
script:
|
||||
- apt-get update -y
|
||||
- apt-get install yarnpkg libsoup-3.0-0 libsoup-3.0-dev libatk-adaptor libgtk-3-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev -y
|
||||
|
|
@ -13,9 +13,10 @@ build-linux:
|
|||
- yarnpkg tauri build
|
||||
- cp src-tauri/target/release/bundle/deb/*.deb .
|
||||
- cp src-tauri/target/release/bundle/rpm/*.rpm .
|
||||
- cp src-tauri/target/release/bundle/appimage/*.AppImage .
|
||||
artifacts:
|
||||
paths:
|
||||
- "*.{deb,rpm}"
|
||||
- "*.{deb,rpm,AppImage}"
|
||||
|
||||
build-windows:
|
||||
stage: build
|
||||
|
|
|
|||
14
app.vue
14
app.vue
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<NuxtLayout class="select-none w-screen h-screen">
|
||||
<NuxtPage />
|
||||
<ModalStack />
|
||||
</NuxtLayout>
|
||||
<ModalStack />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -20,10 +20,18 @@ import {
|
|||
const router = useRouter();
|
||||
|
||||
const state = useAppState();
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
}
|
||||
|
||||
router.beforeEach(async () => {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
}
|
||||
});
|
||||
|
||||
setupHooks();
|
||||
|
|
|
|||
287
changelog.md
287
changelog.md
|
|
@ -1,5 +1,292 @@
|
|||
|
||||
|
||||
## Release 0.2.0-beta
|
||||
|
||||
### Fixes
|
||||
- Re-enabled killing games #005bab2
|
||||
- fixed queue manipulation and waiting for downloads #01260f0
|
||||
- fix logic error in detecting dir #04368ff
|
||||
- absolute executable invoke #17759c4
|
||||
- don't crash download manager if multiple errors come in #21204de
|
||||
- clear stale data before requesting new #327628b
|
||||
- fixed completed indexes #39f2ebd
|
||||
- add file & line to console logs #4d8eadc
|
||||
- Games not launching due to string semantics #4ef49cc
|
||||
- Added error handling for chunk request errors #4fc0855
|
||||
- Chunk counting logic error #5ba151f
|
||||
- modal stack doesn't cover whole app #5db9ae5
|
||||
- use set_file_name instead of pushing to strings #60d0a48
|
||||
- use of completed signal, and pause/resuming #64d7f64
|
||||
- add message about nonce expiration #6a8d0af
|
||||
- Added "LIbrary Failed to Update" content to recover from library load fail #76bae3d
|
||||
- Restored RUST_LOG env functionality #7a0cf4f
|
||||
- initialise doesn't recreate default install dir #7a3841b
|
||||
- update routes for new server #7ab53f3
|
||||
- use vendored flag #7c8089e
|
||||
- fix poorly designed parsing for executables with spaces #7c90d2b
|
||||
- assorted fixes #89ea34c
|
||||
- Added Settings component #8aad64f
|
||||
- windows build #8d9234f
|
||||
- fix ugly scrollbars on edge webview #95f2174
|
||||
- windows shadow #9a8cc59
|
||||
- add better error message #9af0d08
|
||||
- Broken command invoke logic in settings/downloads.vue #9e29aa7
|
||||
- Accidentally was attempting to lock onto something that was already in scope #9e82a0b
|
||||
- fix incorrect error assumptions & update types #a17311a
|
||||
- Re-enabled uninstalling apps #a56ee25
|
||||
- types #af056c0
|
||||
- fix other metadata endpoints #c2f54c1
|
||||
- Re-enabled deep links #c3f6222
|
||||
- added console as an appender #d12bf15
|
||||
- remove unnecessary unstable feature #d5ac1b0
|
||||
- fix install button #d7b0302
|
||||
- stop loading on error #d83aae6
|
||||
- use unix timestamp to avoid invalid characters in filename #dafce24
|
||||
- Renamed game_id to id #dceaa56
|
||||
- use chrono library to generate timestamps #e22e6d8
|
||||
- clear stale data before requesting new #e72662c
|
||||
- fix scrollbars on edge webview #f09605a
|
||||
- update readme instructions #f0c47d8
|
||||
- Adding usize to completed_contexts_lock instead of &usize #f508186
|
||||
|
||||
|
||||
### Features
|
||||
- Game kill tauri command #01e6162
|
||||
- add debug page #02f8591
|
||||
- Add signout functionality (#16) #0a0d9d6
|
||||
- queue and library UIs #0a20139
|
||||
- add note about more install dirs #139bc0c
|
||||
- Using SerializeDisplay for better error management with Result #170fde5
|
||||
- add pre-launch log to file #17f8d76
|
||||
- Added option to change root directory #1aa52c0
|
||||
- add speed and time remaining information #1f899ec
|
||||
- lockless tracking of downloaded chunks #2183585
|
||||
- quit button #239b8d5
|
||||
- use shift or DEBUG RUST_LOG to show Debug Info #245a84d
|
||||
- Added database corruption dialog #25ba200
|
||||
- only allow downloads for supported platforms #269dcbb
|
||||
- add installed ui in the library menu #2c8164e
|
||||
- added file-based logging #2d4a7e8
|
||||
- automatically fetch remote data if not available #2dedfbb
|
||||
- Added database recovery #32ae7d5
|
||||
- ability to add more download dirs #384f7a5
|
||||
- re-enable checksums #3ca87fc
|
||||
- background processes and close/open menu #3d60fd5
|
||||
- launch games with log files #3f71149
|
||||
- Download cancelling #450bca9
|
||||
- refactoring and error message #469a2d6
|
||||
- Added UI to change download threads #4e93eb4
|
||||
- Made save button include user feedback & only allow numeric characters #53234d2
|
||||
- download widget and queue fix #532d13e
|
||||
- Pausing and resuming game downloads #55b7921
|
||||
- Allow settings to update UI using fetch_settings command #5bb04da
|
||||
- temporary queue ui and flamegraph instructions #5cbeb3b
|
||||
- Added DownloadThreadControl struct #5e05e68
|
||||
- Added max_download_threads setting and separated settings from db #5ea47d7
|
||||
- Added generic download manager #6159319
|
||||
- Added AgentInterfaceData to get information about all downloads in queue #63c3cc1
|
||||
- debug queue interface #671d45f
|
||||
- reduce scope of download agent #6a38ea3
|
||||
- Added multi-argument game launch and setup support #6ad3837
|
||||
- shared child with stop command #6b96e40
|
||||
- Added function to take and set any game state #6bc6482
|
||||
- Added line numbers to file logging and highlighting to console #7c3140e
|
||||
- Separated chunk updates into individual counters #7d3c601
|
||||
- Ensure that any database issues are resolved by standalone functions #7d4651d
|
||||
- ui to install games #8670bca
|
||||
- Implemented spawning with umu (using umu-wrapper-lib) #88b2505
|
||||
- offer manual signin #949acfc
|
||||
- better process management, including running state #a135b13
|
||||
- Added Download Manager #a1ada07
|
||||
- retry connnection on server unavailable #a53d838
|
||||
- finish download dir CRUD interface #a580a46
|
||||
- better download manager errors + modal #ad92dbe
|
||||
- syncs state to disk to persist across reboots #b556842
|
||||
- prevent default context menu and emit event on elements #c560656
|
||||
- initial creation and logo update #d9a51cf
|
||||
- Added manifest.json utility for persistent download progress #d9d0122
|
||||
- game uninstalling & partial compat #dd7f567
|
||||
- combined db and download interface improvements #de52dac
|
||||
- update db state with ui and emit events #e4df4eb
|
||||
- Generic function to set download state #f10d92d
|
||||
- Convert DownloadThreadControlFlag to AtomicBool #f25bfed
|
||||
- add note about more install dirs #f4ac1c8
|
||||
- Added rolling progress window #fd30b3e
|
||||
|
||||
|
||||
### Other Changes
|
||||
- quexeky <git@quexeky.dev>
|
||||
- Convert DATA_ROOT_DIR to Mutex #00b7179
|
||||
- Converting DB access to a trait #01b092c
|
||||
- Updated changelog #022330b
|
||||
- Progress on cleanup and exit #0381b8b
|
||||
- library ui #03fa364
|
||||
- Scoping changes and removing qualifications #046ba64
|
||||
- Moved all files relevant to game downloads to their own directory #06d1e9e
|
||||
- SLowly integrating game_download into the FE. Started with using the manifest minimal example in the server (#1) #07379b2
|
||||
- Ran cargo clippy & moved DownloadManagerInterface #075d6ec
|
||||
- Made logging systems match #0a1dddf
|
||||
- Some easy cleanup of the download manager #0a2ac25
|
||||
- client now fetches user information from Drop server #0c0cfeb
|
||||
- Included in AppStatus (Also trying to link to Issue #1)
|
||||
- Accidentally serialized AppStatus and broke everything :/ #10791ed
|
||||
- Removed debugging statements #10c8344
|
||||
- Wrappers are the bane of my existence. Also here's the download cancelling logic. #13df631
|
||||
- Merge branch 'error-handling' #1520471
|
||||
- Updated README.md #165a967
|
||||
- Removed unnecessary dependencies #1724449
|
||||
- merge(download-manager) -> 'main' #172d6b0
|
||||
- More refactoring and renaming camelCase struct definitions to snake_case #1742793
|
||||
- General cleanup #182361e
|
||||
- Delete pages/library.vue #1861659
|
||||
- progress on more precise download control #18b9149
|
||||
- Allowing some dead code features because they are there for future use (potentially) #191e62c
|
||||
- Ensure that Downloadable is also send and sync #1a89135
|
||||
- I think that download queuing is working #1ab61c8
|
||||
- auth initiate, database and more #22b1aee
|
||||
- Update .gitlab-ci.yml" #2307704
|
||||
- More fleshing out on how specifically game downloads will work (#1) #23137dd
|
||||
- Removed utils.rs #270bc8b
|
||||
- Fixing some references to "id" vs "game_id" #27e5a8e
|
||||
- More cleanup after cargo clippy #2822b7a
|
||||
- Updated contributing link #2aa5b9c
|
||||
- More fleshing out on how specifically game downloads will work #2b90de9
|
||||
- Cleaning up downloads playing and pausing #2c7b5fb
|
||||
- fixed multi-chunk downloads #2ec351f
|
||||
- Clippy refactoring #2efe304
|
||||
- remove unpacker mod statement #32067c0
|
||||
- Progress on adding tools #3299c71
|
||||
- Fixed bug with bad initial loading into store instead of auth #3923acf
|
||||
- add nvm rc #3ccd444
|
||||
- partial download manager #3dbf5ab
|
||||
- Update .gitlab-ci.yml with artifacts #3e10f17
|
||||
- Removed tools/ #3eda979
|
||||
- Downloads should be fixed now #403ca65
|
||||
- transient vs synced state now defined #42c0198
|
||||
- added adenmgb's autostart feature #472eb1d
|
||||
- better download defaults #4779383
|
||||
- Progress on downloads. Currently working on parsing functions to be run asynchronously #496c6a5
|
||||
- Ran cargo clippy & cargo fmt #4983b25
|
||||
- handshakes #4bb33c8
|
||||
- Convert DOWNLOAD_MAX_THREADS to const #4fc13a1
|
||||
- Merge branch 'downloads' #50ed841
|
||||
- Moved generateGameMeta.ts to composables, using PathBuf instead of String for install_dirs #50f37fd
|
||||
- Added time debugging and fixed logging formatting #5243694
|
||||
- Clippy changes #553bc37
|
||||
- Queue is running game downloads sequentially now #5564d23
|
||||
- migrate to new droplet ca system #556898f
|
||||
- Add LICENSE #57a5737
|
||||
- ran cargo clippy & cargo fmt #5e3d26b
|
||||
- my own take on some BASED design decisions #5ed0833
|
||||
- cleanup and game UI beginnings #5ef6b8e
|
||||
- Progress on terminator #5f5cbd0
|
||||
- Implement better error system and segregate errors and commands (#23) #604d5b5
|
||||
- moved to completed index arr to help serialization #64ebc19
|
||||
- Ran cargo clippy & cargo fmt #653717e
|
||||
- Removed all references to anything outside of the DownloadManager #6568faa
|
||||
- Merge remote-tracking branch 'origin/main' #68ca4a7
|
||||
- swap file name and to binary encoding #694f2fd
|
||||
- chore(polish & cleanup) #6cc0c67
|
||||
- Update .gitlab-ci.yml #6d7630e
|
||||
- Moved some variable declarations outside of the spawned download thread #6ea4cf2
|
||||
- Encoding game IDs and versions #6ef444e
|
||||
- restructing and renaming #7049673
|
||||
- Converted to md5 #706f525
|
||||
- Merge branch 'main' into downloads #714b968
|
||||
- Semantic naming changes #725f16b
|
||||
- Abstracted queue system #76b0975
|
||||
- Moved manifest and stored_manifest to download_manager" #78149bb
|
||||
- README update #78fc668
|
||||
- Ensured everything is serializing/deserializing to camelCase #7a95b7f
|
||||
- fixed some of quexeky's BASED design decisions #7e3da04
|
||||
- Progress checker works #7fec00d
|
||||
- Progress on refactoring and abiding by cargo clippy #816b427
|
||||
- Added GAME_PAUSE_CHECK_INTERVAL value #8204795
|
||||
- Ran cargo clippy & fmt #82804eb
|
||||
- update metadata #85a0899
|
||||
- Renamed most instances of "game" outside of actual game downloads #881fcc6
|
||||
- Debugging & starting work on parsing manifest #89d2814
|
||||
- slight ui/ux fixes and updates to auth protocol #8a2d23d
|
||||
- Removed Arc requirement for DownloadableMetadata #8be1dd4
|
||||
- compliant with new APIs #8f6f184
|
||||
- Ran cargo clippy & cargo fmt #9272970
|
||||
- Added rolling_progress_updates.rs #9369ff1
|
||||
- Add files via upload #93b8b83
|
||||
- More refining info!() statements #94cf678
|
||||
- fixed windows issues #959dad3
|
||||
- Starting p2p progress #97bb1fa
|
||||
- Game downloads from the client are working (multithreaded) by parsing in gameID, GameVersion, and maxThreads from FE (#1) #984472e
|
||||
- Version bump & appimage build #9897698
|
||||
- Some progress on thread terminations #99beca4
|
||||
- rename files to what they contain #99c8b39
|
||||
- Created separate function to generate requests #9a184a8
|
||||
- cleanup of lib and toml #9b1cfa7
|
||||
- refactor for generic way to implement cross platform launchers #9ea2aa4
|
||||
- Updated logging format #a213765
|
||||
- fix(windows build) #a24cc8a
|
||||
- Added ToolDownloadAgent #a2e63aa
|
||||
- copy direct to disk #a628fc1
|
||||
- Moved manifest and stored_manifest to download_manager #a846eed
|
||||
- adds nvm rc! #a881d8e
|
||||
- Reordered DownloadThreadControlFlag to agree with From<bool> #ab606e8
|
||||
- ci/cd and patches for windows builds #ac1c3b6
|
||||
- patch for not draggable windows during setup #ac66b20
|
||||
- another stage of client authentication #ae4c65b
|
||||
- Renamed GameDonwloadError to ApplicationDownloadError and moved #aed58e4
|
||||
- Progress on write speeds & added debug statements #b065e10
|
||||
- Updated logging #b3963b6
|
||||
- Created file settings.rs #b47b7ea
|
||||
- Added Downloadable trait and replaced references to GameDownloadAgent #b4d70a3
|
||||
- Update .gitlab-ci.yml #b6a54c0
|
||||
- Moved download manager to separate directory #b6c64e5
|
||||
- Ran cargo fmt #b8cf44c
|
||||
- Imported appropriate logging macros #b99ff67
|
||||
- Merge branch 'main' into download-manager #bb60942
|
||||
- Ran cargo clippy & cargo fmt #bd3deac
|
||||
- beginnings of game state management #bf46dec
|
||||
- Update Cargo.toml #c1fb39e
|
||||
- migrated unpacking to rust zstd to conform with droplet #c46c54b
|
||||
- More progress on checksums #c51e761
|
||||
- Delete pages/library.vue #c722a54
|
||||
- Merge branch 'downloads' (again) #c748aec
|
||||
- migrate to nuxt and groundwork #c957744
|
||||
- More debugging because apparently checksums are the bane of my existence. But it works and I was just an idiot #c9d9d2e
|
||||
- Fully separate & generic download manager #cac612b
|
||||
- Progress on rolling progress window #cf19477
|
||||
- Ensured that all logs start with lowercase capital and have no trailing punctuation #cfc9d13
|
||||
- Validated that loading data works #d21b1d2
|
||||
- Mostly finished with checksums. Just merging main in at the same time #d39e7cb
|
||||
- Ran cargo clippy #dcb1564
|
||||
- Add files via upload #dcb2c0f
|
||||
- Theoretically adding queue support and optimistic manifest downloading (#1). Needs tests when actual functions are implemented #dcd8fa8
|
||||
- Merge remote-tracking branch 'origin/downloads' into downloads #dd23ca8
|
||||
- Debugging line #ddc585d
|
||||
- Re-enabled closing the window and some more renaming #defba51
|
||||
- drop no longer freaks out if server is unavailable on startup #df88395
|
||||
- Apply stashed changes #e0ea8c9
|
||||
- Merge remote-tracking branch 'origin/downloads' into downloads #e4e605b
|
||||
- convert to more sensible permission schema #e504c00
|
||||
- Update on GameDownload #e71e4cf
|
||||
- reorganisation, cleanup and new nonce protocol #e828bca
|
||||
- rustix fs feature #e9805a8
|
||||
- Added manage_go_signal command #ea70ec9
|
||||
- Drop will no longer crash when the server goes down #eb3311a
|
||||
- Made all errors type-based #ec2f414
|
||||
- Added description on how the DownloadManager works #f029cbf
|
||||
- Using more appropriate logging statements #f183a9d
|
||||
- remove unnecessary compat code (#20) #f1c8bbf
|
||||
- Manifests are now being parsed successfully #f28c880
|
||||
- Removed tests/ #f29e989
|
||||
- I think that downloads are working. Need to test and set decent file locations now #f388237
|
||||
- Just debugging tauri's damn Sync command features #f60ca2b
|
||||
- fixes and patches for merged changes #f6476bc
|
||||
- Added manage_queue_signal #f64782e
|
||||
- initial commit #f6cd7c3
|
||||
- Update .gitlab-ci.yml #fc6bab9
|
||||
|
||||
|
||||
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
|
||||
|
||||
## Release 0.1.0-beta
|
||||
|
||||
### Fixes
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ import { listen } from "@tauri-apps/api/event";
|
|||
import type { DownloadableMetadata } from "~/types";
|
||||
|
||||
export type QueueState = {
|
||||
queue: Array<{ meta: DownloadableMetadata; status: string; progress: number | null }>;
|
||||
queue: Array<{
|
||||
meta: DownloadableMetadata;
|
||||
status: string;
|
||||
progress: number | null;
|
||||
current: number;
|
||||
max: number;
|
||||
}>;
|
||||
status: string;
|
||||
};
|
||||
|
||||
|
|
@ -25,4 +31,4 @@ listen("update_queue", (event) => {
|
|||
listen("update_stats", (event) => {
|
||||
const stats = useStatsState();
|
||||
stats.value = event.payload as StatsState;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,9 +48,7 @@ export function initialNavigation(state: Ref<AppState>) {
|
|||
|
||||
switch (state.value.status) {
|
||||
case AppStatus.NotConfigured:
|
||||
router.push({ path: "/setup" }).then(() => {
|
||||
console.log("Pushed Setup");
|
||||
});
|
||||
router.push({ path: "/setup" });
|
||||
break;
|
||||
case AppStatus.SignedOut:
|
||||
router.push("/auth");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="flex flex-col bg-zinc-900 overflow-hidden">
|
||||
<Header class="select-none" />
|
||||
<div class="grow overflow-y-auto">
|
||||
<div class="relative grow overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="flex flex-col bg-zinc-950 overflow-hidden">
|
||||
<MiniHeader />
|
||||
<div class="grow overflow-y-auto">
|
||||
<div class="relative grow overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "drop-app",
|
||||
"private": true,
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0-beta",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
|
||||
const route = useRoute();
|
||||
const message = route.query.error ?? "An unknown error occurred.";
|
||||
const message = route.query.error ?? "An unknown error occurred";
|
||||
|
||||
definePageMeta({
|
||||
layout: "mini",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<template />
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: false
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="grow overflow-y-auto">
|
||||
<NuxtPage />
|
||||
<NuxtPage :libraryDownloadError = "libraryDownloadError" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -40,7 +40,19 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
|
||||
|
||||
const rawGames: Array<Game> = await invoke("fetch_library");
|
||||
let libraryDownloadError = false;
|
||||
|
||||
async function calculateGames(): Promise<Game[]> {
|
||||
try {
|
||||
return await invoke("fetch_library");
|
||||
}
|
||||
catch(e) {
|
||||
libraryDownloadError = true;
|
||||
return new Array();
|
||||
}
|
||||
}
|
||||
|
||||
const rawGames: Array<Game> = await calculateGames();
|
||||
const games = await Promise.all(rawGames.map((e) => useGame(e.id)));
|
||||
const icons = await Promise.all(
|
||||
games.map(({ game, status }) => useObject(game.mIconId))
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@
|
|||
"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="w-full sm:w-fit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
>
|
||||
Install
|
||||
</LoadingButton>
|
||||
|
|
@ -306,6 +306,7 @@ async function installFlow() {
|
|||
versionOptions.value = await invoke("fetch_game_verion_options", {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
console.log(versionOptions.value);
|
||||
installDirs.value = await invoke("fetch_download_dir_stats");
|
||||
} catch (error) {
|
||||
installError.value = (error as string).toString();
|
||||
|
|
@ -319,19 +320,19 @@ const installDir = ref(0);
|
|||
async function install() {
|
||||
try {
|
||||
if (!versionOptions.value)
|
||||
throw new Error("Versions have not been loaded.");
|
||||
throw new Error("Versions have not been loaded");
|
||||
installLoading.value = true;
|
||||
await invoke("download_game", {
|
||||
gameId: game.value.id,
|
||||
gameVersion: versionOptions.value[installVersionIndex.value].versionName,
|
||||
installDir: installDir.value,
|
||||
});
|
||||
installLoading.value = false;
|
||||
|
||||
installFlowOpen.value = false;
|
||||
} catch (error) {
|
||||
installError.value = (error as string).toString();
|
||||
}
|
||||
|
||||
installLoading.value = false;
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{ libraryDownloadError: boolean }>();
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<div v-if="libraryDownloadError" class="mx-auto pt-10 text-center text-gray-500">
|
||||
Library Failed to update
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
|
@ -1,23 +1,37 @@
|
|||
<template>
|
||||
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
|
||||
<div class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900">
|
||||
<div
|
||||
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
|
||||
>
|
||||
<div
|
||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2">
|
||||
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}</span>
|
||||
<span v-if="stats.time > 0" class="text-sm">{{ formatTime(stats.time) }} left</span>
|
||||
|
||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
|
||||
>
|
||||
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
|
||||
<span v-if="stats.time > 0" class="text-sm"
|
||||
>{{ formatTime(stats.time) }} left</span
|
||||
>
|
||||
</div>
|
||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
||||
<div v-for="bar in speedHistory" :style="{ height: `${bar / speedMax * 100}%` }"
|
||||
class="w-[8px] bg-blue-600/40" />
|
||||
<div
|
||||
v-for="bar in speedHistory"
|
||||
:style="{ height: `${(bar / speedMax) * 100}%` }"
|
||||
class="w-[8px] bg-blue-600/40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<draggable v-model="queue.queue" @end="onEnd">
|
||||
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
|
||||
<li v-if="games[element.meta.id]" :key="element.meta.id"
|
||||
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4">
|
||||
<li
|
||||
v-if="games[element.meta.id]"
|
||||
:key="element.meta.id"
|
||||
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"
|
||||
>
|
||||
<div class="w-full flex items-center max-w-md gap-x-4 relative">
|
||||
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="games[element.meta.id].cover" alt="" />
|
||||
<img
|
||||
class="size-24 flex-none bg-zinc-800 object-cover rounded"
|
||||
:src="games[element.meta.id].cover"
|
||||
alt=""
|
||||
/>
|
||||
<div class="min-w-0 flex-auto">
|
||||
<p class="text-xl font-semibold text-zinc-100">
|
||||
<NuxtLink :href="`/library/${element.meta.id}`" class="">
|
||||
|
|
@ -35,47 +49,68 @@
|
|||
<p class="text-md text-zinc-500 uppercase font-display font-bold">
|
||||
{{ element.status }}
|
||||
</p>
|
||||
<div v-if="element.progress" class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden">
|
||||
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" />
|
||||
<div
|
||||
v-if="element.progress"
|
||||
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-2 bg-blue-600"
|
||||
:style="{ width: `${element.progress * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
|
||||
><span class="text-zinc-300">{{
|
||||
formatKilobytes(element.current / 1000)
|
||||
}}</span>
|
||||
/
|
||||
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
|
||||
><ServerIcon class="size-5"
|
||||
/></span>
|
||||
</div>
|
||||
<button @click="() => cancelGame(element.meta)" class="group">
|
||||
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
||||
aria-hidden="true" />
|
||||
<XMarkIcon
|
||||
class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<p v-else>Loading...</p>
|
||||
</template>
|
||||
</draggable>
|
||||
<div class="text-zinc-600 uppercase font-semibold font-display w-full text-center" v-if="queue.queue.length == 0">
|
||||
<div
|
||||
class="text-zinc-600 uppercase font-semibold font-display w-full text-center"
|
||||
v-if="queue.queue.length == 0"
|
||||
>
|
||||
No items in the queue
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
|
||||
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
window.addEventListener('resize', (event) => {
|
||||
window.addEventListener("resize", (event) => {
|
||||
windowWidth.value = window.innerWidth;
|
||||
})
|
||||
});
|
||||
|
||||
const queue = useQueueState();
|
||||
const stats = useStatsState();
|
||||
const speedHistory = useState<Array<number>>(() => []);
|
||||
const speedHistoryMax = computed(() => windowWidth.value / 8);
|
||||
const speedMax = computed(() => speedHistory.value.reduce((a, b) => a > b ? a : b) * 1.3);
|
||||
const speedMax = computed(
|
||||
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
|
||||
);
|
||||
const previousGameId = ref<string | undefined>();
|
||||
|
||||
const games: Ref<{
|
||||
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
||||
}> = ref({});
|
||||
|
||||
|
||||
function resetHistoryGraph() {
|
||||
speedHistory.value = [];
|
||||
stats.value = { time: 0, speed: 0 };
|
||||
|
|
@ -97,8 +132,7 @@ function checkReset(v: QueueState) {
|
|||
return;
|
||||
}
|
||||
// If it's a different game now
|
||||
if (currentGame != previousGameId.value
|
||||
) {
|
||||
if (currentGame != previousGameId.value) {
|
||||
previousGameId.value = currentGame;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
|
|
@ -115,10 +149,12 @@ watch(stats, (v) => {
|
|||
speedHistory.value.splice(0, 1);
|
||||
}
|
||||
checkReset(queue.value);
|
||||
})
|
||||
});
|
||||
|
||||
function loadGamesForQueue(v: typeof queue.value) {
|
||||
for (const { meta: { id } } of v.queue) {
|
||||
for (const {
|
||||
meta: { id },
|
||||
} of v.queue) {
|
||||
if (games.value[id]) return;
|
||||
(async () => {
|
||||
const gameData = await useGame(id);
|
||||
|
|
@ -131,7 +167,7 @@ function loadGamesForQueue(v: typeof queue.value) {
|
|||
loadGamesForQueue(queue.value);
|
||||
|
||||
async function onEnd(event: { oldIndex: number; newIndex: number }) {
|
||||
await invoke("move_game_in_queue", {
|
||||
await invoke("move_download_in_queue", {
|
||||
oldIndex: event.oldIndex,
|
||||
newIndex: event.newIndex,
|
||||
});
|
||||
|
|
@ -152,7 +188,7 @@ function formatKilobytes(bytes: number): string {
|
|||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}/s`;
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
|
|
@ -162,7 +198,7 @@ function formatTime(seconds: number): string {
|
|||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ${Math.round(seconds % 60)}s`
|
||||
return `${minutes}m ${Math.round(seconds % 60)}s`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
|
|
|||
|
|
@ -79,11 +79,14 @@
|
|||
min="1"
|
||||
max="32"
|
||||
v-model="downloadThreads"
|
||||
@keypress="validateNumberInput"
|
||||
@paste="validatePaste"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400">
|
||||
The maximum number of concurrent download threads. Higher values may download faster but use more system resources. Default is 4.
|
||||
The maximum number of concurrent download threads. Higher values may
|
||||
download faster but use more system resources. Default is 4.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -91,9 +94,16 @@
|
|||
<button
|
||||
type="button"
|
||||
@click="saveDownloadThreads"
|
||||
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:bg-blue-600/50 disabled:cursor-not-allowed"
|
||||
:disabled="saveState.loading"
|
||||
:class="[
|
||||
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
|
||||
saveState.success
|
||||
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
||||
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
|
||||
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
|
||||
]"
|
||||
>
|
||||
Save Changes
|
||||
{{ saveState.success ? 'Saved' : 'Save Changes' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -210,6 +220,7 @@ import {
|
|||
} from "@headlessui/vue";
|
||||
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { type Settings } from "~/types";
|
||||
|
||||
const open = ref(false);
|
||||
const currentDirectory = ref<string | undefined>(undefined);
|
||||
|
|
@ -218,8 +229,13 @@ const createDirectoryLoading = ref(false);
|
|||
|
||||
const dirs = ref<Array<string>>([]);
|
||||
|
||||
const downloadThreads = ref(4);
|
||||
const downloadThreadsChanged = computed(() => downloadThreads.value !== 4);
|
||||
const settings = await invoke<Settings>("fetch_settings");
|
||||
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
|
||||
|
||||
const saveState = reactive({
|
||||
loading: false,
|
||||
success: false
|
||||
});
|
||||
|
||||
async function updateDirs() {
|
||||
const newDirs = await invoke<Array<string>>("fetch_download_dir_stats");
|
||||
|
|
@ -254,7 +270,7 @@ async function submitDirectory() {
|
|||
try {
|
||||
error.value = undefined;
|
||||
if (!currentDirectory.value)
|
||||
throw new Error("Please select a directory first.");
|
||||
throw new Error("Please select a directory first");
|
||||
createDirectoryLoading.value = true;
|
||||
|
||||
// Add directory
|
||||
|
|
@ -278,7 +294,40 @@ async function deleteDirectory(index: number) {
|
|||
}
|
||||
|
||||
async function saveDownloadThreads() {
|
||||
//Would save download threads downloadThreads.value);
|
||||
await invoke("update_settings", { newSettings: { maxDownloadThreads: downloadThreads.value } })
|
||||
try {
|
||||
saveState.loading = true;
|
||||
await invoke("update_settings", {
|
||||
newSettings: { maxDownloadThreads: downloadThreads.value },
|
||||
});
|
||||
|
||||
// Show success state
|
||||
saveState.success = true;
|
||||
|
||||
// Reset back to normal state after 2 seconds
|
||||
setTimeout(() => {
|
||||
saveState.success = false;
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
} finally {
|
||||
saveState.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumberInput(event: KeyboardEvent) {
|
||||
// Allow only numbers and basic control keys
|
||||
if (!/^\d$/.test(event.key) &&
|
||||
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function validatePaste(event: ClipboardEvent) {
|
||||
// Prevent paste if content contains non-numeric characters
|
||||
const pastedData = event.clipboardData?.getData('text');
|
||||
if (pastedData && !/^\d+$/.test(pastedData)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
// Also possible
|
||||
nuxtApp.hook("vue:error", (error, instance, info) => {
|
||||
console.log(error);
|
||||
console.error(error, info);
|
||||
const router = useRouter();
|
||||
router.replace(`/error`);
|
||||
});
|
||||
|
|
|
|||
64
src-tauri/Cargo.lock
generated
64
src-tauri/Cargo.lock
generated
|
|
@ -255,6 +255,12 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-instant-full"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db6541700e074cda41b1c6f98c2cae6cde819967bf142078f069cad85387cdbe"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
|
|
@ -846,37 +852,6 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.91",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.91",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.18"
|
||||
|
|
@ -1031,18 +1006,17 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "drop-app"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0-beta-prerelease-1"
|
||||
dependencies = [
|
||||
"atomic-instant-full",
|
||||
"boxcar",
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"directories",
|
||||
"hex",
|
||||
"http",
|
||||
"log",
|
||||
"log4rs",
|
||||
"md5",
|
||||
"merge-struct",
|
||||
"openssl",
|
||||
"parking_lot 0.12.3",
|
||||
"rayon",
|
||||
|
|
@ -1052,7 +1026,6 @@ dependencies = [
|
|||
"serde",
|
||||
"serde-binary",
|
||||
"serde_json",
|
||||
"serde_merge",
|
||||
"serde_with",
|
||||
"shared_child",
|
||||
"slice-deque",
|
||||
|
|
@ -2455,16 +2428,6 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "merge-struct"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d82012d21e24135b839b6b9bebd622b7ff0cb40071498bc2d066d3a6d04dd4a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
|
@ -3913,17 +3876,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_merge"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "606e91878516232ac3b16c12e063d4468d762f16d77e7aef14a1f2326c5f409b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0-beta-prerelease-1"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2021"
|
||||
|
|
@ -47,11 +47,9 @@ tauri-plugin-autostart = "2.0.0"
|
|||
shared_child = "1.0.1"
|
||||
serde_with = "3.12.0"
|
||||
slice-deque = "0.3.0"
|
||||
derive_builder = "0.20.2"
|
||||
throttle_my_fn = "0.2.6"
|
||||
parking_lot = "0.12.3"
|
||||
merge-struct = "0.1.0"
|
||||
serde_merge = "0.1.3"
|
||||
atomic-instant-full = "0.1.0"
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.1.1"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::DB;
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||
use log::debug;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
|
|
@ -7,24 +7,24 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
|
|||
let manager = app.autolaunch();
|
||||
if enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
debug!("Enabled autostart");
|
||||
debug!("enabled autostart");
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
debug!("Disabled autostart");
|
||||
debug!("eisabled autostart");
|
||||
}
|
||||
|
||||
// Store the state in DB
|
||||
let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.settings.autostart = enabled;
|
||||
drop(db_handle);
|
||||
DB.save().map_err(|e| e.to_string())?;
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
// First check DB state
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let db_handle = borrow_db_checked();
|
||||
let db_state = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_
|
|||
|
||||
// New function to sync state on startup
|
||||
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
|
||||
let db_handle = borrow_db_checked();
|
||||
let should_be_enabled = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
|
|
@ -56,10 +56,10 @@ pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
|||
if current_state != should_be_enabled {
|
||||
if should_be_enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
debug!("Synced autostart: enabled");
|
||||
debug!("synced autostart: enabled");
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
debug!("Synced autostart: disabled");
|
||||
debug!("synced autostart: disabled");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
use log::{debug, info};
|
||||
use log::{debug, error};
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn quit(app: tauri::AppHandle) {
|
||||
cleanup_and_exit(&app);
|
||||
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
cleanup_and_exit(&app, &state);
|
||||
}
|
||||
|
||||
pub fn cleanup_and_exit(app: &AppHandle) {
|
||||
debug!("Cleaning up and exiting application");
|
||||
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
debug!("cleaning up and exiting application");
|
||||
let download_manager = state.lock().unwrap().download_manager.clone();
|
||||
match download_manager.ensure_terminated() {
|
||||
Ok(res) => match res {
|
||||
Ok(_) => debug!("download manager terminated correctly"),
|
||||
Err(_) => error!("download manager failed to terminate correctly"),
|
||||
},
|
||||
Err(e) => panic!("{:?}", e),
|
||||
}
|
||||
|
||||
app.exit(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
use tauri::AppHandle;
|
||||
|
||||
use crate::{
|
||||
autostart::{get_autostart_enabled_logic, toggle_autostart_logic},
|
||||
AppState,
|
||||
};
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_state(
|
||||
|
|
|
|||
|
|
@ -6,74 +6,83 @@ use std::{
|
|||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{database::settings::Settings, error::user_error::UserValue, DB};
|
||||
use crate::{
|
||||
database::{db::borrow_db_mut_checked, settings::Settings},
|
||||
download_manager::internal_error::InternalError,
|
||||
};
|
||||
|
||||
use super::{db::DATA_ROOT_DIR, debug::SystemData};
|
||||
use super::{
|
||||
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
|
||||
debug::SystemData,
|
||||
};
|
||||
|
||||
// Will, in future, return disk/remaining size
|
||||
// Just returns the directories that have been set up
|
||||
#[tauri::command]
|
||||
pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
||||
let lock = DB.borrow_data().unwrap();
|
||||
let lock = borrow_db_checked();
|
||||
lock.applications.install_dirs.clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_download_dir(index: usize) {
|
||||
let mut lock = DB.borrow_data_mut().unwrap();
|
||||
let mut lock = borrow_db_mut_checked();
|
||||
lock.applications.install_dirs.remove(index);
|
||||
drop(lock);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_download_dir(new_dir: PathBuf) -> UserValue<(), Error> {
|
||||
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
|
||||
// Check the new directory is all good
|
||||
let new_dir_path = Path::new(&new_dir);
|
||||
if new_dir_path.exists() {
|
||||
let dir_contents = new_dir_path.read_dir()?;
|
||||
if dir_contents.count() != 0 {
|
||||
return UserValue::Err(Error::new(
|
||||
return Err(Error::new(
|
||||
ErrorKind::DirectoryNotEmpty,
|
||||
"Selected directory cannot contain any existing files",
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
create_dir_all(new_dir_path)?;
|
||||
}
|
||||
|
||||
// Add it to the dictionary
|
||||
let mut lock = DB.borrow_data_mut().unwrap();
|
||||
let mut lock = borrow_db_mut_checked();
|
||||
if lock.applications.install_dirs.contains(&new_dir) {
|
||||
return UserValue::Err(Error::new(
|
||||
return Err(Error::new(
|
||||
ErrorKind::AlreadyExists,
|
||||
"Selected directory already exists in database",
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
UserValue::Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_settings(new_settings: Value) {
|
||||
println!("{}", new_settings);
|
||||
let mut db_lock = DB.borrow_data_mut().unwrap();
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
|
||||
for (key, value) in new_settings.as_object().unwrap() {
|
||||
current_settings[key] = value.clone();
|
||||
}
|
||||
println!("New settings unset: {}", ¤t_settings);
|
||||
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
|
||||
db_lock.settings = new_settings;
|
||||
println!("New Settings: {:?}", db_lock.settings);
|
||||
println!("new Settings: {:?}", db_lock.settings);
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn fetch_settings() -> Settings {
|
||||
borrow_db_checked().settings.clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_system_data() -> SystemData {
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let db_handle = borrow_db_checked();
|
||||
SystemData::new(
|
||||
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||
db_handle.base_url.clone(),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, create_dir_all},
|
||||
hash::Hash,
|
||||
path::{Path, PathBuf},
|
||||
sync::{LazyLock, Mutex, RwLockWriteGuard},
|
||||
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use directories::BaseDirs;
|
||||
use log::{debug, info};
|
||||
use log::{debug, error, info};
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
|
@ -56,11 +57,23 @@ pub enum ApplicationTransientStatus {
|
|||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameVersion {
|
||||
pub version_index: usize,
|
||||
pub game_id: String,
|
||||
pub version_name: String,
|
||||
pub launch_command: String,
|
||||
pub setup_command: String,
|
||||
|
||||
pub platform: Platform,
|
||||
|
||||
pub launch_command: String,
|
||||
pub launch_args: Vec<String>,
|
||||
|
||||
pub setup_command: String,
|
||||
pub setup_args: Vec<String>,
|
||||
|
||||
pub only_setup: bool,
|
||||
|
||||
pub version_index: usize,
|
||||
pub delta: bool,
|
||||
|
||||
pub umu_id_override: Option<String>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
|
@ -91,10 +104,18 @@ impl Database {
|
|||
Self {
|
||||
applications: DatabaseApplications {
|
||||
install_dirs: vec![games_base_dir.into()],
|
||||
..Default::default()
|
||||
game_statuses: HashMap::new(),
|
||||
game_versions: HashMap::new(),
|
||||
installed_game_version: HashMap::new(),
|
||||
transient_statuses: HashMap::new(),
|
||||
},
|
||||
prev_database,
|
||||
..Default::default()
|
||||
base_url: "".to_owned(),
|
||||
auth: None,
|
||||
settings: Settings {
|
||||
autostart: false,
|
||||
max_download_threads: 4,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +151,7 @@ impl DatabaseImpls for DatabaseInterface {
|
|||
let games_base_dir = data_root_dir.join("games");
|
||||
let logs_root_dir = data_root_dir.join("logs");
|
||||
|
||||
debug!("Creating data directory at {:?}", data_root_dir);
|
||||
debug!("creating data directory at {:?}", data_root_dir);
|
||||
create_dir_all(data_root_dir.clone()).unwrap();
|
||||
create_dir_all(games_base_dir.clone()).unwrap();
|
||||
create_dir_all(logs_root_dir.clone()).unwrap();
|
||||
|
|
@ -169,10 +190,10 @@ pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &Downloada
|
|||
meta: DownloadableMetadata,
|
||||
setter: F,
|
||||
) {
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
setter(&mut db_handle, &meta);
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
let status = GameStatusManager::fetch_state(&meta.id);
|
||||
|
||||
|
|
@ -187,16 +208,49 @@ fn handle_invalid_database(
|
|||
let new_path = {
|
||||
let time = Utc::now().timestamp();
|
||||
let mut base = db_path.clone();
|
||||
base.set_file_name(format!("drop.db.backup-{}", time.to_string()));
|
||||
base.set_file_name(format!("drop.db.backup-{}", time));
|
||||
base
|
||||
};
|
||||
info!("{:?}", new_path);
|
||||
info!(
|
||||
"old database stored at: {}",
|
||||
new_path.to_string_lossy().to_string()
|
||||
);
|
||||
fs::rename(&db_path, &new_path).unwrap();
|
||||
|
||||
let db = Database::new(
|
||||
games_base_dir.into_os_string().into_string().unwrap(),
|
||||
Some(new_path.into()),
|
||||
Some(new_path),
|
||||
);
|
||||
|
||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||
}
|
||||
|
||||
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
|
||||
match DB.borrow_data() {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
error!("database borrow failed with error {}", e);
|
||||
panic!("database borrow failed with error {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
|
||||
match DB.borrow_data_mut() {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
error!("database borrow mut failed with error {}", e);
|
||||
panic!("database borrow mut failed with error {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_db() {
|
||||
match DB.save() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("database failed to save with error {}", e);
|
||||
panic!("database failed to save with error {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::{DATA_ROOT_DIR, DB};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::sync::{mpsc::SendError, Arc, Mutex};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ use std::{
|
|||
fmt::Debug,
|
||||
sync::{
|
||||
mpsc::{SendError, Sender},
|
||||
MutexGuard,
|
||||
Mutex, MutexGuard,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use log::info;
|
||||
use log::{debug, info};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::error::application_download_error::ApplicationDownloadError;
|
||||
|
|
@ -84,7 +84,7 @@ pub enum DownloadStatus {
|
|||
/// which provides raw access to the underlying queue.
|
||||
/// THIS EDITING IS BLOCKING!!!
|
||||
pub struct DownloadManager {
|
||||
terminator: JoinHandle<Result<(), ()>>,
|
||||
terminator: Mutex<Option<JoinHandle<Result<(), ()>>>>,
|
||||
download_queue: Queue,
|
||||
progress: CurrentProgressObject,
|
||||
command_sender: Sender<DownloadManagerSignal>,
|
||||
|
|
@ -99,7 +99,7 @@ impl DownloadManager {
|
|||
command_sender: Sender<DownloadManagerSignal>,
|
||||
) -> Self {
|
||||
Self {
|
||||
terminator,
|
||||
terminator: Mutex::new(Some(terminator)),
|
||||
download_queue,
|
||||
progress,
|
||||
command_sender,
|
||||
|
|
@ -110,7 +110,7 @@ impl DownloadManager {
|
|||
&self,
|
||||
download: DownloadAgent,
|
||||
) -> Result<(), SendError<DownloadManagerSignal>> {
|
||||
info!("Adding download id {:?}", download.metadata());
|
||||
info!("creating download with meta {:?}", download.metadata());
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Queue(download))?;
|
||||
self.command_sender.send(DownloadManagerSignal::Go)
|
||||
|
|
@ -151,12 +151,14 @@ impl DownloadManager {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
info!("moving {} to {}", current_index, new_index);
|
||||
debug!(
|
||||
"moving download at index {} to index {}",
|
||||
current_index, new_index
|
||||
);
|
||||
|
||||
let mut queue = self.edit();
|
||||
let to_move = queue.remove(current_index).unwrap();
|
||||
queue.insert(new_index, to_move);
|
||||
info!("new queue: {:?}", queue);
|
||||
drop(queue);
|
||||
|
||||
if needs_pause {
|
||||
|
|
@ -165,6 +167,10 @@ impl DownloadManager {
|
|||
self.command_sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Go)
|
||||
.unwrap();
|
||||
|
||||
}
|
||||
pub fn pause_downloads(&self) {
|
||||
self.command_sender
|
||||
|
|
@ -174,11 +180,12 @@ impl DownloadManager {
|
|||
pub fn resume_downloads(&self) {
|
||||
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
pub fn ensure_terminated(self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
|
||||
pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Finish)
|
||||
.unwrap();
|
||||
self.terminator.join()
|
||||
let terminator = self.terminator.lock().unwrap().take();
|
||||
terminator.unwrap().join()
|
||||
}
|
||||
pub fn uninstall_application(&self, meta: DownloadableMetadata) {
|
||||
self.command_sender
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::{
|
|||
thread::{spawn, JoinHandle},
|
||||
};
|
||||
|
||||
use log::{debug, error, info};
|
||||
use log::{debug, error, info, warn};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -179,13 +179,13 @@ impl DownloadManagerBuilder {
|
|||
}
|
||||
}
|
||||
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||
info!("Got signal Queue");
|
||||
debug!("got signal Queue");
|
||||
let meta = download_agent.metadata();
|
||||
|
||||
info!("Meta: {:?}", meta);
|
||||
debug!("queue metadata: {:?}", meta);
|
||||
|
||||
if self.download_queue.exists(meta.clone()) {
|
||||
info!("Download with same ID already exists");
|
||||
warn!("download with same ID already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -199,9 +199,9 @@ impl DownloadManagerBuilder {
|
|||
}
|
||||
|
||||
fn manage_go_signal(&mut self) {
|
||||
info!("Got signal Go");
|
||||
debug!("got signal Go");
|
||||
if self.download_agent_registry.is_empty() {
|
||||
info!(
|
||||
debug!(
|
||||
"Download agent registry: {:?}",
|
||||
self.download_agent_registry.len()
|
||||
);
|
||||
|
|
@ -209,19 +209,21 @@ impl DownloadManagerBuilder {
|
|||
}
|
||||
|
||||
if self.current_download_agent.is_some() {
|
||||
info!(
|
||||
"Current download agent: {:?}",
|
||||
self.current_download_agent.as_ref().unwrap().metadata()
|
||||
);
|
||||
return;
|
||||
if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() {
|
||||
debug!(
|
||||
"Current download agent: {:?}",
|
||||
self.current_download_agent.as_ref().unwrap().metadata()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Current download queue: {:?}", self.download_queue.read());
|
||||
debug!("current download queue: {:?}", self.download_queue.read());
|
||||
|
||||
// Should always be Some if the above two statements keep going
|
||||
let agent_data = self.download_queue.read().front().unwrap().clone();
|
||||
|
||||
info!("Starting download for {:?}", agent_data);
|
||||
info!("starting download for {:?}", agent_data);
|
||||
|
||||
let download_agent = self
|
||||
.download_agent_registry
|
||||
|
|
@ -265,7 +267,7 @@ impl DownloadManagerBuilder {
|
|||
active_control_flag.set(DownloadThreadControlFlag::Go);
|
||||
}
|
||||
fn manage_stop_signal(&mut self) {
|
||||
info!("Got signal Stop");
|
||||
debug!("got signal Stop");
|
||||
|
||||
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
|
|
@ -273,10 +275,9 @@ impl DownloadManagerBuilder {
|
|||
}
|
||||
}
|
||||
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
||||
info!("Got signal Completed");
|
||||
debug!("got signal Completed");
|
||||
if let Some(interface) = &self.current_download_agent {
|
||||
if interface.metadata() == meta {
|
||||
info!("Popping consumed data");
|
||||
self.remove_and_cleanup_front_download(&meta);
|
||||
}
|
||||
}
|
||||
|
|
@ -284,18 +285,17 @@ impl DownloadManagerBuilder {
|
|||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||
info!("Got signal Error");
|
||||
let current_agent = self.current_download_agent.clone().unwrap();
|
||||
|
||||
current_agent.on_error(&self.app_handle, error.clone());
|
||||
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||
debug!("got signal Error");
|
||||
if let Some(current_agent) = self.current_download_agent.clone() {
|
||||
current_agent.on_error(&self.app_handle, error.clone());
|
||||
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||
}
|
||||
self.set_status(DownloadManagerStatus::Error(error));
|
||||
}
|
||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||
info!("Got signal Cancel");
|
||||
debug!("got signal Cancel");
|
||||
|
||||
if let Some(current_download) = &self.current_download_agent {
|
||||
if ¤t_download.metadata() == meta {
|
||||
|
|
@ -306,32 +306,30 @@ impl DownloadManagerBuilder {
|
|||
self.download_queue.pop_front();
|
||||
|
||||
self.cleanup_current_download();
|
||||
info!("Current donwload queue: {:?}", self.download_queue.read());
|
||||
debug!("current download queue: {:?}", self.download_queue.read());
|
||||
}
|
||||
// TODO: Collapse these two into a single if statement somehow
|
||||
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||
info!("Object exists in registry");
|
||||
let index = self.download_queue.get_by_meta(meta);
|
||||
if let Some(index) = index {
|
||||
download_agent.on_cancelled(&self.app_handle);
|
||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||
let removed = self.download_agent_registry.remove(meta);
|
||||
info!(
|
||||
"Removed {:?} from queue {:?}",
|
||||
debug!(
|
||||
"removed {:?} from queue {:?}",
|
||||
removed.map(|x| x.metadata()),
|
||||
self.download_queue.read()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||
info!("Object exists in registry");
|
||||
let index = self.download_queue.get_by_meta(meta);
|
||||
if let Some(index) = index {
|
||||
download_agent.on_cancelled(&self.app_handle);
|
||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||
let removed = self.download_agent_registry.remove(meta);
|
||||
info!(
|
||||
"Removed {:?} from queue {:?}",
|
||||
debug!(
|
||||
"removed {:?} from queue {:?}",
|
||||
removed.map(|x| x.metadata()),
|
||||
self.download_queue.read()
|
||||
);
|
||||
|
|
@ -354,6 +352,8 @@ impl DownloadManagerBuilder {
|
|||
meta: DownloadableMetadata::clone(key),
|
||||
status: val.status(),
|
||||
progress: val.progress().get_progress(),
|
||||
current: val.progress().sum(),
|
||||
max: val.progress().get_max(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
27
src-tauri/src/download_manager/internal_error.rs
Normal file
27
src-tauri/src/download_manager/internal_error.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use std::{fmt::Display, io, sync::mpsc::SendError};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(SerializeDisplay)]
|
||||
pub enum InternalError<T> {
|
||||
IOError(io::Error),
|
||||
SignalError(SendError<T>),
|
||||
}
|
||||
impl<T> Display for InternalError<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
InternalError::IOError(error) => write!(f, "{}", error),
|
||||
InternalError::SignalError(send_error) => write!(f, "{}", send_error),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> From<SendError<T>> for InternalError<T> {
|
||||
fn from(value: SendError<T>) -> Self {
|
||||
InternalError::SignalError(value)
|
||||
}
|
||||
}
|
||||
impl<T> From<io::Error> for InternalError<T> {
|
||||
fn from(value: io::Error) -> Self {
|
||||
InternalError::IOError(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,5 +4,7 @@ pub mod download_manager_builder;
|
|||
pub mod download_thread_control_flag;
|
||||
pub mod downloadable;
|
||||
pub mod downloadable_metadata;
|
||||
pub mod internal_error;
|
||||
pub mod progress_object;
|
||||
pub mod queue;
|
||||
pub mod rolling_progress_updates;
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ use std::{
|
|||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::Sender,
|
||||
Arc, Mutex, RwLock,
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use log::info;
|
||||
use atomic_instant_full::AtomicInstant;
|
||||
use throttle_my_fn::throttle;
|
||||
|
||||
use super::download_manager::DownloadManagerSignal;
|
||||
use super::{
|
||||
download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgressObject {
|
||||
|
|
@ -18,10 +20,10 @@ pub struct ProgressObject {
|
|||
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
|
||||
start: Arc<Mutex<Instant>>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
points_towards_update: Arc<AtomicUsize>,
|
||||
points_to_push_update: Arc<AtomicUsize>,
|
||||
last_update: Arc<RwLock<Instant>>,
|
||||
amount_last_update: Arc<AtomicUsize>,
|
||||
//last_update: Arc<RwLock<Instant>>,
|
||||
last_update_time: Arc<AtomicInstant>,
|
||||
bytes_last_update: Arc<AtomicUsize>,
|
||||
rolling: RollingProgressWindow<250>,
|
||||
}
|
||||
|
||||
pub struct ProgressHandle {
|
||||
|
|
@ -42,67 +44,32 @@ impl ProgressHandle {
|
|||
pub fn add(&self, amount: usize) {
|
||||
self.progress
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
||||
self.progress_object.check_push_update(amount);
|
||||
calculate_update(&self.progress_object);
|
||||
}
|
||||
pub fn skip(&self, amount: usize) {
|
||||
self.progress
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
||||
// Offset the bytes at last offset by this amount
|
||||
self.progress_object
|
||||
.bytes_last_update
|
||||
.fetch_add(amount, Ordering::Relaxed);
|
||||
// Dont' fire update
|
||||
}
|
||||
}
|
||||
|
||||
static PROGRESS_UPDATES: usize = 100;
|
||||
|
||||
impl ProgressObject {
|
||||
pub fn new(max: usize, length: usize, sender: Sender<DownloadManagerSignal>) -> Self {
|
||||
let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
|
||||
// TODO: consolidate this calculation with the set_max function below
|
||||
let points_to_push_update = max / PROGRESS_UPDATES;
|
||||
Self {
|
||||
max: Arc::new(Mutex::new(max)),
|
||||
progress_instances: Arc::new(arr),
|
||||
start: Arc::new(Mutex::new(Instant::now())),
|
||||
sender,
|
||||
|
||||
points_towards_update: Arc::new(AtomicUsize::new(0)),
|
||||
points_to_push_update: Arc::new(AtomicUsize::new(points_to_push_update)),
|
||||
last_update: Arc::new(RwLock::new(Instant::now())),
|
||||
amount_last_update: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_push_update(&self, amount_added: usize) {
|
||||
let current_amount = self
|
||||
.points_towards_update
|
||||
.fetch_add(amount_added, Ordering::Relaxed);
|
||||
|
||||
let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed);
|
||||
|
||||
if current_amount >= to_update {
|
||||
self.points_towards_update
|
||||
.fetch_sub(to_update, Ordering::Relaxed);
|
||||
update_queue(&self);
|
||||
}
|
||||
|
||||
let last_update = self.last_update.read().unwrap();
|
||||
let last_update_difference = Instant::now().duration_since(*last_update).as_millis();
|
||||
if last_update_difference > 1000 {
|
||||
// push update
|
||||
drop(last_update);
|
||||
let mut last_update = self.last_update.write().unwrap();
|
||||
*last_update = Instant::now();
|
||||
drop(last_update);
|
||||
|
||||
let current_amount = self.sum();
|
||||
let max = self.get_max();
|
||||
let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed);
|
||||
self.amount_last_update
|
||||
.store(current_amount, Ordering::Relaxed);
|
||||
|
||||
let amount_since_last_update = current_amount - amount_at_last_update;
|
||||
|
||||
let kilobytes_per_second =
|
||||
amount_since_last_update / (last_update_difference as usize).max(1);
|
||||
|
||||
let remaining = max - current_amount; // bytes
|
||||
let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1);
|
||||
|
||||
update_ui(&self, kilobytes_per_second, time_remaining);
|
||||
last_update_time: Arc::new(AtomicInstant::now()),
|
||||
bytes_last_update: Arc::new(AtomicUsize::new(0)),
|
||||
rolling: RollingProgressWindow::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,9 +89,6 @@ impl ProgressObject {
|
|||
}
|
||||
pub fn set_max(&self, new_max: usize) {
|
||||
*self.max.lock().unwrap() = new_max;
|
||||
self.points_to_push_update
|
||||
.store(new_max / PROGRESS_UPDATES, Ordering::Relaxed);
|
||||
info!("points to push update: {}", new_max / PROGRESS_UPDATES);
|
||||
}
|
||||
pub fn set_size(&self, length: usize) {
|
||||
*self.progress_instances.lock().unwrap() =
|
||||
|
|
@ -136,9 +100,43 @@ impl ProgressObject {
|
|||
pub fn get(&self, index: usize) -> Arc<AtomicUsize> {
|
||||
self.progress_instances.lock().unwrap()[index].clone()
|
||||
}
|
||||
fn update_window(&self, kilobytes_per_second: usize) {
|
||||
self.rolling.update(kilobytes_per_second);
|
||||
}
|
||||
}
|
||||
|
||||
#[throttle(1, Duration::from_millis(20))]
|
||||
pub fn calculate_update(progress: &ProgressObject) {
|
||||
let last_update_time = progress
|
||||
.last_update_time
|
||||
.swap(Instant::now(), Ordering::SeqCst);
|
||||
let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis();
|
||||
|
||||
let current_bytes_downloaded = progress.sum();
|
||||
let max = progress.get_max();
|
||||
let bytes_at_last_update = progress
|
||||
.bytes_last_update
|
||||
.swap(current_bytes_downloaded, Ordering::Relaxed);
|
||||
|
||||
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
|
||||
|
||||
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
|
||||
|
||||
let bytes_remaining = max - current_bytes_downloaded; // bytes
|
||||
|
||||
progress.update_window(kilobytes_per_second);
|
||||
push_update(progress, bytes_remaining);
|
||||
}
|
||||
|
||||
#[throttle(1, Duration::from_millis(500))]
|
||||
pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
|
||||
let average_speed = progress.rolling.get_average();
|
||||
let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
|
||||
|
||||
update_ui(progress, average_speed, time_remaining);
|
||||
update_queue(progress);
|
||||
}
|
||||
|
||||
#[throttle(50, Duration::from_secs(1))]
|
||||
fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
|
||||
progress_object
|
||||
.sender
|
||||
|
|
@ -149,11 +147,9 @@ fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[throttle(50, Duration::from_secs(1))]
|
||||
fn update_queue(progress: &ProgressObject) {
|
||||
progress
|
||||
.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl Queue {
|
|||
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
|
||||
self.edit().pop_front()
|
||||
}
|
||||
pub fn empty(&self) -> bool {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.lock().unwrap().len() == 0
|
||||
}
|
||||
pub fn exists(&self, meta: DownloadableMetadata) -> bool {
|
||||
|
|
@ -52,10 +52,7 @@ impl Queue {
|
|||
}
|
||||
pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option<DownloadableMetadata> {
|
||||
let mut queue = self.edit();
|
||||
let front = match queue.front() {
|
||||
Some(front) => front,
|
||||
None => return None,
|
||||
};
|
||||
let front = queue.front()?;
|
||||
if front == meta {
|
||||
return queue.pop_front();
|
||||
}
|
||||
|
|
|
|||
33
src-tauri/src/download_manager/rolling_progress_updates.rs
Normal file
33
src-tauri/src/download_manager/rolling_progress_updates.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RollingProgressWindow<const S: usize> {
|
||||
window: Arc<[AtomicUsize; S]>,
|
||||
current: Arc<AtomicUsize>,
|
||||
}
|
||||
impl<const S: usize> RollingProgressWindow<S> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
window: Arc::new([(); S].map(|_| AtomicUsize::new(0))),
|
||||
current: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
pub fn update(&self, kilobytes_per_second: usize) {
|
||||
let index = self.current.fetch_add(1, Ordering::SeqCst);
|
||||
let current = &self.window[index % S];
|
||||
current.store(kilobytes_per_second, Ordering::SeqCst);
|
||||
}
|
||||
pub fn get_average(&self) -> usize {
|
||||
let current = self.current.load(Ordering::SeqCst);
|
||||
self.window
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| i < ¤t)
|
||||
.map(|(_, x)| x.load(Ordering::Relaxed))
|
||||
.sum::<usize>()
|
||||
/ S
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,12 @@ use std::{
|
|||
io,
|
||||
};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
|
||||
|
||||
// TODO: Rename / separate from downloads
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, SerializeDisplay)]
|
||||
pub enum ApplicationDownloadError {
|
||||
Communication(RemoteAccessError),
|
||||
Checksum,
|
||||
|
|
@ -20,11 +22,11 @@ impl Display for ApplicationDownloadError {
|
|||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
|
||||
ApplicationDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", 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::Setup(error) => write!(f, "an error occurred while setting up the download: {}", 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, "{}", error),
|
||||
ApplicationDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
|
||||
ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(SerializeDisplay)]
|
||||
pub enum LibraryError {
|
||||
MetaNotFound(String),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,3 @@ pub mod library_error;
|
|||
pub mod process_error;
|
||||
pub mod remote_access_error;
|
||||
pub mod setup_error;
|
||||
pub mod user_error;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
use std::{fmt::Display, io::Error};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(SerializeDisplay)]
|
||||
pub enum ProcessError {
|
||||
SetupRequired,
|
||||
NotInstalled,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ use std::{
|
|||
};
|
||||
|
||||
use http::StatusCode;
|
||||
use serde_with::SerializeDisplay;
|
||||
use url::ParseError;
|
||||
|
||||
use super::drop_server_error::DropServerError;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, SerializeDisplay)]
|
||||
pub enum RemoteAccessError {
|
||||
FetchError(Arc<reqwest::Error>),
|
||||
ParsingError(ParseError),
|
||||
|
|
@ -39,17 +40,17 @@ impl Display for RemoteAccessError {
|
|||
RemoteAccessError::ParsingError(parse_error) => {
|
||||
write!(f, "{}", parse_error)
|
||||
}
|
||||
RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
|
||||
RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message),
|
||||
RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),
|
||||
RemoteAccessError::InvalidResponse(error) => write!(f, "Server returned an invalid response: {} {}", error.status_code, error.status_message),
|
||||
RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"),
|
||||
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
|
||||
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message),
|
||||
RemoteAccessError::GameNotFound => write!(f, "could not find game on server"),
|
||||
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message),
|
||||
RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"),
|
||||
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
|
||||
f,
|
||||
"Failed to download game manifest: {} {}",
|
||||
"failed to download game manifest: {} {}",
|
||||
status, response
|
||||
),
|
||||
RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."),
|
||||
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
|
||||
RemoteAccessError::Generic(message) => write!(f, "{}", message),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub enum SetupError {
|
|||
impl Display for SetupError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SetupError::Context => write!(f, "Failed to generate contexts for download"),
|
||||
SetupError::Context => write!(f, "failed to generate contexts for download"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
use std::{
|
||||
fmt::Display,
|
||||
ops::{FromResidual, Try},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
pub enum UserValue<T, D>
|
||||
where
|
||||
T: Serialize,
|
||||
D: Display,
|
||||
{
|
||||
Ok(T),
|
||||
Err(D),
|
||||
}
|
||||
impl<T: Serialize, D: Display> Serialize for UserValue<T, D> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
UserValue::Ok(data) => data.serialize(serializer),
|
||||
UserValue::Err(err) => serializer.serialize_str(err.to_string().as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize, D: Display> From<Result<T, D>> for UserValue<T, D> {
|
||||
fn from(value: Result<T, D>) -> Self {
|
||||
match value {
|
||||
Ok(data) => UserValue::Ok(data),
|
||||
Err(data) => UserValue::Err(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize, D: Display> Try for UserValue<T, D> {
|
||||
type Output = T;
|
||||
|
||||
type Residual = D;
|
||||
|
||||
fn from_output(output: Self::Output) -> Self {
|
||||
Self::Ok(output)
|
||||
}
|
||||
|
||||
fn branch(self) -> std::ops::ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
UserValue::Ok(data) => std::ops::ControlFlow::Continue(data),
|
||||
UserValue::Err(e) => std::ops::ControlFlow::Break(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: Serialize, D: Display> FromResidual for UserValue<T, D> {
|
||||
fn from_residual(residual: <Self as std::ops::Try>::Residual) -> Self {
|
||||
UserValue::Err(residual)
|
||||
}
|
||||
}
|
||||
impl<T: Serialize, D: Display, U> FromResidual<Result<U, D>> for UserValue<T, D> {
|
||||
fn from_residual(residual: Result<U, D>) -> Self {
|
||||
match residual {
|
||||
Ok(_) => unreachable!(),
|
||||
Err(e) => UserValue::Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3,32 +3,28 @@ use std::sync::Mutex;
|
|||
use tauri::AppHandle;
|
||||
|
||||
use crate::{
|
||||
error::{
|
||||
library_error::LibraryError, remote_access_error::RemoteAccessError, user_error::UserValue,
|
||||
},
|
||||
games::library::{get_current_meta, uninstall_game_logic},
|
||||
AppState,
|
||||
database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{get_current_meta, uninstall_game_logic}, AppState
|
||||
};
|
||||
|
||||
use super::{
|
||||
library::{
|
||||
fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
|
||||
Game, GameVersionOption,
|
||||
Game,
|
||||
},
|
||||
state::{GameStatusManager, GameStatusWithTransient},
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_library(app: AppHandle) -> UserValue<Vec<Game>, RemoteAccessError> {
|
||||
fetch_library_logic(app).into()
|
||||
pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
fetch_library_logic(app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game(
|
||||
game_id: String,
|
||||
app: tauri::AppHandle,
|
||||
) -> UserValue<FetchGameStruct, RemoteAccessError> {
|
||||
fetch_game_logic(game_id, app).into()
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
fetch_game_logic(game_id, app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -37,21 +33,21 @@ pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> UserValue<(), LibraryError> {
|
||||
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
|
||||
let meta = match get_current_meta(&game_id) {
|
||||
Some(data) => data,
|
||||
None => return UserValue::Err(LibraryError::MetaNotFound(game_id)),
|
||||
None => return Err(LibraryError::MetaNotFound(game_id)),
|
||||
};
|
||||
println!("{:?}", meta);
|
||||
uninstall_game_logic(meta, &app_handle);
|
||||
|
||||
UserValue::Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_verion_options(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> UserValue<Vec<GameVersionOption>, RemoteAccessError> {
|
||||
fetch_game_verion_options_logic(game_id, state).into()
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
fetch_game_verion_options_logic(game_id, state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use std::sync::{mpsc::SendError, Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable},
|
||||
error::user_error::UserValue,
|
||||
download_manager::{
|
||||
download_manager::DownloadManagerSignal, downloadable::Downloadable,
|
||||
internal_error::InternalError,
|
||||
},
|
||||
AppState,
|
||||
};
|
||||
|
||||
|
|
@ -14,7 +16,7 @@ pub fn download_game(
|
|||
game_version: String,
|
||||
install_dir: usize,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> UserValue<(), SendError<DownloadManagerSignal>> {
|
||||
) -> Result<(), InternalError<DownloadManagerSignal>> {
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
||||
game_id,
|
||||
|
|
@ -22,10 +24,9 @@ pub fn download_game(
|
|||
install_dir,
|
||||
sender,
|
||||
)) as Box<dyn Downloadable + Send + Sync>);
|
||||
state
|
||||
Ok(state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)
|
||||
.into()
|
||||
.queue_download(game_download_agent)?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::auth::generate_authorization_header;
|
||||
use crate::database::db::{
|
||||
set_game_status, ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus,
|
||||
borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls,
|
||||
GameDownloadStatus,
|
||||
};
|
||||
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
||||
use crate::download_manager::download_thread_control_flag::{
|
||||
|
|
@ -13,11 +14,11 @@ use crate::error::application_download_error::ApplicationDownloadError;
|
|||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::DB;
|
||||
use log::{debug, error, info};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use slice_deque::SliceDeque;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
|
@ -55,7 +56,7 @@ impl GameDownloadAgent {
|
|||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let db_lock = borrow_db_checked();
|
||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
|
|
@ -82,10 +83,8 @@ impl GameDownloadAgent {
|
|||
// Blocking
|
||||
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
|
||||
self.ensure_manifest_exists()?;
|
||||
info!("Ensured manifest exists");
|
||||
|
||||
self.ensure_contexts()?;
|
||||
info!("Ensured contexts exists");
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
|
|
@ -94,11 +93,8 @@ impl GameDownloadAgent {
|
|||
|
||||
// Blocking
|
||||
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
info!("Setting up download");
|
||||
self.setup_download()?;
|
||||
info!("Setting progress object params");
|
||||
self.set_progress_object_params();
|
||||
info!("Running");
|
||||
let timer = Instant::now();
|
||||
push_game_update(
|
||||
app_handle,
|
||||
|
|
@ -114,7 +110,7 @@ impl GameDownloadAgent {
|
|||
.run()
|
||||
.map_err(|_| ApplicationDownloadError::DownloadError);
|
||||
|
||||
info!(
|
||||
debug!(
|
||||
"{} took {}ms to download",
|
||||
self.id,
|
||||
timer.elapsed().as_millis()
|
||||
|
|
@ -131,25 +127,17 @@ impl GameDownloadAgent {
|
|||
}
|
||||
|
||||
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let base_url = DB.fetch_base_url();
|
||||
let manifest_url = base_url
|
||||
.join(
|
||||
format!(
|
||||
"/api/v1/client/game/manifest?id={}&version={}",
|
||||
self.id,
|
||||
encode(&self.version)
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(manifest_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.unwrap();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/manifest"],
|
||||
&[("id", &self.id), ("version", &self.version)],
|
||||
|f| f.header("Authorization", header),
|
||||
)
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e))?
|
||||
.send()
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
|
|
@ -182,11 +170,8 @@ impl GameDownloadAgent {
|
|||
|
||||
let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
|
||||
|
||||
debug!("Setting ProgressObject max to {}", chunk_count);
|
||||
self.progress.set_max(chunk_count);
|
||||
debug!("Setting ProgressObject size to {}", length);
|
||||
self.progress.set_size(length);
|
||||
debug!("Setting ProgressObject time to now");
|
||||
self.progress.set_time_now();
|
||||
}
|
||||
|
||||
|
|
@ -250,9 +235,9 @@ impl GameDownloadAgent {
|
|||
|
||||
// TODO: Change return value on Err
|
||||
pub fn run(&self) -> Result<bool, ()> {
|
||||
let max_download_threads = DB.borrow_data().unwrap().settings.max_download_threads;
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
info!(
|
||||
debug!(
|
||||
"downloading game: {} with {} threads",
|
||||
self.id, max_download_threads
|
||||
);
|
||||
|
|
@ -264,8 +249,6 @@ impl GameDownloadAgent {
|
|||
let completed_indexes = Arc::new(boxcar::Vec::new());
|
||||
let completed_indexes_loop_arc = completed_indexes.clone();
|
||||
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
pool.scope(|scope| {
|
||||
let client = &reqwest::blocking::Client::new();
|
||||
|
|
@ -275,15 +258,32 @@ impl GameDownloadAgent {
|
|||
|
||||
let progress = self.progress.get(index);
|
||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||
|
||||
// If we've done this one already, skip it
|
||||
if self.completed_contexts.lock().unwrap().contains(&index) {
|
||||
progress_handle.add(context.length);
|
||||
progress_handle.skip(context.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
let request = generate_request(&base_url, client, context);
|
||||
let request = match make_request(
|
||||
&client,
|
||||
&["/api/v1/client/chunk"],
|
||||
&[
|
||||
("id", &context.game_id),
|
||||
("version", &context.version),
|
||||
("name", &context.file_name),
|
||||
("chunk", &context.index.to_string()),
|
||||
],
|
||||
|r| r.header("Authorization", generate_authorization_header()),
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap();
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match download_game_chunk(context, &self.control_flag, progress_handle, request)
|
||||
|
|
@ -298,7 +298,6 @@ impl GameDownloadAgent {
|
|||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
info!("Completed context id {}", index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -307,28 +306,27 @@ impl GameDownloadAgent {
|
|||
|
||||
let completed_lock_len = {
|
||||
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||
for (item, _) in newly_completed.iter() {
|
||||
completed_contexts_lock.push_front(item);
|
||||
for (_, item) in newly_completed.iter() {
|
||||
completed_contexts_lock.push_front(*item);
|
||||
}
|
||||
|
||||
completed_contexts_lock.len()
|
||||
};
|
||||
|
||||
info!("Got newly completed");
|
||||
|
||||
// If we're not out of contexts, we're not done, so we don't fire completed
|
||||
if completed_lock_len != contexts.len() {
|
||||
info!("da for {} exited without completing", self.id.clone());
|
||||
info!(
|
||||
"download agent for {} exited without completing ({}/{})",
|
||||
self.id.clone(),
|
||||
completed_lock_len,
|
||||
contexts.len(),
|
||||
);
|
||||
self.stored_manifest
|
||||
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
|
||||
info!("Setting completed contexts");
|
||||
self.stored_manifest.write();
|
||||
info!("Wrote completed contexts");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
info!("Sending completed signal");
|
||||
|
||||
// We've completed
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::Completed(self.metadata()))
|
||||
|
|
@ -338,27 +336,6 @@ impl GameDownloadAgent {
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_request(
|
||||
base_url: &url::Url,
|
||||
client: reqwest::blocking::Client,
|
||||
context: &DropDownloadContext,
|
||||
) -> reqwest::blocking::RequestBuilder {
|
||||
let chunk_url = base_url
|
||||
.join(&format!(
|
||||
"/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
|
||||
// Encode the parts we don't trust
|
||||
context.game_id,
|
||||
encode(&context.version),
|
||||
encode(&context.file_name),
|
||||
context.index
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
client.get(chunk_url).header("Authorization", header)
|
||||
}
|
||||
|
||||
impl Downloadable for GameDownloadAgent {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
||||
|
|
@ -407,6 +384,7 @@ impl Downloadable for GameDownloadAgent {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
|
||||
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
|
||||
let meta = self.metadata();
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ use crate::download_manager::progress_object::ProgressHandle;
|
|||
use crate::error::application_download_error::ApplicationDownloadError;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::downloads::manifest::DropDownloadContext;
|
||||
use log::{error, warn};
|
||||
use log::warn;
|
||||
use md5::{Context, Digest};
|
||||
use reqwest::blocking::{RequestBuilder, Response};
|
||||
|
||||
use std::fs::{set_permissions, Permissions};
|
||||
use std::io::Read;
|
||||
use std::io::{ErrorKind, Read};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::{
|
||||
|
|
@ -39,19 +39,17 @@ impl DropWriter<File> {
|
|||
// Write automatically pushes to file and hasher
|
||||
impl Write for DropWriter<File> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
/*
|
||||
self.hasher.write_all(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Unable to write to hasher: {}", e),
|
||||
)
|
||||
})?;
|
||||
*/
|
||||
self.destination.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
// self.hasher.flush()?;
|
||||
self.hasher.flush()?;
|
||||
self.destination.flush()
|
||||
}
|
||||
}
|
||||
|
|
@ -135,7 +133,6 @@ pub fn download_game_chunk(
|
|||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
warn!("{:?}", err);
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(err),
|
||||
));
|
||||
|
|
@ -151,7 +148,7 @@ pub fn download_game_chunk(
|
|||
|
||||
let content_length = response.content_length();
|
||||
if content_length.is_none() {
|
||||
warn!("Recieved 0 length content from server");
|
||||
warn!("recieved 0 length content from server");
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(response.json().unwrap()),
|
||||
));
|
||||
|
|
@ -179,16 +176,14 @@ pub fn download_game_chunk(
|
|||
set_permissions(ctx.path.clone(), permissions).unwrap();
|
||||
}
|
||||
|
||||
/*
|
||||
let checksum = pipeline
|
||||
.finish()
|
||||
.map_err(|e| GameDownloadError::IoError(e))?;
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
|
||||
let res = hex::encode(checksum.0);
|
||||
if res != ctx.checksum {
|
||||
return Err(GameDownloadError::Checksum);
|
||||
return Err(ApplicationDownloadError::Checksum);
|
||||
}
|
||||
*/
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,20 @@ use std::fs::remove_dir_all;
|
|||
use std::sync::Mutex;
|
||||
use std::thread::spawn;
|
||||
|
||||
use log::{error, info, warn};
|
||||
use log::{debug, error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Emitter;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use urlencoding::encode;
|
||||
|
||||
use crate::database::db::GameVersion;
|
||||
use crate::database::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
|
||||
use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
|
||||
use crate::download_manager::download_manager::DownloadStatus;
|
||||
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||
use crate::process::process_manager::Platform;
|
||||
use crate::remote::auth::generate_authorization_header;
|
||||
use crate::{AppState, DB};
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct FetchGameStruct {
|
||||
|
|
@ -52,6 +51,8 @@ pub struct QueueUpdateEventQueueData {
|
|||
pub meta: DownloadableMetadata,
|
||||
pub status: DownloadStatus,
|
||||
pub progress: f64,
|
||||
pub current: usize,
|
||||
pub max: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
|
|
@ -65,31 +66,14 @@ pub struct StatsUpdateEvent {
|
|||
pub time: usize,
|
||||
}
|
||||
|
||||
// Game version with some fields missing and size information
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameVersionOption {
|
||||
version_index: usize,
|
||||
version_name: String,
|
||||
platform: Platform,
|
||||
setup_command: String,
|
||||
launch_command: String,
|
||||
delta: bool,
|
||||
umu_id_override: Option<String>,
|
||||
// total_size: usize,
|
||||
}
|
||||
|
||||
pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
let base_url = DB.fetch_base_url();
|
||||
let library_url = base_url.join("/api/v1/client/user/library")?;
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(library_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})?
|
||||
.send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
|
|
@ -102,7 +86,7 @@ pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessErro
|
|||
let state = app.state::<Mutex<AppState>>();
|
||||
let mut handle = state.lock().unwrap();
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
|
||||
for game in games.iter() {
|
||||
handle.games.insert(game.id.clone(), game.clone());
|
||||
|
|
@ -137,17 +121,11 @@ pub fn fetch_game_logic(
|
|||
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let endpoint = base_url.join(&format!("/api/v1/game/{}", id))?;
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
let response = make_request(&client, &["/api/v1/game/", &id], &[], |r| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
})?
|
||||
.send()?;
|
||||
|
||||
if response.status() == 404 {
|
||||
return Err(RemoteAccessError::GameNotFound);
|
||||
|
|
@ -161,7 +139,7 @@ pub fn fetch_game_logic(
|
|||
let game: Game = response.json()?;
|
||||
state_handle.games.insert(id.clone(), game.clone());
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
|
||||
db_handle
|
||||
.applications
|
||||
|
|
@ -183,18 +161,16 @@ pub fn fetch_game_logic(
|
|||
pub fn fetch_game_verion_options_logic(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<GameVersionOption>, RemoteAccessError> {
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let endpoint =
|
||||
base_url.join(format!("/api/v1/client/game/versions?id={}", game_id).as_str())?;
|
||||
let header = generate_authorization_header();
|
||||
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/versions"],
|
||||
&[("id", &game_id)],
|
||||
|r| r.header("Authorization", generate_authorization_header()),
|
||||
)?
|
||||
.send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
|
|
@ -202,11 +178,11 @@ pub fn fetch_game_verion_options_logic(
|
|||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let data: Vec<GameVersionOption> = response.json()?;
|
||||
let data: Vec<GameVersion> = response.json()?;
|
||||
|
||||
let state_lock = state.lock().unwrap();
|
||||
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
let data: Vec<GameVersionOption> = data
|
||||
let data: Vec<GameVersion> = data
|
||||
.into_iter()
|
||||
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
|
||||
.collect();
|
||||
|
|
@ -217,8 +193,8 @@ pub fn fetch_game_verion_options_logic(
|
|||
}
|
||||
|
||||
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||
println!("Triggered uninstall for agent");
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
println!("triggered uninstall for agent");
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
|
|
@ -233,7 +209,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||
|
||||
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||
if previous_state.is_none() {
|
||||
info!("uninstall job doesn't have previous state, failing silently");
|
||||
warn!("uninstall job doesn't have previous state, failing silently");
|
||||
return;
|
||||
}
|
||||
let previous_state = previous_state.unwrap();
|
||||
|
|
@ -261,7 +237,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||
error!("{}", e);
|
||||
}
|
||||
Ok(_) => {
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
|
|
@ -269,9 +245,9 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
info!("uninstalled game id {}", &meta.id);
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
|
||||
push_game_update(
|
||||
&app_handle,
|
||||
|
|
@ -284,8 +260,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||
}
|
||||
|
||||
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
DB.borrow_data()
|
||||
.unwrap()
|
||||
borrow_db_checked()
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(game_id)
|
||||
|
|
@ -298,30 +273,27 @@ pub fn on_game_complete(
|
|||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
// Fetch game version information from remote
|
||||
let base_url = DB.fetch_base_url();
|
||||
if meta.version.is_none() {
|
||||
return Err(RemoteAccessError::GameNotFound);
|
||||
}
|
||||
|
||||
let endpoint = base_url.join(
|
||||
format!(
|
||||
"/api/v1/client/game/version?id={}&version={}",
|
||||
meta.id,
|
||||
encode(meta.version.as_ref().unwrap())
|
||||
)
|
||||
.as_str(),
|
||||
)?;
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/metadata/version"],
|
||||
&[
|
||||
("id", &meta.id),
|
||||
("version", meta.version.as_ref().unwrap()),
|
||||
],
|
||||
|f| f.header("Authorization", header),
|
||||
)?
|
||||
.send()?;
|
||||
|
||||
let data: GameVersion = response.json()?;
|
||||
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
|
|
@ -334,7 +306,7 @@ pub fn on_game_complete(
|
|||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
let status = if data.setup_command.is_empty() {
|
||||
GameDownloadStatus::Installed {
|
||||
|
|
@ -348,13 +320,13 @@ pub fn on_game_complete(
|
|||
}
|
||||
};
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
use crate::{
|
||||
database::db::{ApplicationTransientStatus, GameDownloadStatus},
|
||||
DB,
|
||||
};
|
||||
use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus};
|
||||
|
||||
pub type GameStatusWithTransient = (
|
||||
Option<GameDownloadStatus>,
|
||||
|
|
@ -11,7 +8,7 @@ pub struct GameStatusManager {}
|
|||
|
||||
impl GameStatusManager {
|
||||
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let db_lock = borrow_db_checked();
|
||||
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
|
||||
None => None,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
#![feature(try_trait_v2)]
|
||||
|
||||
mod database;
|
||||
mod games;
|
||||
|
||||
|
|
@ -16,10 +14,12 @@ use autostart::{get_autostart_enabled, toggle_autostart};
|
|||
use cleanup::{cleanup_and_exit, quit};
|
||||
use commands::fetch_state;
|
||||
use database::commands::{
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_system_data,
|
||||
update_settings,
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
|
||||
fetch_system_data, update_settings,
|
||||
};
|
||||
use database::db::{
|
||||
borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR,
|
||||
};
|
||||
use database::db::{DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR};
|
||||
use download_manager::commands::{
|
||||
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
||||
};
|
||||
|
|
@ -44,8 +44,11 @@ use remote::auth::{self, generate_authorization_header, recieve_handshake};
|
|||
use remote::commands::{
|
||||
auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
|
||||
};
|
||||
use remote::requests::make_request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
|
@ -92,15 +95,21 @@ pub struct AppState<'a> {
|
|||
|
||||
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
let logfile = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
|
||||
.encoder(Box::new(PatternEncoder::new(
|
||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||
)))
|
||||
.append(false)
|
||||
.build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log"))
|
||||
.unwrap();
|
||||
|
||||
let console = ConsoleAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
|
||||
.encoder(Box::new(PatternEncoder::new(
|
||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||
)))
|
||||
.build();
|
||||
|
||||
let log_level = env::var("RUST_LOG").unwrap_or(String::from("Info"));
|
||||
|
||||
let config = Config::builder()
|
||||
.appenders(vec![
|
||||
Appender::builder().build("logfile", Box::new(logfile)),
|
||||
|
|
@ -109,7 +118,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
.build(
|
||||
Root::builder()
|
||||
.appenders(vec!["logfile", "console"])
|
||||
.build(LevelFilter::Info),
|
||||
.build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -119,7 +128,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
||||
|
||||
debug!("Checking if database is set up");
|
||||
debug!("checking if database is set up");
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
if !is_set_up {
|
||||
return AppState {
|
||||
|
|
@ -131,12 +140,12 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
};
|
||||
}
|
||||
|
||||
debug!("Database is set up");
|
||||
debug!("database is set up");
|
||||
|
||||
// TODO: Account for possible failure
|
||||
let (app_status, user) = auth::setup().unwrap();
|
||||
let (app_status, user) = auth::setup();
|
||||
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let db_handle = borrow_db_checked();
|
||||
let mut missing_games = Vec::new();
|
||||
let statuses = db_handle.applications.game_statuses.clone();
|
||||
drop(db_handle);
|
||||
|
|
@ -166,7 +175,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
|
||||
info!("detected games missing: {:?}", missing_games);
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
for game_id in missing_games {
|
||||
db_handle
|
||||
.applications
|
||||
|
|
@ -177,11 +186,11 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
|
||||
drop(db_handle);
|
||||
|
||||
info!("finished setup!");
|
||||
debug!("finished setup!");
|
||||
|
||||
// Sync autostart state
|
||||
if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
|
||||
warn!("Failed to sync autostart state: {}", e);
|
||||
warn!("failed to sync autostart state: {}", e);
|
||||
}
|
||||
|
||||
AppState {
|
||||
|
|
@ -218,6 +227,7 @@ pub fn run() {
|
|||
fetch_system_data,
|
||||
// User utils
|
||||
update_settings,
|
||||
fetch_settings,
|
||||
// Auth
|
||||
auth_initiate,
|
||||
retry_connect,
|
||||
|
|
@ -256,14 +266,14 @@ pub fn run() {
|
|||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
let state = setup(handle);
|
||||
info!("initialized drop client");
|
||||
debug!("initialized drop client");
|
||||
app.manage(Mutex::new(state));
|
||||
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
app.deep_link().register_all()?;
|
||||
info!("registered all pre-defined deep links");
|
||||
debug!("registered all pre-defined deep links");
|
||||
}
|
||||
|
||||
let handle = app.handle().clone();
|
||||
|
|
@ -283,7 +293,7 @@ pub fn run() {
|
|||
.unwrap();
|
||||
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
info!("handling drop:// url");
|
||||
debug!("handling drop:// url");
|
||||
let binding = event.urls();
|
||||
let url = binding.first().unwrap();
|
||||
if url.host_str().unwrap() == "handshake" {
|
||||
|
|
@ -313,18 +323,18 @@ pub fn run() {
|
|||
app.webview_windows().get("main").unwrap().show().unwrap();
|
||||
}
|
||||
"quit" => {
|
||||
cleanup_and_exit(app);
|
||||
cleanup_and_exit(app, &app.state());
|
||||
}
|
||||
|
||||
_ => {
|
||||
println!("Menu event not handled: {:?}", event.id);
|
||||
println!("menu event not handled: {:?}", event.id);
|
||||
}
|
||||
})
|
||||
.build(app)
|
||||
.expect("error while setting up tray menu");
|
||||
|
||||
{
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
if let Some(original) = db_handle.prev_database.take() {
|
||||
warn!(
|
||||
"Database corrupted. Original file at {}",
|
||||
|
|
@ -347,23 +357,16 @@ pub fn run() {
|
|||
Ok(())
|
||||
})
|
||||
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
// Drop leading /
|
||||
let object_id = &request.uri().path()[1..];
|
||||
|
||||
let object_url = base_url
|
||||
.join("/api/v1/client/object/")
|
||||
.unwrap()
|
||||
.join(object_id)
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(object_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send();
|
||||
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})
|
||||
.unwrap()
|
||||
.send();
|
||||
if response.is_err() {
|
||||
warn!(
|
||||
"failed to fetch object with error: {}",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
error::{process_error::ProcessError, user_error::UserValue},
|
||||
AppState, DB,
|
||||
};
|
||||
use crate::{error::process_error::ProcessError, AppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn launch_game(
|
||||
id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> UserValue<(), ProcessError> {
|
||||
) -> Result<(), ProcessError> {
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
|
||||
|
|
@ -21,24 +18,23 @@ pub fn launch_game(
|
|||
|
||||
match process_manager_lock.launch_process(id) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return UserValue::Err(e),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
|
||||
UserValue::Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn kill_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> UserValue<(), ProcessError> {
|
||||
) -> Result<(), ProcessError> {
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
process_manager_lock
|
||||
.kill_game(game_id)
|
||||
.map_err(ProcessError::IOError)
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ use tauri::{AppHandle, Manager};
|
|||
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
|
||||
|
||||
use crate::{
|
||||
database::db::{ApplicationTransientStatus, GameDownloadStatus, DATA_ROOT_DIR},
|
||||
database::db::{
|
||||
borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, DATA_ROOT_DIR
|
||||
},
|
||||
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
|
||||
error::process_error::ProcessError,
|
||||
games::{library::push_game_update, state::GameStatusManager},
|
||||
|
|
@ -64,12 +66,8 @@ impl ProcessManager<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
// There's no easy way to distinguish between an executable name with
|
||||
// spaces and it's arguments.
|
||||
// I think if we just join the install_dir to whatever the user provides us, we'll be alright
|
||||
// In future, we should have a separate field for executable name and it's arguments
|
||||
fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec<String>) {
|
||||
let root = raw_command;
|
||||
fn process_command(&self, install_dir: &String, command: Vec<String>) -> (PathBuf, Vec<String>) {
|
||||
let root = &command[0];
|
||||
|
||||
let install_dir = Path::new(install_dir);
|
||||
let absolute_exe = install_dir.join(root);
|
||||
|
|
@ -83,7 +81,7 @@ impl ProcessManager<'_> {
|
|||
(absolute_exe, Vec::new())
|
||||
}
|
||||
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
|
||||
return match self.processes.get(&game_id) {
|
||||
match self.processes.get(&game_id) {
|
||||
Some(child) => {
|
||||
child.kill()?;
|
||||
child.wait()?;
|
||||
|
|
@ -93,7 +91,7 @@ impl ProcessManager<'_> {
|
|||
io::ErrorKind::NotFound,
|
||||
"Game ID not running",
|
||||
)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
|
||||
|
|
@ -106,7 +104,7 @@ impl ProcessManager<'_> {
|
|||
|
||||
self.processes.remove(&game_id);
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let meta = db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
|
|
@ -166,9 +164,9 @@ impl ProcessManager<'_> {
|
|||
{
|
||||
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
|
||||
Some(GameDownloadStatus::SetupRequired { .. }) => {
|
||||
return Err(ProcessError::SetupRequired).into()
|
||||
return Err(ProcessError::SetupRequired)
|
||||
}
|
||||
_ => return Err(ProcessError::NotInstalled).into(),
|
||||
_ => return Err(ProcessError::NotInstalled),
|
||||
};
|
||||
let meta = DownloadableMetadata {
|
||||
id: game_id.clone(),
|
||||
|
|
@ -176,7 +174,7 @@ impl ProcessManager<'_> {
|
|||
download_type: DownloadType::Game,
|
||||
};
|
||||
|
||||
let mut db_lock = DB.borrow_data_mut().unwrap();
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
debug!(
|
||||
"Launching process {:?} with games {:?}",
|
||||
&game_id, db_lock.applications.game_versions
|
||||
|
|
@ -188,23 +186,18 @@ impl ProcessManager<'_> {
|
|||
.get(&game_id)
|
||||
.ok_or(ProcessError::NotInstalled)?;
|
||||
|
||||
let status_metadata: Option<(&String, &String)> = match game_status {
|
||||
let (version_name, install_dir) = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((&version_name, &install_dir)),
|
||||
} => (version_name, install_dir),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
} => (version_name, install_dir),
|
||||
_ => return Err(ProcessError::NotDownloaded),
|
||||
};
|
||||
|
||||
if status_metadata.is_none() {
|
||||
return Err(ProcessError::NotDownloaded);
|
||||
}
|
||||
|
||||
let (version_name, install_dir) = status_metadata.unwrap();
|
||||
|
||||
let game_version = db_lock
|
||||
.applications
|
||||
|
|
@ -214,19 +207,28 @@ impl ProcessManager<'_> {
|
|||
.get(version_name)
|
||||
.ok_or(ProcessError::InvalidVersion)?;
|
||||
|
||||
let raw_command: String = match game_status {
|
||||
let mut command: Vec<String> = Vec::new();
|
||||
|
||||
match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => game_version.launch_command.clone(),
|
||||
} => {
|
||||
command.extend([game_version.launch_command.clone()]);
|
||||
command.extend(game_version.launch_args.clone());
|
||||
},
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => game_version.setup_command.clone(),
|
||||
} => {
|
||||
command.extend([game_version.setup_command.clone()]);
|
||||
command.extend(game_version.setup_args.clone());
|
||||
},
|
||||
_ => panic!("unreachable code"),
|
||||
};
|
||||
info!("Command: {:?}", &command);
|
||||
|
||||
let (command, args) = self.process_command(install_dir, raw_command);
|
||||
let (command, args) = self.process_command(install_dir, command);
|
||||
|
||||
let target_current_dir = command.parent().unwrap().to_str().unwrap();
|
||||
|
||||
|
|
@ -274,8 +276,8 @@ impl ProcessManager<'_> {
|
|||
let launch_process = game_launcher
|
||||
.launch_process(
|
||||
&meta,
|
||||
command.to_str().unwrap().to_owned(),
|
||||
args,
|
||||
command.to_string_lossy().to_string(),
|
||||
game_version,
|
||||
target_current_dir,
|
||||
log_file,
|
||||
error_file,
|
||||
|
|
@ -316,9 +318,6 @@ impl ProcessManager<'_> {
|
|||
});
|
||||
|
||||
self.processes.insert(meta.id, wait_thread_handle);
|
||||
|
||||
info!("finished spawning process");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -333,8 +332,8 @@ pub trait ProcessHandler: Send + 'static {
|
|||
fn launch_process(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
|
|
@ -346,17 +345,17 @@ impl ProcessHandler for NativeGameLauncher {
|
|||
fn launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, Error> {
|
||||
Command::new(command)
|
||||
Command::new(PathBuf::from(launch_command))
|
||||
.current_dir(current_dir)
|
||||
.stdout(log_file)
|
||||
.stderr(error_file)
|
||||
.args(args)
|
||||
.args(game_version.launch_args.clone())
|
||||
.spawn()
|
||||
}
|
||||
}
|
||||
|
|
@ -367,15 +366,21 @@ impl ProcessHandler for UMULauncher {
|
|||
fn launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
_log_file: File,
|
||||
_error_file: File,
|
||||
) -> Result<Child, Error> {
|
||||
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command)
|
||||
.game_id(String::from("0"))
|
||||
.launch_args(args)
|
||||
println!("Game override: .{:?}.", &game_version.umu_id_override);
|
||||
let game_id = match &game_version.umu_id_override {
|
||||
Some(game_override) => game_override.is_empty().then_some(game_version.game_id.clone()).unwrap_or(game_override.clone()) ,
|
||||
None => game_version.game_id.clone()
|
||||
};
|
||||
info!("Game ID: {}", game_id);
|
||||
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, launch_command)
|
||||
.game_id(game_id)
|
||||
.launch_args(game_version.launch_args.clone())
|
||||
.build()
|
||||
.spawn()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
use std::{env, sync::Mutex};
|
||||
|
||||
use chrono::Utc;
|
||||
use log::{debug, error, info, warn};
|
||||
use log::{debug, error, warn};
|
||||
use openssl::{ec::EcKey, hash::MessageDigest, pkey::PKey, sign::Signer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::{DatabaseAuth, DatabaseImpls},
|
||||
database::db::{
|
||||
borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls,
|
||||
},
|
||||
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
||||
AppState, AppStatus, User, DB,
|
||||
};
|
||||
|
||||
use super::requests::make_request;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InitiateRequestBody {
|
||||
|
|
@ -51,7 +55,7 @@ pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
|
|||
|
||||
pub fn generate_authorization_header() -> String {
|
||||
let certs = {
|
||||
let db = DB.borrow_data().unwrap();
|
||||
let db = borrow_db_checked();
|
||||
db.auth.clone().unwrap()
|
||||
};
|
||||
|
||||
|
|
@ -65,16 +69,15 @@ pub fn generate_authorization_header() -> String {
|
|||
pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let endpoint = base_url.join("/api/v1/client/user")?;
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})?
|
||||
.send()?;
|
||||
if response.status() != 200 {
|
||||
let err: DropServerError = response.json().unwrap();
|
||||
let err: DropServerError = response.json()?;
|
||||
warn!("{:?}", err);
|
||||
|
||||
if err.status_message == "Nonce expired" {
|
||||
|
|
@ -97,7 +100,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||
}
|
||||
|
||||
let base_url = {
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
let handle = borrow_db_checked();
|
||||
Url::parse(handle.base_url.as_str())?
|
||||
};
|
||||
|
||||
|
|
@ -111,18 +114,18 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.post(endpoint).json(&body).send()?;
|
||||
debug!("Handshake responsded with {}", response.status().as_u16());
|
||||
debug!("handshake responsded with {}", response.status().as_u16());
|
||||
let response_struct: HandshakeResponse = response.json()?;
|
||||
|
||||
{
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = Some(DatabaseAuth {
|
||||
private: response_struct.private,
|
||||
cert: response_struct.certificate,
|
||||
client_id: response_struct.id,
|
||||
});
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -151,7 +154,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
|
|||
|
||||
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
||||
let base_url = {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let db_lock = borrow_db_checked();
|
||||
Url::parse(&db_lock.base_url.clone())?
|
||||
};
|
||||
|
||||
|
|
@ -166,7 +169,7 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
|||
|
||||
if response.status() != 200 {
|
||||
let data: DropServerError = response.json()?;
|
||||
error!("Could not start handshake: {}", data.status_message);
|
||||
error!("could not start handshake: {}", data.status_message);
|
||||
|
||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||
}
|
||||
|
|
@ -180,15 +183,19 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup() -> Result<(AppStatus, Option<User>), RemoteAccessError> {
|
||||
let data = DB.borrow_data().unwrap();
|
||||
pub fn setup() -> (AppStatus, Option<User>) {
|
||||
let data = borrow_db_checked();
|
||||
let auth = data.auth.clone();
|
||||
drop(data);
|
||||
|
||||
if auth.is_some() {
|
||||
let user_result = fetch_user()?;
|
||||
return Ok((AppStatus::SignedIn, Some(user_result)));
|
||||
let user_result = match fetch_user() {
|
||||
Ok(data) => data,
|
||||
Err(RemoteAccessError::FetchError(_)) => return (AppStatus::ServerUnavailable, None),
|
||||
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
||||
};
|
||||
return (AppStatus::SignedIn, Some(user_result));
|
||||
}
|
||||
|
||||
Ok((AppStatus::SignedOut, None))
|
||||
(AppStatus::SignedOut, None)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ use tauri::{AppHandle, Emitter, Manager};
|
|||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
error::{remote_access_error::RemoteAccessError, user_error::UserValue},
|
||||
AppState, AppStatus, DB,
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
AppState, AppStatus,
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
|
@ -17,31 +18,31 @@ use super::{
|
|||
pub fn use_remote(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> UserValue<(), RemoteAccessError> {
|
||||
UserValue::Ok(use_remote_logic(url, state)?)
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
use_remote_logic(url, state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn gen_drop_url(path: String) -> UserValue<String, RemoteAccessError> {
|
||||
pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||
let base_url = {
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
let handle = borrow_db_checked();
|
||||
|
||||
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
||||
};
|
||||
|
||||
let url = base_url.join(&path).unwrap();
|
||||
|
||||
UserValue::Ok(url.to_string())
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sign_out(app: AppHandle) {
|
||||
// Clear auth from database
|
||||
{
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = None;
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
}
|
||||
|
||||
// Update app state
|
||||
|
|
@ -57,19 +58,18 @@ pub fn sign_out(app: AppHandle) {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> UserValue<(), RemoteAccessError> {
|
||||
let (app_status, user) = setup()?;
|
||||
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
let (app_status, user) = setup();
|
||||
|
||||
let mut guard = state.lock().unwrap();
|
||||
guard.status = app_status;
|
||||
guard.user = user;
|
||||
drop(guard);
|
||||
UserValue::Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn auth_initiate() -> UserValue<(), RemoteAccessError> {
|
||||
auth_initiate_logic().into()
|
||||
pub fn auth_initiate() -> Result<(), RemoteAccessError> {
|
||||
auth_initiate_logic()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod auth;
|
||||
pub mod commands;
|
||||
pub mod remote;
|
||||
pub mod requests;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Formatter},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use http::StatusCode;
|
||||
use log::{info, warn};
|
||||
use log::{debug, warn};
|
||||
use serde::Deserialize;
|
||||
use url::{ParseError, Url};
|
||||
use url::Url;
|
||||
|
||||
use crate::{error::remote_access_error::RemoteAccessError, AppState, AppStatus, DB};
|
||||
use crate::{
|
||||
database::db::{borrow_db_mut_checked, save_db},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
AppState, AppStatus,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -21,7 +20,7 @@ pub fn use_remote_logic(
|
|||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
info!("connecting to url {}", url);
|
||||
debug!("connecting to url {}", url);
|
||||
let base_url = Url::parse(&url)?;
|
||||
|
||||
// Test Drop url
|
||||
|
|
@ -39,11 +38,11 @@ pub fn use_remote_logic(
|
|||
app_state.status = AppStatus::SignedOut;
|
||||
drop(app_state);
|
||||
|
||||
let mut db_state = DB.borrow_data_mut().unwrap();
|
||||
let mut db_state = borrow_db_mut_checked();
|
||||
db_state.base_url = base_url.to_string();
|
||||
drop(db_state);
|
||||
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
23
src-tauri/src/remote/requests.rs
Normal file
23
src-tauri/src/remote/requests.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use reqwest::blocking::{Client, RequestBuilder};
|
||||
|
||||
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
|
||||
|
||||
pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
|
||||
client: &Client,
|
||||
path_components: &[T],
|
||||
query: &[(T, T)],
|
||||
f: F,
|
||||
) -> Result<RequestBuilder, RemoteAccessError> {
|
||||
let mut base_url = DB.fetch_base_url();
|
||||
for endpoint in path_components {
|
||||
base_url = base_url.join(endpoint.as_ref())?;
|
||||
}
|
||||
{
|
||||
let mut queries = base_url.query_pairs_mut();
|
||||
for (param, val) in query {
|
||||
queries.append_pair(param.as_ref(), val.as_ref());
|
||||
}
|
||||
}
|
||||
let response = client.get(base_url);
|
||||
Ok(f(response))
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||
"productName": "Drop Desktop Client",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0-beta",
|
||||
"identifier": "dev.drop.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn dev --port 1432",
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis", "deb", "rpm", "dmg"],
|
||||
"targets": ["nsis", "deb", "rpm", "dmg", "appimage"],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "both"
|
||||
|
|
|
|||
5
types.ts
5
types.ts
|
|
@ -71,4 +71,9 @@ export type DownloadableMetadata = {
|
|||
id: string,
|
||||
version: string,
|
||||
downloadType: DownloadableType
|
||||
}
|
||||
|
||||
export type Settings = {
|
||||
autostart: boolean,
|
||||
maxDownloadThreads: number,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue