Compare commits
59 commits
main
...
generic-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a154d1ea11 | ||
|
|
cba7d8e523 | ||
|
|
02f8591a60 | ||
|
|
d58348434c | ||
|
|
4a5f416d95 | ||
|
|
a2bcf5325f | ||
|
|
6b029041ed | ||
|
|
5c12a29f66 | ||
|
|
df922f8580 | ||
|
|
d17ad6fcc9 | ||
|
|
9e7208edae | ||
|
|
bed2df49f8 | ||
|
|
a4a5839a3a | ||
|
|
352926af6c | ||
|
|
ca0fa8ffcb | ||
|
|
0a0d9d6294 | ||
|
|
adf9907dbf | ||
|
|
267a2beddf | ||
|
|
e7b1465478 | ||
|
|
8d46a94f90 | ||
|
|
8db2393346 | ||
|
|
61b3271559 | ||
|
|
5b5d6092fe | ||
|
|
972ebd6ab6 | ||
|
|
a17311a88d | ||
|
|
fb20c56d45 | ||
|
|
d9136b6ca8 | ||
|
|
472eb1d435 | ||
|
|
e91f79bf57 | ||
|
|
22f6d3083f | ||
|
|
6b96e408b2 | ||
|
|
c3f62222fe | ||
|
|
01e6162527 | ||
|
|
88b2505e71 | ||
|
|
95f2174f8d | ||
|
|
7c90d2b8fd | ||
|
|
d7b0302bdd | ||
|
|
3ccd44466f | ||
|
|
93b8b83c20 | ||
|
|
1861659daa | ||
|
|
327628b780 | ||
|
|
f4ac1c87cd | ||
|
|
03fa3646fa | ||
|
|
a881d8e248 | ||
|
|
dcb2c0f004 | ||
|
|
c722a54132 | ||
|
|
e72662c4a8 | ||
|
|
139bc0ca36 | ||
|
|
949acfc161 | ||
|
|
9af0d08875 | ||
|
|
dcb1564568 | ||
|
|
1f899ec349 | ||
|
|
6a8d0af87d | ||
|
|
21835858f1 | ||
|
|
a135b1321c | ||
|
|
ad92dbec08 | ||
|
|
85a08990c3 | ||
|
|
dd7f5675d8 | ||
|
|
9ea2aa4997 |
84 changed files with 1502 additions and 2310 deletions
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
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
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
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}/rustlang/rust:nightly
|
||||
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rust:1.81.0-bookworm
|
||||
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,10 +13,9 @@ 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,AppImage}"
|
||||
- "*.{deb,rpm}"
|
||||
|
||||
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,18 +20,10 @@ import {
|
|||
const router = useRouter();
|
||||
|
||||
const state = useAppState();
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
}
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
|
||||
router.beforeEach(async () => {
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
}
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
});
|
||||
|
||||
setupHooks();
|
||||
|
|
|
|||
287
changelog.md
287
changelog.md
|
|
@ -1,292 +1,5 @@
|
|||
|
||||
|
||||
## 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,13 +2,7 @@ import { listen } from "@tauri-apps/api/event";
|
|||
import type { DownloadableMetadata } from "~/types";
|
||||
|
||||
export type QueueState = {
|
||||
queue: Array<{
|
||||
meta: DownloadableMetadata;
|
||||
status: string;
|
||||
progress: number | null;
|
||||
current: number;
|
||||
max: number;
|
||||
}>;
|
||||
queue: Array<{ meta: DownloadableMetadata; status: string; progress: number | null }>;
|
||||
status: string;
|
||||
};
|
||||
|
||||
|
|
@ -31,4 +25,4 @@ listen("update_queue", (event) => {
|
|||
listen("update_stats", (event) => {
|
||||
const stats = useStatsState();
|
||||
stats.value = event.payload as StatsState;
|
||||
});
|
||||
});
|
||||
|
|
@ -12,7 +12,7 @@ export type SerializedGameStatus = [
|
|||
OptionGameStatus | null
|
||||
];
|
||||
|
||||
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||
const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||
console.log(status);
|
||||
if (status[0]) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ export function initialNavigation(state: Ref<AppState>) {
|
|||
|
||||
switch (state.value.status) {
|
||||
case AppStatus.NotConfigured:
|
||||
router.push({ path: "/setup" });
|
||||
router.push({ path: "/setup" }).then(() => {
|
||||
console.log("Pushed 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="relative grow overflow-y-auto">
|
||||
<div class="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="relative grow overflow-y-auto">
|
||||
<div class="grow overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "drop-app",
|
||||
"private": true,
|
||||
"version": "0.2.0-beta",
|
||||
"version": "0.1.1",
|
||||
"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,7 +1,4 @@
|
|||
<template />
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: false
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
<template>
|
||||
<div class="flex flex-row h-full">
|
||||
<div
|
||||
class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1"
|
||||
>
|
||||
<div class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1">
|
||||
<ul class="flex flex-col gap-y-1">
|
||||
<NuxtLink
|
||||
v-for="(nav, navIdx) in navigation"
|
||||
:key="nav.route"
|
||||
:class="[
|
||||
'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
|
||||
navIdx === currentNavigationIndex
|
||||
? 'bg-zinc-800 text-zinc-100'
|
||||
: nav.isInstalled.value
|
||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||
navIdx === currentNavigationIndex
|
||||
? 'bg-zinc-800 text-zinc-100'
|
||||
: 'bg-zinc-900/50 text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||
]"
|
||||
:href="nav.route"
|
||||
>
|
||||
<div class="flex items-center w-full gap-x-3">
|
||||
<img
|
||||
class="size-6 flex-none object-cover bg-zinc-900 rounded"
|
||||
class="h-8 w-10 flex-none object-cover bg-zinc-900"
|
||||
:src="icons[navIdx]"
|
||||
alt=""
|
||||
/>
|
||||
|
|
@ -31,45 +27,23 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="grow overflow-y-auto">
|
||||
<NuxtPage :libraryDownloadError = "libraryDownloadError" />
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
|
||||
import type { Game, NavigationItem } from "~/types";
|
||||
|
||||
let libraryDownloadError = false;
|
||||
const games: Array<Game> = await invoke("fetch_library");
|
||||
const icons = await Promise.all(games.map((e) => useObject(e.mIconId)));
|
||||
|
||||
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))
|
||||
);
|
||||
|
||||
const navigation = games.map(({ game, status }) => {
|
||||
const isInstalled = computed(
|
||||
() =>
|
||||
status.value.type == GameStatusEnum.Installed ||
|
||||
status.value.type == GameStatusEnum.SetupRequired
|
||||
);
|
||||
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
isInstalled,
|
||||
const navigation = games.map((e) => {
|
||||
const item: NavigationItem = {
|
||||
label: e.mName,
|
||||
route: `/library/${e.id}`,
|
||||
prefix: `/library/${e.id}`,
|
||||
};
|
||||
return item;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@
|
|||
"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
class="w-full sm:w-fit"
|
||||
>
|
||||
Install
|
||||
</LoadingButton>
|
||||
|
|
@ -306,7 +306,6 @@ 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();
|
||||
|
|
@ -320,19 +319,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,9 +1,3 @@
|
|||
<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,37 +1,23 @@
|
|||
<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) }}/s</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) }}</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="">
|
||||
|
|
@ -49,68 +35,47 @@
|
|||
<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 { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import { 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 };
|
||||
|
|
@ -132,7 +97,8 @@ 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;
|
||||
|
|
@ -149,12 +115,10 @@ 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);
|
||||
|
|
@ -167,7 +131,7 @@ function loadGamesForQueue(v: typeof queue.value) {
|
|||
loadGamesForQueue(queue.value);
|
||||
|
||||
async function onEnd(event: { oldIndex: number; newIndex: number }) {
|
||||
await invoke("move_download_in_queue", {
|
||||
await invoke("move_game_in_queue", {
|
||||
oldIndex: event.oldIndex,
|
||||
newIndex: event.newIndex,
|
||||
});
|
||||
|
|
@ -188,7 +152,7 @@ function formatKilobytes(bytes: number): string {
|
|||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}/s`;
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
|
|
@ -198,7 +162,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);
|
||||
|
|
|
|||
|
|
@ -44,45 +44,8 @@ import {
|
|||
import type { Component } from "vue";
|
||||
import type { NavigationItem } from "~/types";
|
||||
import { platform } from '@tauri-apps/plugin-os';
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const systemData = await invoke<{
|
||||
clientId: string;
|
||||
baseUrl: string;
|
||||
dataDir: string;
|
||||
logLevel: string;
|
||||
}>("fetch_system_data");
|
||||
|
||||
const isDebugMode = ref(systemData.logLevel.toLowerCase() === "debug");
|
||||
const debugRevealed = ref(false);
|
||||
|
||||
// Track shift key state and debug reveal
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Shift') {
|
||||
isDebugMode.value = true;
|
||||
debugRevealed.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Shift') {
|
||||
isDebugMode.value = debugRevealed.value || systemData.logLevel.toLowerCase() === "debug";
|
||||
}
|
||||
});
|
||||
|
||||
// Reset debug reveal when leaving the settings page
|
||||
const router = useRouter();
|
||||
router.beforeEach((to) => {
|
||||
if (!to.path.startsWith('/settings')) {
|
||||
debugRevealed.value = false;
|
||||
isDebugMode.value = systemData.logLevel.toLowerCase() === "debug";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make navigation reactive by wrapping in computed
|
||||
const navigation = computed(() => [
|
||||
const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
{
|
||||
label: "Home",
|
||||
route: "/settings",
|
||||
|
|
@ -90,7 +53,7 @@ const navigation = computed(() => [
|
|||
icon: HomeIcon,
|
||||
},
|
||||
{
|
||||
label: "Interface",
|
||||
label: "Interface",
|
||||
route: "/settings/interface",
|
||||
prefix: "/settings/interface",
|
||||
icon: RectangleGroupIcon,
|
||||
|
|
@ -101,21 +64,20 @@ const navigation = computed(() => [
|
|||
prefix: "/settings/downloads",
|
||||
icon: ArrowDownTrayIcon,
|
||||
},
|
||||
...(isDebugMode.value ? [{
|
||||
{
|
||||
label: "Debug Info",
|
||||
route: "/settings/debug",
|
||||
route: "/settings/debug",
|
||||
prefix: "/settings/debug",
|
||||
icon: BugAntIcon,
|
||||
}] : []),
|
||||
]);
|
||||
},
|
||||
];
|
||||
|
||||
const currentPlatform = platform();
|
||||
switch (currentPlatform) {
|
||||
case "linux":
|
||||
navigation.splice(2, 0, { label: "Compatibility", route: "/settings/compatibility", prefix: "/settings/compatibility", icon: CubeIcon });
|
||||
break;
|
||||
}
|
||||
|
||||
// Use .value to unwrap the computed ref
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation.value);
|
||||
|
||||
// Watch for navigation changes and update currentPageIndex
|
||||
watch(navigation, (newNav) => {
|
||||
currentPageIndex.value = useCurrentNavigationIndex(newNav).value;
|
||||
});
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
||||
</script>
|
||||
|
|
|
|||
3
pages/settings/compatibility.vue
Normal file
3
pages/settings/compatibility.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
|
@ -58,6 +58,26 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="compatInfo">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<CubeIcon class="h-5 w-5 text-zinc-400" />
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">
|
||||
Compatibility Settings
|
||||
</h3>
|
||||
</div>
|
||||
<div class="mt-2 ml-8 space-y-1">
|
||||
<p class="text-sm text-zinc-400 font-mono">
|
||||
Enabled: {{ compatInfo.enabled ? "Yes" : "No" }}
|
||||
</p>
|
||||
<p class="text-sm text-zinc-400 font-mono">
|
||||
Runner: {{ compatInfo.runner || "Not configured" }}
|
||||
</p>
|
||||
<p class="text-sm text-zinc-400 font-mono">
|
||||
Prefix: {{ compatInfo.prefix || "Not configured" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-6 flex gap-x-4">
|
||||
<button
|
||||
@click="() => openDataDir()"
|
||||
|
|
@ -99,6 +119,11 @@ const clientId = ref<string | null>(null);
|
|||
const platformInfo = ref("Loading...");
|
||||
const baseUrl = ref<string | null>(null);
|
||||
const dataDir = ref<string | null>(null);
|
||||
const compatInfo = ref<{
|
||||
enabled: boolean;
|
||||
runner: string | null;
|
||||
prefix: string | null;
|
||||
} | null>(null);
|
||||
|
||||
const systemData = await invoke<{
|
||||
clientId: string;
|
||||
|
|
|
|||
|
|
@ -59,54 +59,6 @@
|
|||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="border-t border-zinc-600 py-6">
|
||||
<h3 class="text-base font-display font-semibold text-zinc-100">
|
||||
Download Settings
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-zinc-400 max-w-xl">
|
||||
Configure how Drop downloads games and other content.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 max-w-xl">
|
||||
<label for="threads" class="block text-sm font-medium text-zinc-100">
|
||||
Maximum Download Threads
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
type="number"
|
||||
name="threads"
|
||||
id="threads"
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
@click="saveDownloadThreads"
|
||||
: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'
|
||||
]"
|
||||
>
|
||||
{{ saveState.success ? 'Saved' : 'Save Changes' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TransitionRoot as="template" :show="open">
|
||||
<Dialog class="relative z-50" @close="open = false">
|
||||
|
|
@ -220,7 +172,6 @@ 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);
|
||||
|
|
@ -229,14 +180,6 @@ const createDirectoryLoading = ref(false);
|
|||
|
||||
const dirs = ref<Array<string>>([]);
|
||||
|
||||
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");
|
||||
dirs.value = newDirs;
|
||||
|
|
@ -270,7 +213,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
|
||||
|
|
@ -292,42 +235,4 @@ async function deleteDirectory(index: number) {
|
|||
await invoke("delete_download_dir", { index });
|
||||
await updateDirs();
|
||||
}
|
||||
|
||||
async function saveDownloadThreads() {
|
||||
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.error(error, info);
|
||||
console.log(error);
|
||||
const router = useRouter();
|
||||
router.replace(`/error`);
|
||||
});
|
||||
|
|
|
|||
90
src-tauri/Cargo.lock
generated
90
src-tauri/Cargo.lock
generated
|
|
@ -255,12 +255,6 @@ 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"
|
||||
|
|
@ -1006,9 +1000,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "drop-app"
|
||||
version = "0.2.0-beta-prerelease-1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"atomic-instant-full",
|
||||
"boxcar",
|
||||
"chrono",
|
||||
"directories",
|
||||
|
|
@ -1018,7 +1011,6 @@ dependencies = [
|
|||
"log4rs",
|
||||
"md5",
|
||||
"openssl",
|
||||
"parking_lot 0.12.3",
|
||||
"rayon",
|
||||
"reqwest",
|
||||
"rustbreak",
|
||||
|
|
@ -1028,7 +1020,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_with",
|
||||
"shared_child",
|
||||
"slice-deque",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
|
|
@ -1037,7 +1028,6 @@ dependencies = [
|
|||
"tauri-plugin-os",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"throttle_my_fn",
|
||||
"tokio",
|
||||
"umu-wrapper-lib",
|
||||
"url",
|
||||
|
|
@ -2351,7 +2341,7 @@ dependencies = [
|
|||
"log",
|
||||
"log-mdc",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde-value",
|
||||
|
|
@ -2369,15 +2359,6 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mach"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
|
|
@ -2977,17 +2958,6 @@ version = "2.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
|
|
@ -2995,21 +2965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core 0.9.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall 0.2.16",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3020,7 +2976,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.5.8",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
|
@ -3455,15 +3411,6 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.8"
|
||||
|
|
@ -4051,17 +3998,6 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slice-deque"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mach",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
|
|
@ -4094,7 +4030,7 @@ dependencies = [
|
|||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"redox_syscall 0.5.8",
|
||||
"redox_syscall",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"windows-sys 0.59.0",
|
||||
|
|
@ -4152,7 +4088,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
|
|||
dependencies = [
|
||||
"new_debug_unreachable",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"parking_lot",
|
||||
"phf_shared 0.10.0",
|
||||
"precomputed-hash",
|
||||
"serde",
|
||||
|
|
@ -4305,7 +4241,7 @@ dependencies = [
|
|||
"ndk-sys",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"parking_lot",
|
||||
"raw-window-handle",
|
||||
"scopeguard",
|
||||
"tao-macros",
|
||||
|
|
@ -4766,18 +4702,6 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "throttle_my_fn"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "482c185e5675626c9a130b3a8f362c322a239338c882f745a1d9a85838b987f0"
|
||||
dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.37"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.2.0-beta-prerelease-1"
|
||||
version = "0.1.0"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2021"
|
||||
|
|
@ -46,10 +46,6 @@ umu-wrapper-lib = "0.1.0"
|
|||
tauri-plugin-autostart = "2.0.0"
|
||||
shared_child = "1.0.1"
|
||||
serde_with = "3.12.0"
|
||||
slice-deque = "0.3.0"
|
||||
throttle_my_fn = "0.2.6"
|
||||
parking_lot = "0.12.3"
|
||||
atomic-instant-full = "0.1.0"
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.1.1"
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
||||
|
|
@ -1,22 +1,18 @@
|
|||
use std::{env, sync::Mutex};
|
||||
|
||||
use chrono::Utc;
|
||||
use log::{debug, error, warn};
|
||||
use log::{info, 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::{
|
||||
borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls,
|
||||
},
|
||||
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
||||
db::{DatabaseAuth, DatabaseImpls},
|
||||
remote::{DropServerError, RemoteAccessError},
|
||||
AppState, AppStatus, User, DB,
|
||||
};
|
||||
|
||||
use super::requests::make_request;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InitiateRequestBody {
|
||||
|
|
@ -39,7 +35,6 @@ struct HandshakeResponse {
|
|||
id: String,
|
||||
}
|
||||
|
||||
// TODO: Change return value on Err
|
||||
pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
|
||||
let client_private_key = EcKey::private_key_from_pem(private_key.as_bytes()).unwrap();
|
||||
let pkey_private_key = PKey::from_ec_key(client_private_key).unwrap();
|
||||
|
|
@ -55,7 +50,7 @@ pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
|
|||
|
||||
pub fn generate_authorization_header() -> String {
|
||||
let certs = {
|
||||
let db = borrow_db_checked();
|
||||
let db = DB.borrow_data().unwrap();
|
||||
db.auth.clone().unwrap()
|
||||
};
|
||||
|
||||
|
|
@ -69,25 +64,29 @@ 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 = make_request(&client, &["/api/v1/client/user"], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})?
|
||||
.send()?;
|
||||
if response.status() != 200 {
|
||||
let err: DropServerError = response.json()?;
|
||||
warn!("{:?}", err);
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
|
||||
if err.status_message == "Nonce expired" {
|
||||
if response.status() != 200 {
|
||||
let data = response.json::<DropServerError>()?;
|
||||
info!("Could not fetch user: {}", data.status_message);
|
||||
|
||||
if data.status_message == "Nonce expired" {
|
||||
return Err(RemoteAccessError::OutOfSync);
|
||||
}
|
||||
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
return Err(RemoteAccessError::InvalidCodeError(0));
|
||||
}
|
||||
|
||||
response.json::<User>().map_err(|e| e.into())
|
||||
let user = response.json::<User>()?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||
|
|
@ -100,7 +99,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||
}
|
||||
|
||||
let base_url = {
|
||||
let handle = borrow_db_checked();
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
Url::parse(handle.base_url.as_str())?
|
||||
};
|
||||
|
||||
|
|
@ -114,18 +113,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());
|
||||
let response_struct: HandshakeResponse = response.json()?;
|
||||
info!("{}", response.status().as_u16());
|
||||
let response_struct = response.json::<HandshakeResponse>()?;
|
||||
|
||||
{
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
handle.auth = Some(DatabaseAuth {
|
||||
private: response_struct.private,
|
||||
cert: response_struct.certificate,
|
||||
client_id: response_struct.id,
|
||||
});
|
||||
drop(handle);
|
||||
save_db();
|
||||
DB.save().unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -138,6 +137,12 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), String> {
|
||||
recieve_handshake(app, format!("handshake/{}", token));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||
// Tell the app we're processing
|
||||
app.emit("auth/processing", ()).unwrap();
|
||||
|
|
@ -152,9 +157,9 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
|
|||
app.emit("auth/finished", ()).unwrap();
|
||||
}
|
||||
|
||||
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
||||
fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
||||
let base_url = {
|
||||
let db_lock = borrow_db_checked();
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
Url::parse(&db_lock.base_url.clone())?
|
||||
};
|
||||
|
||||
|
|
@ -168,8 +173,8 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
|||
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let data: DropServerError = response.json()?;
|
||||
error!("could not start handshake: {}", data.status_message);
|
||||
let data = response.json::<DropServerError>()?;
|
||||
info!("Could not start handshake: {}", data.status_message);
|
||||
|
||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||
}
|
||||
|
|
@ -177,25 +182,79 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
|||
let redir_url = response.text()?;
|
||||
let complete_redir_url = base_url.join(&redir_url)?;
|
||||
|
||||
debug!("opening web browser to continue authentication");
|
||||
info!("opening web browser to continue authentication");
|
||||
webbrowser::open(complete_redir_url.as_ref()).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup() -> (AppStatus, Option<User>) {
|
||||
let data = borrow_db_checked();
|
||||
#[tauri::command]
|
||||
pub fn auth_initiate<'a>() -> Result<(), String> {
|
||||
let result = auth_initiate_wrapper();
|
||||
if result.is_err() {
|
||||
return Err(result.err().unwrap().to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> Result<(), ()> {
|
||||
let (app_status, user) = setup()?;
|
||||
|
||||
let mut guard = state.lock().unwrap();
|
||||
guard.status = app_status;
|
||||
guard.user = user;
|
||||
drop(guard);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup() -> Result<(AppStatus, Option<User>), ()> {
|
||||
let data = DB.borrow_data().unwrap();
|
||||
let auth = data.auth.clone();
|
||||
drop(data);
|
||||
|
||||
if auth.is_some() {
|
||||
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));
|
||||
let user_result = fetch_user();
|
||||
if user_result.is_err() {
|
||||
let error = user_result.err().unwrap();
|
||||
warn!("auth setup failed with: {}", error);
|
||||
match error {
|
||||
RemoteAccessError::FetchError(_) => {
|
||||
return Ok((AppStatus::ServerUnavailable, None))
|
||||
}
|
||||
_ => return Ok((AppStatus::SignedInNeedsReauth, None)),
|
||||
}
|
||||
}
|
||||
return Ok((AppStatus::SignedIn, Some(user_result.unwrap())));
|
||||
}
|
||||
|
||||
(AppStatus::SignedOut, None)
|
||||
Ok((AppStatus::SignedOut, None))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sign_out(app: AppHandle) -> Result<(), String> {
|
||||
info!("Signing out user");
|
||||
|
||||
// Clear auth from database
|
||||
{
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
handle.auth = None;
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
}
|
||||
|
||||
// Update app state
|
||||
{
|
||||
let app_state = app.state::<Mutex<AppState>>();
|
||||
let mut app_state_handle = app_state.lock().unwrap();
|
||||
app_state_handle.status = AppStatus::SignedOut;
|
||||
app_state_handle.user = None;
|
||||
}
|
||||
|
||||
// Emit event for frontend
|
||||
app.emit("auth/signedout", ()).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,76 +1,69 @@
|
|||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||
use log::debug;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
|
||||
pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let manager = app.autolaunch();
|
||||
if enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
debug!("enabled autostart");
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
debug!("eisabled autostart");
|
||||
}
|
||||
|
||||
// Store the state in DB
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.settings.autostart = enabled;
|
||||
drop(db_handle);
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
// First check DB state
|
||||
let db_handle = borrow_db_checked();
|
||||
let db_state = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
// Get actual system state
|
||||
let manager = app.autolaunch();
|
||||
let system_state = manager.is_enabled()?;
|
||||
|
||||
// If they don't match, sync to DB state
|
||||
if db_state != system_state {
|
||||
if db_state {
|
||||
manager.enable()?;
|
||||
} else {
|
||||
manager.disable()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(db_state)
|
||||
}
|
||||
|
||||
// New function to sync state on startup
|
||||
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||
let db_handle = borrow_db_checked();
|
||||
let should_be_enabled = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
let manager = app.autolaunch();
|
||||
let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
|
||||
|
||||
if current_state != should_be_enabled {
|
||||
if should_be_enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
debug!("synced autostart: enabled");
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
debug!("synced autostart: disabled");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
toggle_autostart_logic(app, enabled)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
get_autostart_enabled_logic(app)
|
||||
}
|
||||
use log::info;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
use crate::DB;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let manager = app.autolaunch();
|
||||
if enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
info!("Enabled autostart");
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
info!("Disabled autostart");
|
||||
}
|
||||
|
||||
// Store the state in DB
|
||||
let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?;
|
||||
db_handle.settings.autostart = enabled;
|
||||
drop(db_handle);
|
||||
DB.save().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_autostart_enabled(app: AppHandle) -> Result<bool, String> {
|
||||
// First check DB state
|
||||
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
|
||||
let db_state = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
// Get actual system state
|
||||
let manager = app.autolaunch();
|
||||
let system_state = manager.is_enabled().map_err(|e| e.to_string())?;
|
||||
|
||||
// If they don't match, sync to DB state
|
||||
if db_state != system_state {
|
||||
if db_state {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(db_state)
|
||||
}
|
||||
|
||||
// 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 should_be_enabled = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
let manager = app.autolaunch();
|
||||
let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
|
||||
|
||||
if current_state != should_be_enabled {
|
||||
if should_be_enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
info!("Synced autostart: enabled");
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
info!("Synced autostart: disabled");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,15 @@
|
|||
use log::{debug, error};
|
||||
|
||||
use log::info;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
cleanup_and_exit(&app, &state);
|
||||
pub fn quit(app: tauri::AppHandle) {
|
||||
cleanup_and_exit(&app);
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
pub fn cleanup_and_exit(app: &AppHandle) {
|
||||
info!("exiting drop application...");
|
||||
|
||||
app.exit(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_state(
|
||||
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||
) -> Result<String, String> {
|
||||
let guard = state.lock().unwrap();
|
||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||
drop(guard);
|
||||
Ok(cloned_state)
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
use std::{
|
||||
fs::create_dir_all,
|
||||
io::{Error, ErrorKind},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
database::{db::borrow_db_mut_checked, settings::Settings},
|
||||
download_manager::internal_error::InternalError,
|
||||
};
|
||||
|
||||
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 = borrow_db_checked();
|
||||
lock.applications.install_dirs.clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_download_dir(index: usize) {
|
||||
let mut lock = borrow_db_mut_checked();
|
||||
lock.applications.install_dirs.remove(index);
|
||||
drop(lock);
|
||||
save_db();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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 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 = borrow_db_mut_checked();
|
||||
if lock.applications.install_dirs.contains(&new_dir) {
|
||||
return Err(Error::new(
|
||||
ErrorKind::AlreadyExists,
|
||||
"Selected directory already exists in database",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_settings(new_settings: Value) {
|
||||
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();
|
||||
}
|
||||
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
|
||||
db_lock.settings = new_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 = borrow_db_checked();
|
||||
SystemData::new(
|
||||
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||
db_handle.base_url.clone(),
|
||||
DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
|
||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemData {
|
||||
client_id: String,
|
||||
base_url: String,
|
||||
data_dir: String,
|
||||
log_level: String,
|
||||
}
|
||||
|
||||
impl SystemData {
|
||||
pub fn new(client_id: String, base_url: String, data_dir: String, log_level: String) -> Self {
|
||||
Self {
|
||||
client_id,
|
||||
base_url,
|
||||
data_dir,
|
||||
log_level,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
pub mod commands;
|
||||
pub mod db;
|
||||
pub mod debug;
|
||||
pub mod settings;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Settings {
|
||||
pub autostart: bool,
|
||||
pub max_download_threads: usize,
|
||||
// ... other settings ...
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
autostart: false,
|
||||
max_download_threads: 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ideally use pointers instead of a macro to assign the settings
|
||||
// fn deserialize_into<T>(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
|
||||
// where T: for<'a> Deserialize<'a>
|
||||
// {
|
||||
// *t = serde_json::from_value(v)?;
|
||||
// Ok(())
|
||||
// }
|
||||
|
|
@ -1,27 +1,20 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, create_dir_all},
|
||||
hash::Hash,
|
||||
path::{Path, PathBuf},
|
||||
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
|
||||
sync::{Arc, LazyLock, Mutex, RwLockWriteGuard}, time::{Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use directories::BaseDirs;
|
||||
use log::{debug, error, info};
|
||||
use log::debug;
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
use tauri::AppHandle;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::settings::Settings,
|
||||
download_manager::downloadable_metadata::DownloadableMetadata,
|
||||
games::{library::push_game_update, state::GameStatusManager},
|
||||
process::process_manager::Platform,
|
||||
DB,
|
||||
};
|
||||
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, games::{library::push_game_update, state::GameStatusManager}, process::process_manager::Platform, DB};
|
||||
|
||||
#[derive(serde::Serialize, Clone, Deserialize)]
|
||||
pub struct DatabaseAuth {
|
||||
|
|
@ -57,30 +50,18 @@ pub enum ApplicationTransientStatus {
|
|||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameVersion {
|
||||
pub game_id: String,
|
||||
pub version_name: 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>,
|
||||
pub version_name: String,
|
||||
pub launch_command: String,
|
||||
pub setup_command: String,
|
||||
pub platform: Platform,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Clone, Deserialize, Default)]
|
||||
#[derive(Serialize, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DatabaseApplications {
|
||||
pub install_dirs: Vec<PathBuf>,
|
||||
pub install_dirs: Vec<String>,
|
||||
// Guaranteed to exist if the game also exists in the app state map
|
||||
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
||||
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
||||
|
|
@ -90,34 +71,29 @@ pub struct DatabaseApplications {
|
|||
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Settings {
|
||||
pub autostart: bool,
|
||||
// ... other settings ...
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
autostart: false,
|
||||
// ... other settings defaults ...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Database {
|
||||
#[serde(default)]
|
||||
pub settings: Settings,
|
||||
pub auth: Option<DatabaseAuth>,
|
||||
pub base_url: String,
|
||||
pub applications: DatabaseApplications,
|
||||
pub prev_database: Option<PathBuf>,
|
||||
}
|
||||
impl Database {
|
||||
fn new<T: Into<PathBuf>>(games_base_dir: T, prev_database: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
applications: DatabaseApplications {
|
||||
install_dirs: vec![games_base_dir.into()],
|
||||
game_statuses: HashMap::new(),
|
||||
game_versions: HashMap::new(),
|
||||
installed_game_version: HashMap::new(),
|
||||
transient_statuses: HashMap::new(),
|
||||
},
|
||||
prev_database,
|
||||
base_url: "".to_owned(),
|
||||
auth: None,
|
||||
settings: Settings {
|
||||
autostart: false,
|
||||
max_download_threads: 4,
|
||||
},
|
||||
}
|
||||
}
|
||||
pub prev_database: Option<PathBuf>
|
||||
}
|
||||
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
|
||||
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
|
||||
|
|
@ -151,20 +127,35 @@ 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();
|
||||
debug!("Creating games directory");
|
||||
create_dir_all(games_base_dir.clone()).unwrap();
|
||||
debug!("Creating logs directory");
|
||||
create_dir_all(logs_root_dir.clone()).unwrap();
|
||||
|
||||
let exists = fs::exists(db_path.clone()).unwrap();
|
||||
|
||||
match exists {
|
||||
true => match PathDatabase::load_from_path(db_path.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir),
|
||||
},
|
||||
true =>
|
||||
match PathDatabase::load_from_path(db_path.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir),
|
||||
},
|
||||
false => {
|
||||
let default = Database::new(games_base_dir, None);
|
||||
let default = Database {
|
||||
settings: Settings::default(),
|
||||
auth: None,
|
||||
base_url: "".to_string(),
|
||||
applications: DatabaseApplications {
|
||||
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
|
||||
game_statuses: HashMap::new(),
|
||||
transient_statuses: HashMap::new(),
|
||||
game_versions: HashMap::new(),
|
||||
installed_game_version: HashMap::new(),
|
||||
},
|
||||
prev_database: None,
|
||||
};
|
||||
debug!(
|
||||
"Creating database at path {}",
|
||||
db_path.as_os_str().to_str().unwrap()
|
||||
|
|
@ -185,72 +176,103 @@ impl DatabaseImpls for DatabaseInterface {
|
|||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_download_dir(new_dir: String) -> Result<(), String> {
|
||||
// Check the new directory is all good
|
||||
let new_dir_path = Path::new(&new_dir);
|
||||
if new_dir_path.exists() {
|
||||
let metadata = new_dir_path
|
||||
.metadata()
|
||||
.map_err(|e| format!("Unable to access file or directory: {}", e))?;
|
||||
if !metadata.is_dir() {
|
||||
return Err("Invalid path: not a directory".to_string());
|
||||
}
|
||||
let dir_contents = new_dir_path
|
||||
.read_dir()
|
||||
.map_err(|e| format!("Unable to check directory contents: {}", e))?;
|
||||
if dir_contents.count() != 0 {
|
||||
return Err("Directory is not empty".to_string());
|
||||
}
|
||||
} else {
|
||||
create_dir_all(new_dir_path)
|
||||
.map_err(|e| format!("Unable to create directories to path: {}", e))?;
|
||||
}
|
||||
|
||||
// Add it to the dictionary
|
||||
let mut lock = DB.borrow_data_mut().unwrap();
|
||||
if lock.applications.install_dirs.contains(&new_dir) {
|
||||
return Err("Download directory already used".to_string());
|
||||
}
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
DB.save().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_download_dir(index: usize) -> Result<(), String> {
|
||||
let mut lock = DB.borrow_data_mut().unwrap();
|
||||
lock.applications.install_dirs.remove(index);
|
||||
drop(lock);
|
||||
DB.save().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Will, in future, return disk/remaining size
|
||||
// Just returns the directories that have been set up
|
||||
#[tauri::command]
|
||||
pub fn fetch_download_dir_stats() -> Result<Vec<String>, String> {
|
||||
let lock = DB.borrow_data().unwrap();
|
||||
let directories = lock.applications.install_dirs.clone();
|
||||
drop(lock);
|
||||
|
||||
Ok(directories)
|
||||
}
|
||||
|
||||
pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &DownloadableMetadata)>(
|
||||
app_handle: &AppHandle,
|
||||
meta: DownloadableMetadata,
|
||||
setter: F,
|
||||
) {
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
setter(&mut db_handle, &meta);
|
||||
drop(db_handle);
|
||||
save_db();
|
||||
DB.save().unwrap();
|
||||
|
||||
let status = GameStatusManager::fetch_state(&meta.id);
|
||||
|
||||
push_game_update(app_handle, &meta.id, status);
|
||||
push_game_update(app_handle, &meta, status);
|
||||
}
|
||||
|
||||
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
|
||||
fn handle_invalid_database(
|
||||
_e: RustbreakError,
|
||||
db_path: PathBuf,
|
||||
games_base_dir: PathBuf,
|
||||
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||
let new_path = {
|
||||
fn handle_invalid_database(_e: RustbreakError, db_path: PathBuf, games_base_dir: PathBuf) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||
let new_path = {
|
||||
let time = Utc::now().timestamp();
|
||||
let mut base = db_path.clone();
|
||||
base.set_file_name(format!("drop.db.backup-{}", time));
|
||||
let mut base = db_path.clone().into_os_string();
|
||||
base.push(".");
|
||||
base.push(time.to_string());
|
||||
base
|
||||
};
|
||||
info!(
|
||||
"old database stored at: {}",
|
||||
new_path.to_string_lossy().to_string()
|
||||
);
|
||||
fs::rename(&db_path, &new_path).unwrap();
|
||||
fs::copy(&db_path, &new_path).unwrap();
|
||||
|
||||
let db = Database::new(
|
||||
games_base_dir.into_os_string().into_string().unwrap(),
|
||||
Some(new_path),
|
||||
);
|
||||
let db = Database {
|
||||
auth: None,
|
||||
base_url: "".to_string(),
|
||||
applications: DatabaseApplications {
|
||||
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
|
||||
game_statuses: HashMap::new(),
|
||||
transient_statuses: HashMap::new(),
|
||||
game_versions: HashMap::new(),
|
||||
installed_game_version: HashMap::new(),
|
||||
},
|
||||
prev_database: Some(new_path.into()),
|
||||
settings: Settings { autostart: false },
|
||||
};
|
||||
|
||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||
}
|
||||
PathDatabase::create_at_path(db_path, db)
|
||||
.expect("Database could not be created")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src-tauri/src/debug.rs
Normal file
23
src-tauri/src/debug.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::{DATA_ROOT_DIR, DB};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemData {
|
||||
client_id: String,
|
||||
base_url: String,
|
||||
data_dir: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_system_data() -> Result<SystemData, String> {
|
||||
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
|
||||
let system_data = SystemData {
|
||||
client_id: db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||
base_url: db_handle.base_url.clone(),
|
||||
data_dir: DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
|
||||
};
|
||||
drop(db_handle);
|
||||
|
||||
Ok(system_data)
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
io,
|
||||
};
|
||||
use std::{fmt::{Display, Formatter}, io};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
|
||||
use crate::remote::RemoteAccessError;
|
||||
|
||||
// TODO: Rename / separate from downloads
|
||||
#[derive(Debug, Clone, SerializeDisplay)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ApplicationDownloadError {
|
||||
Communication(RemoteAccessError),
|
||||
Checksum,
|
||||
|
|
@ -22,11 +17,25 @@ 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SetupError {
|
||||
Context,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.pause_downloads()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.resume_downloads()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn move_download_in_queue(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
old_index: usize,
|
||||
new_index: usize,
|
||||
) {
|
||||
state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.rearrange(old_index, new_index)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||
state.lock().unwrap().download_manager.cancel(meta)
|
||||
}
|
||||
|
|
@ -4,21 +4,16 @@ use std::{
|
|||
fmt::Debug,
|
||||
sync::{
|
||||
mpsc::{SendError, Sender},
|
||||
Mutex, MutexGuard,
|
||||
Arc, Mutex, MutexGuard,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use log::{debug, info};
|
||||
use log::info;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::error::application_download_error::ApplicationDownloadError;
|
||||
|
||||
use super::{
|
||||
download_manager_builder::{CurrentProgressObject, DownloadAgent},
|
||||
downloadable_metadata::DownloadableMetadata,
|
||||
queue::Queue,
|
||||
};
|
||||
use super::{application_download_error::ApplicationDownloadError, download_manager_builder::{CurrentProgressObject, DownloadAgent}, downloadable_metadata::DownloadableMetadata, queue::Queue};
|
||||
|
||||
pub enum DownloadManagerSignal {
|
||||
/// Resumes (or starts) the DownloadManager
|
||||
|
|
@ -84,7 +79,7 @@ pub enum DownloadStatus {
|
|||
/// which provides raw access to the underlying queue.
|
||||
/// THIS EDITING IS BLOCKING!!!
|
||||
pub struct DownloadManager {
|
||||
terminator: Mutex<Option<JoinHandle<Result<(), ()>>>>,
|
||||
terminator: JoinHandle<Result<(), ()>>,
|
||||
download_queue: Queue,
|
||||
progress: CurrentProgressObject,
|
||||
command_sender: Sender<DownloadManagerSignal>,
|
||||
|
|
@ -99,7 +94,7 @@ impl DownloadManager {
|
|||
command_sender: Sender<DownloadManagerSignal>,
|
||||
) -> Self {
|
||||
Self {
|
||||
terminator: Mutex::new(Some(terminator)),
|
||||
terminator,
|
||||
download_queue,
|
||||
progress,
|
||||
command_sender,
|
||||
|
|
@ -108,11 +103,10 @@ impl DownloadManager {
|
|||
|
||||
pub fn queue_download(
|
||||
&self,
|
||||
download: DownloadAgent,
|
||||
download: DownloadAgent
|
||||
) -> Result<(), SendError<DownloadManagerSignal>> {
|
||||
info!("creating download with meta {:?}", download.metadata());
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Queue(download))?;
|
||||
info!("Adding download id {:?}", download.metadata());
|
||||
self.command_sender.send(DownloadManagerSignal::Queue(download))?;
|
||||
self.command_sender.send(DownloadManagerSignal::Go)
|
||||
}
|
||||
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
|
||||
|
|
@ -151,14 +145,12 @@ impl DownloadManager {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
debug!(
|
||||
"moving download at index {} to index {}",
|
||||
current_index, new_index
|
||||
);
|
||||
info!("moving {} to {}", 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 {
|
||||
|
|
@ -167,10 +159,6 @@ impl DownloadManager {
|
|||
self.command_sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Go)
|
||||
.unwrap();
|
||||
|
||||
}
|
||||
pub fn pause_downloads(&self) {
|
||||
self.command_sender
|
||||
|
|
@ -180,12 +168,11 @@ 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();
|
||||
let terminator = self.terminator.lock().unwrap().take();
|
||||
terminator.unwrap().join()
|
||||
self.terminator.join()
|
||||
}
|
||||
pub fn uninstall_application(&self, meta: DownloadableMetadata) {
|
||||
self.command_sender
|
||||
|
|
|
|||
|
|
@ -1,28 +1,19 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::remove_dir_all,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Arc, Mutex,
|
||||
Arc, Mutex, RwLockWriteGuard,
|
||||
},
|
||||
thread::{spawn, JoinHandle},
|
||||
};
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
use log::{error, info};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::{
|
||||
error::application_download_error::ApplicationDownloadError,
|
||||
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
|
||||
};
|
||||
use crate::games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent};
|
||||
|
||||
use super::{
|
||||
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
downloadable::Downloadable,
|
||||
downloadable_metadata::DownloadableMetadata,
|
||||
progress_object::ProgressObject,
|
||||
queue::Queue,
|
||||
};
|
||||
use super::{application_download_error::ApplicationDownloadError, download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject, queue::Queue};
|
||||
|
||||
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
||||
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
|
||||
|
|
@ -56,7 +47,7 @@ whichever download queue order is required.
|
|||
+----------------------------------------------------------------------------+
|
||||
|
||||
This download queue does not actually own any of the DownloadAgents. It is
|
||||
simply an id-based reference system. The actual Agents are stored in the
|
||||
simply a id-based reference system. The actual Agents are stored in the
|
||||
download_agent_registry HashMap, as ordering is no issue here. This is why
|
||||
appending or removing from the download_queue must be done via signals.
|
||||
|
||||
|
|
@ -124,6 +115,7 @@ impl DownloadManagerBuilder {
|
|||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
*download_thread_lock = None;
|
||||
drop(download_thread_lock);
|
||||
|
||||
}
|
||||
|
||||
fn stop_and_wait_current_download(&self) {
|
||||
|
|
@ -138,6 +130,7 @@ impl DownloadManagerBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fn manage_queue(mut self) -> Result<(), ()> {
|
||||
loop {
|
||||
let signal = match self.command_receiver.recv() {
|
||||
|
|
@ -179,13 +172,13 @@ impl DownloadManagerBuilder {
|
|||
}
|
||||
}
|
||||
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||
debug!("got signal Queue");
|
||||
info!("Got signal Queue");
|
||||
let meta = download_agent.metadata();
|
||||
|
||||
debug!("queue metadata: {:?}", meta);
|
||||
info!("Meta: {:?}", meta);
|
||||
|
||||
if self.download_queue.exists(meta.clone()) {
|
||||
warn!("download with same ID already exists");
|
||||
info!("Download with same ID already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -193,37 +186,27 @@ impl DownloadManagerBuilder {
|
|||
self.download_queue.append(meta.clone());
|
||||
self.download_agent_registry.insert(meta, download_agent);
|
||||
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
self.sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||
}
|
||||
|
||||
fn manage_go_signal(&mut self) {
|
||||
debug!("got signal Go");
|
||||
if self.download_agent_registry.is_empty() {
|
||||
debug!(
|
||||
"Download agent registry: {:?}",
|
||||
self.download_agent_registry.len()
|
||||
);
|
||||
return;
|
||||
info!("Got signal Go");
|
||||
if self.download_agent_registry.is_empty() {
|
||||
info!("Download agent registry: {:?}", self.download_agent_registry.len());
|
||||
return;
|
||||
}
|
||||
|
||||
if self.current_download_agent.is_some() {
|
||||
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 agent: {:?}", self.current_download_agent.as_ref().unwrap().metadata());
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("current download queue: {:?}", self.download_queue.read());
|
||||
info!("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
|
||||
|
|
@ -243,21 +226,17 @@ impl DownloadManagerBuilder {
|
|||
match download_agent.download(&app_handle) {
|
||||
// Ok(true) is for completed and exited properly
|
||||
Ok(true) => {
|
||||
debug!("download {:?} has completed", download_agent.metadata());
|
||||
download_agent.on_complete(&app_handle);
|
||||
sender
|
||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
||||
.unwrap();
|
||||
}
|
||||
sender.send(DownloadManagerSignal::Completed(download_agent.metadata())).unwrap();
|
||||
},
|
||||
// Ok(false) is for incomplete but exited properly
|
||||
Ok(false) => {
|
||||
download_agent.on_incomplete(&app_handle);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
||||
download_agent.on_error(&app_handle, e.clone());
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
},
|
||||
}
|
||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||
}));
|
||||
|
|
@ -267,7 +246,7 @@ impl DownloadManagerBuilder {
|
|||
active_control_flag.set(DownloadThreadControlFlag::Go);
|
||||
}
|
||||
fn manage_stop_signal(&mut self) {
|
||||
debug!("got signal Stop");
|
||||
info!("Got signal Stop");
|
||||
|
||||
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
|
|
@ -275,9 +254,10 @@ impl DownloadManagerBuilder {
|
|||
}
|
||||
}
|
||||
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
||||
debug!("got signal Completed");
|
||||
info!("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);
|
||||
}
|
||||
}
|
||||
|
|
@ -285,17 +265,18 @@ impl DownloadManagerBuilder {
|
|||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||
debug!("got signal Error");
|
||||
if let Some(current_agent) = self.current_download_agent.clone() {
|
||||
current_agent.on_error(&self.app_handle, error.clone());
|
||||
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());
|
||||
|
||||
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) {
|
||||
debug!("got signal Cancel");
|
||||
info!("Got signal Cancel");
|
||||
|
||||
if let Some(current_download) = &self.current_download_agent {
|
||||
if ¤t_download.metadata() == meta {
|
||||
|
|
@ -306,34 +287,33 @@ impl DownloadManagerBuilder {
|
|||
self.download_queue.pop_front();
|
||||
|
||||
self.cleanup_current_download();
|
||||
debug!("current download queue: {:?}", self.download_queue.read());
|
||||
info!("Current donwload 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) {
|
||||
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 {:?}", removed.and_then(|x| Some(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);
|
||||
debug!(
|
||||
"removed {:?} from queue {:?}",
|
||||
removed.map(|x| x.metadata()),
|
||||
self.download_queue.read()
|
||||
);
|
||||
info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
|
||||
}
|
||||
}
|
||||
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||
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);
|
||||
debug!(
|
||||
"removed {:?} from queue {:?}",
|
||||
removed.map(|x| x.metadata()),
|
||||
self.download_queue.read()
|
||||
);
|
||||
}
|
||||
}
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
|
|
@ -346,19 +326,18 @@ impl DownloadManagerBuilder {
|
|||
let queue = &self.download_queue.read();
|
||||
let queue_objs = queue
|
||||
.iter()
|
||||
.map(|key| {
|
||||
.map(|(key)| {
|
||||
let val = self.download_agent_registry.get(key).unwrap();
|
||||
QueueUpdateEventQueueData {
|
||||
meta: DownloadableMetadata::clone(key),
|
||||
meta: DownloadableMetadata::clone(&key),
|
||||
status: val.status(),
|
||||
progress: val.progress().get_progress(),
|
||||
current: val.progress().sum(),
|
||||
max: val.progress().get_max(),
|
||||
}
|
||||
})
|
||||
progress: val.progress().get_progress()
|
||||
}})
|
||||
.collect();
|
||||
|
||||
let event_data = QueueUpdateEvent { queue: queue_objs };
|
||||
let event_data = QueueUpdateEvent {
|
||||
queue: queue_objs,
|
||||
};
|
||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
use std::sync::Arc;
|
||||
use std::{fmt::{self, Debug}, sync::{mpsc::Sender, Arc}};
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::error::application_download_error::ApplicationDownloadError;
|
||||
|
||||
use super::{
|
||||
download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl,
|
||||
downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject,
|
||||
application_download_error::ApplicationDownloadError, download_manager::{DownloadManagerSignal, DownloadStatus}, download_thread_control_flag::DownloadThreadControl, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject
|
||||
};
|
||||
|
||||
pub trait Downloadable: Send + Sync {
|
||||
|
|
@ -20,4 +17,4 @@ pub trait Downloadable: Send + Sync {
|
|||
fn on_complete(&self, app_handle: &AppHandle);
|
||||
fn on_incomplete(&self, app_handle: &AppHandle);
|
||||
fn on_cancelled(&self, app_handle: &AppHandle);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ pub enum DownloadType {
|
|||
Game,
|
||||
Tool,
|
||||
DLC,
|
||||
Mod,
|
||||
Mod
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
|
||||
|
|
@ -13,14 +13,14 @@ pub enum DownloadType {
|
|||
pub struct DownloadableMetadata {
|
||||
pub id: String,
|
||||
pub version: Option<String>,
|
||||
pub download_type: DownloadType,
|
||||
pub download_type: DownloadType
|
||||
}
|
||||
impl DownloadableMetadata {
|
||||
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
|
||||
Self {
|
||||
id,
|
||||
version,
|
||||
download_type,
|
||||
download_type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src-tauri/src/download_manager/generate_downloadable.rs
Normal file
7
src-tauri/src/download_manager/generate_downloadable.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use super::{download_manager_builder::DownloadAgent, downloadable_metadata::DownloadableMetadata};
|
||||
|
||||
pub fn generate_downloadable(meta: DownloadableMetadata) -> DownloadAgent {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
pub mod commands;
|
||||
pub mod download_manager;
|
||||
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;
|
||||
pub mod download_thread_control_flag;
|
||||
pub mod downloadable;
|
||||
pub mod application_download_error;
|
||||
pub mod downloadable_metadata;
|
||||
pub mod generate_downloadable;
|
||||
|
|
@ -2,17 +2,14 @@ use std::{
|
|||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::Sender,
|
||||
Arc, Mutex,
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use atomic_instant_full::AtomicInstant;
|
||||
use throttle_my_fn::throttle;
|
||||
use log::info;
|
||||
|
||||
use super::{
|
||||
download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
|
||||
};
|
||||
use super::download_manager::DownloadManagerSignal;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgressObject {
|
||||
|
|
@ -20,10 +17,11 @@ pub struct ProgressObject {
|
|||
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
|
||||
start: Arc<Mutex<Instant>>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
//last_update: Arc<RwLock<Instant>>,
|
||||
last_update_time: Arc<AtomicInstant>,
|
||||
bytes_last_update: Arc<AtomicUsize>,
|
||||
rolling: RollingProgressWindow<250>,
|
||||
|
||||
points_towards_update: Arc<AtomicUsize>,
|
||||
points_to_push_update: Arc<AtomicUsize>,
|
||||
last_update: Arc<RwLock<Instant>>,
|
||||
amount_last_update: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
pub struct ProgressHandle {
|
||||
|
|
@ -44,32 +42,72 @@ impl ProgressHandle {
|
|||
pub fn add(&self, amount: usize) {
|
||||
self.progress
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
||||
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
|
||||
self.progress_object.check_push_update(amount);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
last_update_time: Arc::new(AtomicInstant::now()),
|
||||
bytes_last_update: Arc::new(AtomicUsize::new(0)),
|
||||
rolling: RollingProgressWindow::new(),
|
||||
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);
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let last_update = self.last_update.read().unwrap();
|
||||
let last_update_difference = Instant::now().duration_since(*last_update).as_millis();
|
||||
if last_update_difference > 1000 {
|
||||
// push update
|
||||
drop(last_update);
|
||||
let mut last_update = self.last_update.write().unwrap();
|
||||
*last_update = Instant::now();
|
||||
drop(last_update);
|
||||
|
||||
let current_amount = self.sum();
|
||||
let max = self.get_max();
|
||||
let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed);
|
||||
self.amount_last_update
|
||||
.store(current_amount, Ordering::Relaxed);
|
||||
|
||||
let amount_since_last_update = current_amount - amount_at_last_update;
|
||||
|
||||
let kilobytes_per_second = amount_since_last_update / (last_update_difference as usize).max(1);
|
||||
|
||||
let remaining = max - current_amount; // bytes
|
||||
let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1);
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIStats(
|
||||
kilobytes_per_second,
|
||||
time_remaining,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +127,9 @@ 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() =
|
||||
|
|
@ -100,56 +141,4 @@ 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);
|
||||
}
|
||||
|
||||
fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
|
||||
progress_object
|
||||
.sender
|
||||
.send(DownloadManagerSignal::UpdateUIStats(
|
||||
kilobytes_per_second,
|
||||
time_remaining,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn update_queue(progress: &ProgressObject) {
|
||||
progress
|
||||
.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,6 @@ pub struct Queue {
|
|||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Default for Queue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Queue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
@ -32,7 +26,7 @@ impl Queue {
|
|||
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
|
||||
self.edit().pop_front()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
pub fn empty(&self) -> bool {
|
||||
self.inner.lock().unwrap().len() == 0
|
||||
}
|
||||
pub fn exists(&self, meta: DownloadableMetadata) -> bool {
|
||||
|
|
@ -50,9 +44,15 @@ impl Queue {
|
|||
pub fn append(&self, interface: DownloadableMetadata) {
|
||||
self.edit().push_back(interface);
|
||||
}
|
||||
pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option<DownloadableMetadata> {
|
||||
pub fn pop_front_if_equal(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
) -> Option<DownloadableMetadata> {
|
||||
let mut queue = self.edit();
|
||||
let front = queue.front()?;
|
||||
let front = match queue.front() {
|
||||
Some(front) => front,
|
||||
None => return None,
|
||||
};
|
||||
if front == meta {
|
||||
return queue.pop_front();
|
||||
}
|
||||
|
|
@ -61,11 +61,7 @@ impl Queue {
|
|||
pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option<usize> {
|
||||
self.read().iter().position(|data| data == meta)
|
||||
}
|
||||
pub fn move_to_index_by_meta(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
new_index: usize,
|
||||
) -> Result<(), ()> {
|
||||
pub fn move_to_index_by_meta(&self, meta: &DownloadableMetadata, new_index: usize) -> Result<(), ()> {
|
||||
let index = match self.get_by_meta(meta) {
|
||||
Some(index) => index,
|
||||
None => return Err(()),
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DropServerError {
|
||||
pub status_code: usize,
|
||||
pub status_message: String,
|
||||
pub message: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(SerializeDisplay)]
|
||||
pub enum LibraryError {
|
||||
MetaNotFound(String),
|
||||
}
|
||||
impl Display for LibraryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LibraryError::MetaNotFound(id) => write!(
|
||||
f,
|
||||
"Could not locate any installed version of game ID {} in the database",
|
||||
id
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
pub mod application_download_error;
|
||||
pub mod drop_server_error;
|
||||
pub mod library_error;
|
||||
pub mod process_error;
|
||||
pub mod remote_access_error;
|
||||
pub mod setup_error;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
use std::{fmt::Display, io::Error};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(SerializeDisplay)]
|
||||
pub enum ProcessError {
|
||||
SetupRequired,
|
||||
NotInstalled,
|
||||
AlreadyRunning,
|
||||
NotDownloaded,
|
||||
InvalidID,
|
||||
InvalidVersion,
|
||||
IOError(Error),
|
||||
InvalidPlatform,
|
||||
}
|
||||
|
||||
impl Display for ProcessError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
ProcessError::SetupRequired => "Game not set up",
|
||||
ProcessError::NotInstalled => "Game not installed",
|
||||
ProcessError::AlreadyRunning => "Game already running",
|
||||
ProcessError::NotDownloaded => "Game not downloaded",
|
||||
ProcessError::InvalidID => "Invalid Game ID",
|
||||
ProcessError::InvalidVersion => "Invalid Game version",
|
||||
ProcessError::IOError(error) => &error.to_string(),
|
||||
ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Formatter},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use http::StatusCode;
|
||||
use serde_with::SerializeDisplay;
|
||||
use url::ParseError;
|
||||
|
||||
use super::drop_server_error::DropServerError;
|
||||
|
||||
#[derive(Debug, Clone, SerializeDisplay)]
|
||||
pub enum RemoteAccessError {
|
||||
FetchError(Arc<reqwest::Error>),
|
||||
ParsingError(ParseError),
|
||||
InvalidEndpoint,
|
||||
HandshakeFailed(String),
|
||||
GameNotFound,
|
||||
InvalidResponse(DropServerError),
|
||||
InvalidRedirect,
|
||||
ManifestDownloadFailed(StatusCode, String),
|
||||
OutOfSync,
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
impl Display for RemoteAccessError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RemoteAccessError::FetchError(error) => write!(
|
||||
f,
|
||||
"{}: {}",
|
||||
error,
|
||||
error
|
||||
.source()
|
||||
.map(|e| e.to_string())
|
||||
.or_else(|| Some("Unknown error".to_string()))
|
||||
.unwrap()
|
||||
),
|
||||
RemoteAccessError::ParsingError(parse_error) => {
|
||||
write!(f, "{}", parse_error)
|
||||
}
|
||||
RemoteAccessError::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: {} {}",
|
||||
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::Generic(message) => write!(f, "{}", message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for RemoteAccessError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
RemoteAccessError::FetchError(Arc::new(err))
|
||||
}
|
||||
}
|
||||
impl From<ParseError> for RemoteAccessError {
|
||||
fn from(err: ParseError) -> Self {
|
||||
RemoteAccessError::ParsingError(err)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for RemoteAccessError {}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SetupError {
|
||||
Context,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::{
|
||||
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,
|
||||
},
|
||||
state::{GameStatusManager, GameStatusWithTransient},
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
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,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
fetch_game_logic(game_id, app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
||||
GameStatusManager::fetch_state(&id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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 Err(LibraryError::MetaNotFound(game_id)),
|
||||
};
|
||||
println!("{:?}", meta);
|
||||
uninstall_game_logic(meta, &app_handle);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_verion_options(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
fetch_game_verion_options_logic(game_id, state)
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
download_manager::{
|
||||
download_manager::DownloadManagerSignal, downloadable::Downloadable,
|
||||
internal_error::InternalError,
|
||||
},
|
||||
AppState,
|
||||
};
|
||||
|
||||
use super::download_agent::GameDownloadAgent;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn download_game(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
install_dir: usize,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), InternalError<DownloadManagerSignal>> {
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
||||
game_id,
|
||||
game_version,
|
||||
install_dir,
|
||||
sender,
|
||||
)) as Box<dyn Downloadable + Send + Sync>);
|
||||
Ok(state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)?)
|
||||
}
|
||||
|
|
@ -1,30 +1,25 @@
|
|||
use crate::auth::generate_authorization_header;
|
||||
use crate::database::db::{
|
||||
borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls,
|
||||
GameDownloadStatus,
|
||||
};
|
||||
use crate::db::{set_game_status, GameDownloadStatus, ApplicationTransientStatus, DatabaseImpls};
|
||||
use crate::download_manager::application_download_error::ApplicationDownloadError;
|
||||
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
||||
use crate::download_manager::download_thread_control_flag::{
|
||||
DownloadThreadControl, DownloadThreadControlFlag,
|
||||
};
|
||||
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
use crate::download_manager::downloadable::Downloadable;
|
||||
use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
|
||||
use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
|
||||
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::games::library::{on_game_complete, push_game_update};
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::DB;
|
||||
use log::{debug, error, info};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use slice_deque::SliceDeque;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{create_dir_all, remove_dir_all, File};
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::spawn;
|
||||
use std::time::Instant;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use urlencoding::encode;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -38,12 +33,12 @@ pub struct GameDownloadAgent {
|
|||
pub version: String,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||
completed_contexts: Mutex<SliceDeque<usize>>,
|
||||
completed_contexts: Mutex<VecDeque<usize>>,
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub stored_manifest: StoredManifest,
|
||||
status: Mutex<DownloadStatus>,
|
||||
status: Mutex<DownloadStatus>
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
|
|
@ -56,7 +51,7 @@ impl GameDownloadAgent {
|
|||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let db_lock = borrow_db_checked();
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
|
|
@ -72,7 +67,7 @@ impl GameDownloadAgent {
|
|||
control_flag,
|
||||
manifest: Mutex::new(None),
|
||||
contexts: Mutex::new(Vec::new()),
|
||||
completed_contexts: Mutex::new(SliceDeque::new()),
|
||||
completed_contexts: Mutex::new(VecDeque::new()),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
stored_manifest,
|
||||
|
|
@ -83,8 +78,10 @@ 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);
|
||||
|
||||
|
|
@ -93,24 +90,16 @@ 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,
|
||||
&self.metadata().id,
|
||||
(
|
||||
None,
|
||||
Some(ApplicationTransientStatus::Downloading {
|
||||
version_name: self.version.clone(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
let res = self
|
||||
.run()
|
||||
.map_err(|_| ApplicationDownloadError::DownloadError);
|
||||
push_game_update(app_handle, &self.metadata(), (None, Some(ApplicationTransientStatus::Downloading { version_name: self.version.clone() })));
|
||||
let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError);
|
||||
|
||||
debug!(
|
||||
info!(
|
||||
"{} took {}ms to download",
|
||||
self.id,
|
||||
timer.elapsed().as_millis()
|
||||
|
|
@ -127,17 +116,25 @@ 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/metadata/manifest?id={}&version={}",
|
||||
self.id,
|
||||
encode(&self.version)
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
let client = reqwest::blocking::Client::new();
|
||||
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()))?;
|
||||
let response = client
|
||||
.get(manifest_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
|
|
@ -148,7 +145,7 @@ impl GameDownloadAgent {
|
|||
));
|
||||
}
|
||||
|
||||
let manifest_download: DropManifest = response.json().unwrap();
|
||||
let manifest_download = response.json::<DropManifest>().unwrap();
|
||||
|
||||
if let Ok(mut manifest) = self.manifest.lock() {
|
||||
*manifest = Some(manifest_download);
|
||||
|
|
@ -170,8 +167,11 @@ 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();
|
||||
}
|
||||
|
||||
|
|
@ -192,11 +192,12 @@ impl GameDownloadAgent {
|
|||
let base_path = Path::new(&self.stored_manifest.base_path);
|
||||
create_dir_all(base_path).unwrap();
|
||||
|
||||
|
||||
{
|
||||
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||
completed_contexts_lock.clear();
|
||||
completed_contexts_lock
|
||||
.extend_from_slice(&self.stored_manifest.get_completed_contexts());
|
||||
.extend(self.stored_manifest.get_completed_contexts());
|
||||
}
|
||||
|
||||
for (raw_path, chunk) in manifest {
|
||||
|
|
@ -233,22 +234,22 @@ impl GameDownloadAgent {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Change return value on Err
|
||||
pub fn run(&self) -> Result<bool, ()> {
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
info!("downloading game: {}", self.id);
|
||||
const DOWNLOAD_MAX_THREADS: usize = 1;
|
||||
|
||||
debug!(
|
||||
"downloading game: {} with {} threads",
|
||||
self.id, max_download_threads
|
||||
);
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(max_download_threads)
|
||||
.num_threads(DOWNLOAD_MAX_THREADS)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
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();
|
||||
|
|
@ -258,36 +259,19 @@ 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.skip(context.length);
|
||||
progress_handle.add(context.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
let request = generate_request(&base_url, client, &context);
|
||||
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match download_game_chunk(context, &self.control_flag, progress_handle, request)
|
||||
{
|
||||
match download_game_chunk(context, &self.control_flag, progress_handle, request) {
|
||||
Ok(res) => {
|
||||
if res {
|
||||
completed_indexes.push(index);
|
||||
|
|
@ -298,35 +282,39 @@ impl GameDownloadAgent {
|
|||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
info!("Completed context id {}", index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let newly_completed = completed_indexes.to_owned();
|
||||
|
||||
|
||||
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!(
|
||||
"download agent for {} exited without completing ({}/{})",
|
||||
self.id.clone(),
|
||||
completed_lock_len,
|
||||
contexts.len(),
|
||||
);
|
||||
info!("da for {} exited without completing", self.id.clone());
|
||||
self.stored_manifest
|
||||
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
|
||||
.set_completed_contexts(&self.completed_contexts.lock().unwrap().clone().into());
|
||||
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()))
|
||||
|
|
@ -336,6 +324,26 @@ 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();
|
||||
|
||||
let request = client
|
||||
.get(chunk_url)
|
||||
.header("Authorization", header);
|
||||
request
|
||||
}
|
||||
|
||||
impl Downloadable for GameDownloadAgent {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
||||
|
|
@ -360,6 +368,7 @@ impl Downloadable for GameDownloadAgent {
|
|||
|
||||
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
return;
|
||||
}
|
||||
|
||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) {
|
||||
|
|
@ -373,35 +382,23 @@ impl Downloadable for GameDownloadAgent {
|
|||
set_game_status(app_handle, self.metadata(), |db_handle, meta| {
|
||||
db_handle.applications.transient_statuses.remove(meta);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
on_game_complete(
|
||||
&self.metadata(),
|
||||
self.stored_manifest.base_path.to_string_lossy().to_string(),
|
||||
app_handle,
|
||||
)
|
||||
.unwrap();
|
||||
on_game_complete(&self.metadata(), self.stored_manifest.base_path.to_string_lossy().to_string(), app_handle).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();
|
||||
fn on_incomplete(&self, _app_handle: &tauri::AppHandle) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id: meta.id.clone(),
|
||||
status: (Some(GameDownloadStatus::Remote {}), None),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
|
||||
|
||||
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn status(&self) -> DownloadStatus {
|
||||
self.status.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src-tauri/src/games/downloads/download_commands.rs
Normal file
68
src-tauri/src/games/downloads/download_commands.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, AppState};
|
||||
|
||||
use super::download_agent::GameDownloadAgent;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn download_game(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
install_dir: usize,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let game_download_agent = Arc::new(
|
||||
Box::new(GameDownloadAgent::new(game_id, game_version, install_dir, sender)) as Box<dyn Downloadable + Send + Sync>
|
||||
);
|
||||
state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)
|
||||
.map_err(|_| "An error occurred while communicating with the download manager.".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn pause_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.pause_downloads()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn resume_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.resume_downloads()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn move_game_in_queue(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
old_index: usize,
|
||||
new_index: usize,
|
||||
) {
|
||||
state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.rearrange(old_index, new_index)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||
state.lock().unwrap().download_manager.cancel(meta)
|
||||
}
|
||||
|
||||
/*
|
||||
#[tauri::command]
|
||||
pub fn get_current_write_speed(state: tauri::State<'_, Mutex<AppState>>) {}
|
||||
*/
|
||||
|
||||
/*
|
||||
fn use_download_agent(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
game_id: String,
|
||||
) -> Result<Arc<GameDownloadAgent>, String> {
|
||||
let lock = state.lock().unwrap();
|
||||
let download_agent = lock.download_manager.get(&game_id).ok_or("Invalid game ID")?;
|
||||
Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure
|
||||
}
|
||||
*/
|
||||
|
|
@ -1,23 +1,26 @@
|
|||
use crate::download_manager::download_thread_control_flag::{
|
||||
DownloadThreadControl, DownloadThreadControlFlag,
|
||||
};
|
||||
use crate::auth::generate_authorization_header;
|
||||
use crate::db::DatabaseImpls;
|
||||
use crate::download_manager::application_download_error::ApplicationDownloadError;
|
||||
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
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::warn;
|
||||
use crate::remote::{DropServerError, RemoteAccessError};
|
||||
use crate::DB;
|
||||
use log::{error, info, warn};
|
||||
use md5::{Context, Digest};
|
||||
use reqwest::blocking::{RequestBuilder, Response};
|
||||
use reqwest::blocking::{Client, Request, RequestBuilder, Response};
|
||||
|
||||
use std::fs::{set_permissions, Permissions};
|
||||
use std::io::{ErrorKind, Read};
|
||||
use std::io::Read;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
use urlencoding::encode;
|
||||
|
||||
pub struct DropWriter<W: Write> {
|
||||
hasher: Context,
|
||||
|
|
@ -39,17 +42,19 @@ 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -119,7 +124,7 @@ pub fn download_game_chunk(
|
|||
ctx: &DropDownloadContext,
|
||||
control_flag: &DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
request: RequestBuilder,
|
||||
request: RequestBuilder
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
// If we're paused
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
|
|
@ -132,9 +137,9 @@ pub fn download_game_chunk(
|
|||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
warn!("{}", response.text().unwrap());
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(err),
|
||||
RemoteAccessError::InvalidCodeError(400),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -148,9 +153,9 @@ pub fn download_game_chunk(
|
|||
|
||||
let content_length = response.content_length();
|
||||
if content_length.is_none() {
|
||||
warn!("recieved 0 length content from server");
|
||||
error!("Recieved 0 length content from server");
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(response.json().unwrap()),
|
||||
RemoteAccessError::InvalidResponse(response.json::<DropServerError>().unwrap()),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -176,14 +181,16 @@ pub fn download_game_chunk(
|
|||
set_permissions(ctx.path.clone(), permissions).unwrap();
|
||||
}
|
||||
|
||||
/*
|
||||
let checksum = pipeline
|
||||
.finish()
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
.map_err(|e| GameDownloadError::IoError(e))?;
|
||||
|
||||
let res = hex::encode(checksum.0);
|
||||
if res != ctx.checksum {
|
||||
return Err(ApplicationDownloadError::Checksum);
|
||||
return Err(GameDownloadError::Checksum);
|
||||
}
|
||||
*/
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
pub mod commands;
|
||||
pub mod download_agent;
|
||||
pub mod download_commands;
|
||||
mod download_logic;
|
||||
mod manifest;
|
||||
mod stored_manifest;
|
||||
mod stored_manifest;
|
||||
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
sync::Mutex,
|
||||
};
|
||||
|
||||
use log::{error, warn};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_binary::binary_stream::Endian;
|
||||
|
||||
|
|
@ -43,10 +43,12 @@ impl StoredManifest {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
warn!("{}", e);
|
||||
error!("{}", e);
|
||||
StoredManifest::new(game_id, game_version, base_path)
|
||||
}
|
||||
}
|
||||
|
|
@ -70,8 +72,8 @@ impl StoredManifest {
|
|||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
pub fn set_completed_contexts(&self, completed_contexts: &[usize]) {
|
||||
*self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
|
||||
pub fn set_completed_contexts(&self, completed_contexts: &Vec<usize>) {
|
||||
*self.completed_contexts.lock().unwrap() = completed_contexts.clone();
|
||||
}
|
||||
pub fn get_completed_contexts(&self) -> Vec<usize> {
|
||||
self.completed_contexts.lock().unwrap().clone()
|
||||
|
|
|
|||
|
|
@ -2,20 +2,20 @@ use std::fs::remove_dir_all;
|
|||
use std::sync::Mutex;
|
||||
use std::thread::spawn;
|
||||
|
||||
use log::{debug, error, warn};
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Emitter;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use urlencoding::encode;
|
||||
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
|
||||
use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
|
||||
use crate::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
|
||||
use crate::db::GameVersion;
|
||||
use crate::download_manager::download_manager::DownloadStatus;
|
||||
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::process::process_manager::Platform;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||
use crate::remote::auth::generate_authorization_header;
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::AppState;
|
||||
use crate::{auth::generate_authorization_header, AppState, DB};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct FetchGameStruct {
|
||||
|
|
@ -40,10 +40,7 @@ pub struct Game {
|
|||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct GameUpdateEvent {
|
||||
pub game_id: String,
|
||||
pub status: (
|
||||
Option<GameDownloadStatus>,
|
||||
Option<ApplicationTransientStatus>,
|
||||
),
|
||||
pub status: (Option<GameDownloadStatus>, Option<ApplicationTransientStatus>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
|
|
@ -51,8 +48,6 @@ pub struct QueueUpdateEventQueueData {
|
|||
pub meta: DownloadableMetadata,
|
||||
pub status: DownloadStatus,
|
||||
pub progress: f64,
|
||||
pub current: usize,
|
||||
pub max: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
|
|
@ -66,27 +61,42 @@ pub struct StatsUpdateEvent {
|
|||
pub time: usize,
|
||||
}
|
||||
|
||||
pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
// 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,
|
||||
}
|
||||
|
||||
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 = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})?
|
||||
.send()?;
|
||||
let response = client
|
||||
.get(library_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
warn!("{:?}", err);
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
return Err(response.status().as_u16().into());
|
||||
}
|
||||
|
||||
let games: Vec<Game> = response.json()?;
|
||||
let games: Vec<Game> = response.json::<Vec<Game>>()?;
|
||||
|
||||
let state = app.state::<Mutex<AppState>>();
|
||||
let mut handle = state.lock().unwrap();
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
|
||||
for game in games.iter() {
|
||||
handle.games.insert(game.id.clone(), game.clone());
|
||||
|
|
@ -103,7 +113,12 @@ pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessErro
|
|||
Ok(games)
|
||||
}
|
||||
|
||||
pub fn fetch_game_logic(
|
||||
#[tauri::command]
|
||||
pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, String> {
|
||||
fetch_library_logic(app).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn fetch_game_logic(
|
||||
id: String,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
|
|
@ -121,25 +136,31 @@ 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 = make_request(&client, &["/api/v1/game/", &id], &[], |r| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
})?
|
||||
.send()?;
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
|
||||
if response.status() == 404 {
|
||||
return Err(RemoteAccessError::GameNotFound);
|
||||
}
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
warn!("{:?}", err);
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
return Err(RemoteAccessError::InvalidCodeError(
|
||||
response.status().into(),
|
||||
));
|
||||
}
|
||||
|
||||
let game: Game = response.json()?;
|
||||
let game = response.json::<Game>()?;
|
||||
state_handle.games.insert(id.clone(), game.clone());
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
|
||||
db_handle
|
||||
.applications
|
||||
|
|
@ -158,62 +179,95 @@ pub fn fetch_game_logic(
|
|||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn fetch_game_verion_options_logic(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
#[tauri::command]
|
||||
pub fn fetch_game(game_id: String, app: tauri::AppHandle) -> Result<FetchGameStruct, String> {
|
||||
let result = fetch_game_logic(game_id, app);
|
||||
|
||||
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();
|
||||
warn!("{:?}", err);
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
if result.is_err() {
|
||||
return Err(result.err().unwrap().to_string());
|
||||
}
|
||||
|
||||
let data: Vec<GameVersion> = response.json()?;
|
||||
Ok(result.unwrap())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_status(id: String) -> Result<GameStatusWithTransient, String> {
|
||||
let status = GameStatusManager::fetch_state(&id);
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn fetch_game_verion_options_logic<'a>(
|
||||
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/metadata/versions?id={}", game_id).as_str())?;
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(RemoteAccessError::InvalidCodeError(
|
||||
response.status().into(),
|
||||
));
|
||||
}
|
||||
|
||||
let data = response.json::<Vec<GameVersionOption>>()?;
|
||||
|
||||
let state_lock = state.lock().unwrap();
|
||||
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
let data: Vec<GameVersion> = data
|
||||
let data = data
|
||||
.into_iter()
|
||||
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
|
||||
.collect();
|
||||
.collect::<Vec<GameVersionOption>>();
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||
println!("triggered uninstall for agent");
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
#[tauri::command]
|
||||
pub fn uninstall_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
app_handle: AppHandle
|
||||
) -> Result<(), String> {
|
||||
let meta = get_current_meta(&game_id)?;
|
||||
println!("{:?}", meta);
|
||||
uninstall_game_logic(meta, &app_handle);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||
println!("Triggered uninstall for agent");
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.entry(meta.clone())
|
||||
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
||||
|
||||
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&meta.id,
|
||||
&meta,
|
||||
(None, Some(ApplicationTransientStatus::Uninstalling {})),
|
||||
);
|
||||
|
||||
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||
if previous_state.is_none() {
|
||||
warn!("uninstall job doesn't have previous state, failing silently");
|
||||
info!("uninstall job doesn't have previous state, failing silently");
|
||||
return;
|
||||
}
|
||||
let previous_state = previous_state.unwrap();
|
||||
if let Some((_, install_dir)) = match previous_state {
|
||||
if let Some((version_name, install_dir)) = match previous_state {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
|
|
@ -237,7 +291,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||
error!("{}", e);
|
||||
}
|
||||
Ok(_) => {
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
|
|
@ -245,26 +299,29 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
save_db();
|
||||
DB.save().unwrap();
|
||||
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
info!("uninstalled game id {}", &meta.id);
|
||||
|
||||
push_game_update(
|
||||
&app_handle,
|
||||
&meta.id,
|
||||
(Some(GameDownloadStatus::Remote {}), None),
|
||||
);
|
||||
push_game_update(&app_handle, &meta, (Some(GameDownloadStatus::Remote {}), None));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
borrow_db_checked()
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(game_id)
|
||||
.cloned()
|
||||
pub fn get_current_meta(game_id: &String) -> Result<DownloadableMetadata, String> {
|
||||
match DB.borrow_data().unwrap().applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => Ok(meta.clone()),
|
||||
None => Err(String::from("Could not find installed version")),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_verion_options<'a>(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<GameVersionOption>, String> {
|
||||
fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn on_game_complete(
|
||||
|
|
@ -273,27 +330,28 @@ pub fn on_game_complete(
|
|||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
// Fetch game version information from remote
|
||||
if meta.version.is_none() {
|
||||
return Err(RemoteAccessError::GameNotFound);
|
||||
}
|
||||
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/metadata/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 = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/metadata/version"],
|
||||
&[
|
||||
("id", &meta.id),
|
||||
("version", meta.version.as_ref().unwrap()),
|
||||
],
|
||||
|f| f.header("Authorization", header),
|
||||
)?
|
||||
.send()?;
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
|
||||
let data: GameVersion = response.json()?;
|
||||
let data = response.json::<GameVersion>()?;
|
||||
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
|
|
@ -306,7 +364,7 @@ pub fn on_game_complete(
|
|||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
drop(handle);
|
||||
save_db();
|
||||
DB.save().unwrap();
|
||||
|
||||
let status = if data.setup_command.is_empty() {
|
||||
GameDownloadStatus::Installed {
|
||||
|
|
@ -320,13 +378,13 @@ pub fn on_game_complete(
|
|||
}
|
||||
};
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(db_handle);
|
||||
save_db();
|
||||
DB.save().unwrap();
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
|
|
@ -340,14 +398,14 @@ pub fn on_game_complete(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) {
|
||||
pub fn push_game_update(app_handle: &AppHandle, meta: &DownloadableMetadata, status: GameStatusWithTransient) {
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", game_id),
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id: game_id.clone(),
|
||||
game_id: meta.id.clone(),
|
||||
status,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
pub mod commands;
|
||||
pub mod downloads;
|
||||
pub mod library;
|
||||
pub mod state;
|
||||
pub mod state;
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus};
|
||||
use crate::{
|
||||
db::{ApplicationTransientStatus, GameDownloadStatus}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, fetch_state, DB
|
||||
};
|
||||
|
||||
pub type GameStatusWithTransient = (
|
||||
Option<GameDownloadStatus>,
|
||||
Option<ApplicationTransientStatus>,
|
||||
);
|
||||
pub type GameStatusWithTransient = (Option<GameDownloadStatus>, Option<ApplicationTransientStatus>);
|
||||
pub struct GameStatusManager {}
|
||||
|
||||
impl GameStatusManager {
|
||||
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
||||
let db_lock = borrow_db_checked();
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
|
||||
Some(meta) => db_lock
|
||||
.applications
|
||||
.transient_statuses
|
||||
.get(meta)
|
||||
.cloned(),
|
||||
None => None,
|
||||
};
|
||||
let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
|
||||
|
|
@ -26,4 +29,4 @@ impl GameStatusManager {
|
|||
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +1,50 @@
|
|||
mod database;
|
||||
mod auth;
|
||||
mod db;
|
||||
mod games;
|
||||
|
||||
mod autostart;
|
||||
mod cleanup;
|
||||
mod commands;
|
||||
mod download_manager;
|
||||
mod error;
|
||||
mod debug;
|
||||
mod process;
|
||||
mod remote;
|
||||
mod tools;
|
||||
pub mod download_manager;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::database::db::DatabaseImpls;
|
||||
use autostart::{get_autostart_enabled, toggle_autostart};
|
||||
use crate::autostart::{get_autostart_enabled, toggle_autostart};
|
||||
use crate::db::DatabaseImpls;
|
||||
use auth::{
|
||||
auth_initiate, generate_authorization_header, manual_recieve_handshake, recieve_handshake,
|
||||
retry_connect, sign_out,
|
||||
};
|
||||
use cleanup::{cleanup_and_exit, quit};
|
||||
use commands::fetch_state;
|
||||
use database::commands::{
|
||||
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 download_manager::commands::{
|
||||
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
||||
use db::{
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameDownloadStatus,
|
||||
DATA_ROOT_DIR,
|
||||
};
|
||||
use debug::fetch_system_data;
|
||||
use download_manager::download_manager::DownloadManager;
|
||||
use download_manager::download_manager_builder::DownloadManagerBuilder;
|
||||
use games::commands::{
|
||||
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
|
||||
};
|
||||
use games::downloads::commands::download_game;
|
||||
use games::library::Game;
|
||||
use games::downloads::download_commands::{cancel_game, download_game, move_game_in_queue, pause_game_downloads, resume_game_downloads};
|
||||
use http::Response;
|
||||
use http::{header::*, response::Builder as ResponseBuilder};
|
||||
use games::library::{
|
||||
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game
|
||||
};
|
||||
use log::{debug, info, warn, LevelFilter};
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use log4rs::Config;
|
||||
use process::commands::{kill_game, launch_game};
|
||||
use process::compat::CompatibilityManager;
|
||||
use process::process_commands::{kill_game, launch_game};
|
||||
use process::process_manager::ProcessManager;
|
||||
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 remote::{gen_drop_url, use_remote};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
|
@ -56,9 +52,8 @@ use std::{
|
|||
};
|
||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
||||
use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
pub enum AppStatus {
|
||||
|
|
@ -91,25 +86,29 @@ pub struct AppState<'a> {
|
|||
download_manager: Arc<DownloadManager>,
|
||||
#[serde(skip_serializing)]
|
||||
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
||||
#[serde(skip_serializing)]
|
||||
compat_manager: Arc<Mutex<CompatibilityManager>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn fetch_state(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<String, String> {
|
||||
let guard = state.lock().unwrap();
|
||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||
drop(guard);
|
||||
Ok(cloned_state)
|
||||
}
|
||||
|
||||
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
let logfile = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(
|
||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||
)))
|
||||
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {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}:{L} - {m}{n}",
|
||||
)))
|
||||
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {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)),
|
||||
|
|
@ -118,7 +117,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
.build(
|
||||
Root::builder()
|
||||
.appenders(vec!["logfile", "console"])
|
||||
.build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
|
||||
.build(LevelFilter::Info),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -127,8 +126,9 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
let games = HashMap::new();
|
||||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
||||
let compat_manager = Arc::new(Mutex::new(CompatibilityManager::new()));
|
||||
|
||||
debug!("checking if database is set up");
|
||||
debug!("Checking if database is set up");
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
if !is_set_up {
|
||||
return AppState {
|
||||
|
|
@ -137,22 +137,22 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_manager,
|
||||
};
|
||||
}
|
||||
|
||||
debug!("database is set up");
|
||||
debug!("Database is set up");
|
||||
|
||||
// TODO: Account for possible failure
|
||||
let (app_status, user) = auth::setup();
|
||||
let (app_status, user) = auth::setup().unwrap();
|
||||
|
||||
let db_handle = borrow_db_checked();
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let mut missing_games = Vec::new();
|
||||
let statuses = db_handle.applications.game_statuses.clone();
|
||||
drop(db_handle);
|
||||
for (game_id, status) in statuses.into_iter() {
|
||||
match status {
|
||||
database::db::GameDownloadStatus::Remote {} => {}
|
||||
database::db::GameDownloadStatus::SetupRequired {
|
||||
db::GameDownloadStatus::Remote {} => {}
|
||||
db::GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
} => {
|
||||
|
|
@ -161,7 +161,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
missing_games.push(game_id);
|
||||
}
|
||||
}
|
||||
database::db::GameDownloadStatus::Installed {
|
||||
db::GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
} => {
|
||||
|
|
@ -175,7 +175,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
|
||||
info!("detected games missing: {:?}", missing_games);
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
for game_id in missing_games {
|
||||
db_handle
|
||||
.applications
|
||||
|
|
@ -186,11 +186,12 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
|
||||
drop(db_handle);
|
||||
|
||||
debug!("finished setup!");
|
||||
|
||||
info!("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 {
|
||||
|
|
@ -199,6 +200,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_manager,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,9 +227,6 @@ pub fn run() {
|
|||
fetch_state,
|
||||
quit,
|
||||
fetch_system_data,
|
||||
// User utils
|
||||
update_settings,
|
||||
fetch_settings,
|
||||
// Auth
|
||||
auth_initiate,
|
||||
retry_connect,
|
||||
|
|
@ -246,9 +245,9 @@ pub fn run() {
|
|||
fetch_game_verion_options,
|
||||
// Downloads
|
||||
download_game,
|
||||
move_download_in_queue,
|
||||
pause_downloads,
|
||||
resume_downloads,
|
||||
move_game_in_queue,
|
||||
pause_game_downloads,
|
||||
resume_game_downloads,
|
||||
cancel_game,
|
||||
uninstall_game,
|
||||
// Processes
|
||||
|
|
@ -266,14 +265,14 @@ pub fn run() {
|
|||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
let state = setup(handle);
|
||||
debug!("initialized drop client");
|
||||
info!("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()?;
|
||||
debug!("registered all pre-defined deep links");
|
||||
info!("registered all pre-defined deep links");
|
||||
}
|
||||
|
||||
let handle = app.handle().clone();
|
||||
|
|
@ -293,7 +292,7 @@ pub fn run() {
|
|||
.unwrap();
|
||||
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
debug!("handling drop:// url");
|
||||
info!("handling drop:// url");
|
||||
let binding = event.urls();
|
||||
let url = binding.first().unwrap();
|
||||
if url.host_str().unwrap() == "handshake" {
|
||||
|
|
@ -323,50 +322,48 @@ pub fn run() {
|
|||
app.webview_windows().get("main").unwrap().show().unwrap();
|
||||
}
|
||||
"quit" => {
|
||||
cleanup_and_exit(app, &app.state());
|
||||
cleanup_and_exit(app);
|
||||
}
|
||||
|
||||
_ => {
|
||||
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 = borrow_db_mut_checked();
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
if let Some(original) = db_handle.prev_database.take() {
|
||||
warn!(
|
||||
"Database corrupted. Original file at {}",
|
||||
original
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
);
|
||||
warn!("Database corrupted. Original file at {}", original.canonicalize().unwrap().to_string_lossy().to_string());
|
||||
app.dialog()
|
||||
.message(
|
||||
"Database corrupted. A copy has been saved at: ".to_string()
|
||||
+ original.to_str().unwrap(),
|
||||
)
|
||||
.message("Database corrupted. A copy has been saved at: ".to_string() + original.to_str().unwrap())
|
||||
.title("Database corrupted")
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})
|
||||
.unwrap()
|
||||
.send();
|
||||
let response = client
|
||||
.get(object_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send();
|
||||
if response.is_err() {
|
||||
warn!(
|
||||
"failed to fetch object with error: {}",
|
||||
|
|
@ -395,7 +392,7 @@ pub fn run() {
|
|||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|_app_handle, event| {
|
||||
app.run(|app_handle, event| {
|
||||
if let RunEvent::ExitRequested { code, api, .. } = event {
|
||||
if code.is_none() {
|
||||
api.prevent_exit();
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use crate::{error::process_error::ProcessError, AppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn launch_game(
|
||||
id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), ProcessError> {
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
|
||||
//let meta = DownloadableMetadata {
|
||||
// id,
|
||||
// version: Some(version),
|
||||
// download_type: DownloadType::Game,
|
||||
//};
|
||||
|
||||
match process_manager_lock.launch_process(id) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn kill_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> 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)
|
||||
}
|
||||
|
|
@ -1,13 +1,51 @@
|
|||
// Since this code isn't being used, we can either:
|
||||
// 1. Delete the entire file if compatibility features are not planned
|
||||
// 2. Or add a TODO comment if planning to implement later
|
||||
use std::{
|
||||
fs::create_dir_all,
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
// Option 1: Delete the file
|
||||
// Delete src-tauri/src/process/compat.rs
|
||||
use crate::db::DATA_ROOT_DIR;
|
||||
|
||||
pub struct CompatibilityManager {
|
||||
compat_tools_path: PathBuf,
|
||||
prefixes_path: PathBuf,
|
||||
created_paths: AtomicBool,
|
||||
}
|
||||
|
||||
// Option 2: Add TODO comment
|
||||
/*
|
||||
TODO: Compatibility layer for running Windows games on Linux
|
||||
This module is currently unused but reserved for future implementation
|
||||
of Windows game compatibility features on Linux.
|
||||
This gets built into both the Windows & Linux client, but
|
||||
we only need it in the Linux client. Therefore, it should
|
||||
do nothing but take a little bit of memory if we're on
|
||||
Windows.
|
||||
*/
|
||||
impl CompatibilityManager {
|
||||
pub fn new() -> Self {
|
||||
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
|
||||
let compat_tools_path = root_dir_lock.join("compatibility_tools");
|
||||
let prefixes_path = root_dir_lock.join("prefixes");
|
||||
drop(root_dir_lock);
|
||||
|
||||
Self {
|
||||
compat_tools_path,
|
||||
prefixes_path,
|
||||
created_paths: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_paths_exist(&self) -> Result<(), String> {
|
||||
if self.created_paths.fetch_and(true, Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
if !self.compat_tools_path.exists() {
|
||||
create_dir_all(self.compat_tools_path.clone()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
if !self.prefixes_path.exists() {
|
||||
create_dir_all(self.prefixes_path.clone()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
self.created_paths.store(true, Ordering::Relaxed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
pub mod commands;
|
||||
pub mod compat;
|
||||
pub mod process_commands;
|
||||
pub mod process_manager;
|
||||
|
|
|
|||
40
src-tauri/src/process/process_commands.rs
Normal file
40
src-tauri/src/process/process_commands.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use crate::{db::GameDownloadStatus, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, games::library::get_current_meta, AppState, DB};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn launch_game(
|
||||
id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
|
||||
let version = match DB.borrow_data().unwrap().applications.game_statuses.get(&id).cloned() {
|
||||
Some(GameDownloadStatus::Installed { version_name, install_dir }) => version_name,
|
||||
Some(GameDownloadStatus::SetupRequired { version_name, install_dir }) => return Err(String::from("Game setup still required")),
|
||||
_ => return Err(String::from("Game not installed"))
|
||||
};
|
||||
|
||||
|
||||
let meta = DownloadableMetadata { id, version: Some(version), download_type: DownloadType::Game };
|
||||
|
||||
|
||||
process_manager_lock.launch_process(meta)?;
|
||||
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn kill_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let meta = get_current_meta(&game_id)?;
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
process_manager_lock.kill_game(meta).map_err(|x| x.to_string())
|
||||
}
|
||||
|
|
@ -1,33 +1,27 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, Error},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, Command, ExitStatus},
|
||||
sync::{Arc, Mutex},
|
||||
thread::spawn,
|
||||
};
|
||||
|
||||
use log::{debug, info, warn};
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_child::SharedChild;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
|
||||
|
||||
use crate::{
|
||||
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},
|
||||
AppState, DB,
|
||||
db::{GameDownloadStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, games::library::push_game_update, games::state::GameStatusManager, AppState, DB
|
||||
};
|
||||
|
||||
pub struct ProcessManager<'a> {
|
||||
current_platform: Platform,
|
||||
log_output_dir: PathBuf,
|
||||
processes: HashMap<String, Arc<SharedChild>>,
|
||||
processes: HashMap<DownloadableMetadata, Arc<SharedChild>>,
|
||||
app_handle: AppHandle,
|
||||
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
|
||||
}
|
||||
|
|
@ -66,8 +60,13 @@ impl ProcessManager<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn process_command(&self, install_dir: &String, command: Vec<String>) -> (PathBuf, Vec<String>) {
|
||||
let root = &command[0];
|
||||
// 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 command_components = raw_command.split(" ").collect::<Vec<&str>>();
|
||||
let root = raw_command;
|
||||
|
||||
let install_dir = Path::new(install_dir);
|
||||
let absolute_exe = install_dir.join(root);
|
||||
|
|
@ -80,40 +79,34 @@ impl ProcessManager<'_> {
|
|||
*/
|
||||
(absolute_exe, Vec::new())
|
||||
}
|
||||
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
|
||||
match self.processes.get(&game_id) {
|
||||
pub fn kill_game(&mut self, meta: DownloadableMetadata) -> Result<(), io::Error> {
|
||||
return match self.processes.get(&meta) {
|
||||
Some(child) => {
|
||||
child.kill()?;
|
||||
child.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"Game ID not running",
|
||||
)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
|
||||
if !self.processes.contains_key(&game_id) {
|
||||
fn on_process_finish(&mut self, meta: DownloadableMetadata, result: Result<ExitStatus, std::io::Error>) {
|
||||
if !self.processes.contains_key(&meta) {
|
||||
warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result);
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("process for {:?} exited with {:?}", &game_id, result);
|
||||
info!("process for {:?} exited with {:?}", meta, result);
|
||||
|
||||
self.processes.remove(&game_id);
|
||||
self.processes.remove(&meta);
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let meta = db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(&game_id)
|
||||
.cloned()
|
||||
.unwrap();
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
|
||||
let current_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||
if let Some(saved_state) = current_state {
|
||||
if let GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
|
|
@ -123,7 +116,7 @@ impl ProcessManager<'_> {
|
|||
if let Ok(exit_code) = result {
|
||||
if exit_code.success() {
|
||||
db_handle.applications.game_statuses.insert(
|
||||
game_id.clone(),
|
||||
meta.id.clone(),
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: version_name.to_string(),
|
||||
install_dir: install_dir.to_string(),
|
||||
|
|
@ -135,9 +128,9 @@ impl ProcessManager<'_> {
|
|||
}
|
||||
drop(db_handle);
|
||||
|
||||
let status = GameStatusManager::fetch_state(&game_id);
|
||||
let status = GameStatusManager::fetch_state(&meta.id);
|
||||
|
||||
push_game_update(&self.app_handle, &game_id, status);
|
||||
push_game_update(&self.app_handle, &meta, status);
|
||||
|
||||
// TODO better management
|
||||
}
|
||||
|
|
@ -149,86 +142,59 @@ impl ProcessManager<'_> {
|
|||
.contains_key(&(current.clone(), platform.clone())))
|
||||
}
|
||||
|
||||
pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
|
||||
if self.processes.contains_key(&game_id) {
|
||||
return Err(ProcessError::AlreadyRunning);
|
||||
pub fn launch_process(&mut self, meta: DownloadableMetadata) -> Result<(), String> {
|
||||
if self.processes.contains_key(&meta) {
|
||||
return Err("Game or setup is already running.".to_owned());
|
||||
}
|
||||
|
||||
let version = match DB
|
||||
.borrow_data()
|
||||
.unwrap()
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&game_id)
|
||||
.cloned()
|
||||
{
|
||||
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
|
||||
Some(GameDownloadStatus::SetupRequired { .. }) => {
|
||||
return Err(ProcessError::SetupRequired)
|
||||
}
|
||||
_ => return Err(ProcessError::NotInstalled),
|
||||
};
|
||||
let meta = DownloadableMetadata {
|
||||
id: game_id.clone(),
|
||||
version: Some(version.clone()),
|
||||
download_type: DownloadType::Game,
|
||||
};
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
debug!(
|
||||
"Launching process {:?} with games {:?}",
|
||||
&game_id, db_lock.applications.game_versions
|
||||
);
|
||||
let mut db_lock = DB.borrow_data_mut().unwrap();
|
||||
info!("Launching process {:?} with games {:?}", meta, db_lock.applications.game_versions);
|
||||
|
||||
let game_status = db_lock
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&game_id)
|
||||
.ok_or(ProcessError::NotInstalled)?;
|
||||
.get(&meta.id)
|
||||
.ok_or("Game not installed")?;
|
||||
|
||||
let (version_name, install_dir) = match game_status {
|
||||
let status_metadata: Option<(&String, &String)> = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
} => Some((version_name, install_dir)),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
_ => return Err(ProcessError::NotDownloaded),
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if status_metadata.is_none() {
|
||||
return Err("Game has not been downloaded.".to_owned());
|
||||
}
|
||||
|
||||
let (version_name, install_dir) = status_metadata.unwrap();
|
||||
|
||||
let game_version = db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&game_id)
|
||||
.ok_or(ProcessError::InvalidID)?
|
||||
.get(&meta.id)
|
||||
.ok_or("Invalid game ID".to_owned())?
|
||||
.get(version_name)
|
||||
.ok_or(ProcessError::InvalidVersion)?;
|
||||
.ok_or("Invalid version name".to_owned())?;
|
||||
|
||||
let mut command: Vec<String> = Vec::new();
|
||||
|
||||
match game_status {
|
||||
let raw_command: String = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => {
|
||||
command.extend([game_version.launch_command.clone()]);
|
||||
command.extend(game_version.launch_args.clone());
|
||||
},
|
||||
} => game_version.launch_command.clone(),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => {
|
||||
command.extend([game_version.setup_command.clone()]);
|
||||
command.extend(game_version.setup_args.clone());
|
||||
},
|
||||
} => game_version.setup_command.clone(),
|
||||
_ => panic!("unreachable code"),
|
||||
};
|
||||
info!("Command: {:?}", &command);
|
||||
|
||||
let (command, args) = self.process_command(install_dir, command);
|
||||
let (command, args) = self.process_command(install_dir, raw_command);
|
||||
|
||||
let target_current_dir = command.parent().unwrap().to_str().unwrap();
|
||||
|
||||
|
|
@ -244,13 +210,11 @@ impl ProcessManager<'_> {
|
|||
.truncate(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.open(self.log_output_dir.join(format!(
|
||||
"{}-{}-{}.log",
|
||||
&game_id,
|
||||
&version,
|
||||
current_time.timestamp()
|
||||
)))
|
||||
.map_err(ProcessError::IOError)?;
|
||||
.open(
|
||||
self.log_output_dir
|
||||
.join(format!("{}-{}-{}.log", meta.id.clone(), meta.version.clone().unwrap_or_default(), current_time.timestamp())),
|
||||
)
|
||||
.map_err(|v| v.to_string())?;
|
||||
|
||||
let error_file = OpenOptions::new()
|
||||
.write(true)
|
||||
|
|
@ -259,11 +223,11 @@ impl ProcessManager<'_> {
|
|||
.create(true)
|
||||
.open(self.log_output_dir.join(format!(
|
||||
"{}-{}-{}-error.log",
|
||||
&game_id,
|
||||
&version,
|
||||
meta.id.clone(),
|
||||
meta.version.clone().unwrap_or_default(),
|
||||
current_time.timestamp()
|
||||
)))
|
||||
.map_err(ProcessError::IOError)?;
|
||||
.map_err(|v| v.to_string())?;
|
||||
|
||||
let current_platform = self.current_platform.clone();
|
||||
let target_platform = game_version.platform.clone();
|
||||
|
|
@ -271,21 +235,20 @@ impl ProcessManager<'_> {
|
|||
let game_launcher = self
|
||||
.game_launchers
|
||||
.get(&(current_platform, target_platform))
|
||||
.ok_or(ProcessError::InvalidPlatform)?;
|
||||
.ok_or("Invalid version for this platform.")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let launch_process = game_launcher
|
||||
.launch_process(
|
||||
&meta,
|
||||
command.to_string_lossy().to_string(),
|
||||
game_version,
|
||||
target_current_dir,
|
||||
log_file,
|
||||
error_file,
|
||||
)
|
||||
.map_err(ProcessError::IOError)?;
|
||||
let launch_process = game_launcher.launch_process(
|
||||
&meta,
|
||||
command.to_str().unwrap().to_owned(),
|
||||
args,
|
||||
&target_current_dir.to_string(),
|
||||
log_file,
|
||||
error_file,
|
||||
)?;
|
||||
|
||||
let launch_process_handle =
|
||||
Arc::new(SharedChild::new(launch_process).map_err(ProcessError::IOError)?);
|
||||
Arc::new(SharedChild::new(launch_process).map_err(|e| e.to_string())?);
|
||||
|
||||
db_lock
|
||||
.applications
|
||||
|
|
@ -294,7 +257,7 @@ impl ProcessManager<'_> {
|
|||
|
||||
push_game_update(
|
||||
&self.app_handle,
|
||||
&meta.id,
|
||||
&meta,
|
||||
(None, Some(ApplicationTransientStatus::Running {})),
|
||||
);
|
||||
|
||||
|
|
@ -309,7 +272,7 @@ impl ProcessManager<'_> {
|
|||
let app_state_handle = app_state.lock().unwrap();
|
||||
|
||||
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
|
||||
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
|
||||
process_manager_handle.on_process_finish(wait_thread_game_id, result);
|
||||
|
||||
// As everything goes out of scope, they should get dropped
|
||||
// But just to explicit about it
|
||||
|
|
@ -317,7 +280,10 @@ impl ProcessManager<'_> {
|
|||
drop(app_state_handle);
|
||||
});
|
||||
|
||||
self.processes.insert(meta.id, wait_thread_handle);
|
||||
self.processes.insert(meta, wait_thread_handle);
|
||||
|
||||
info!("finished spawning process");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -332,31 +298,32 @@ pub trait ProcessHandler: Send + 'static {
|
|||
fn launch_process(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, Error>;
|
||||
) -> Result<Child, String>;
|
||||
}
|
||||
|
||||
struct NativeGameLauncher;
|
||||
impl ProcessHandler for NativeGameLauncher {
|
||||
fn launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
meta: &DownloadableMetadata,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, Error> {
|
||||
Command::new(PathBuf::from(launch_command))
|
||||
) -> Result<Child, String> {
|
||||
Command::new(command)
|
||||
.current_dir(current_dir)
|
||||
.stdout(log_file)
|
||||
.stderr(error_file)
|
||||
.args(game_version.launch_args.clone())
|
||||
.args(args)
|
||||
.spawn()
|
||||
.map_err(|v| v.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -365,23 +332,18 @@ struct UMULauncher;
|
|||
impl ProcessHandler for UMULauncher {
|
||||
fn launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
_log_file: File,
|
||||
_error_file: File,
|
||||
) -> Result<Child, Error> {
|
||||
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())
|
||||
meta: &DownloadableMetadata,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, String> {
|
||||
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command)
|
||||
.game_id(String::from("0"))
|
||||
.launch_args(args)
|
||||
.build()
|
||||
.spawn()
|
||||
.map_err(|x| x.to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
156
src-tauri/src/remote.rs
Normal file
156
src-tauri/src/remote.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Formatter},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use http::StatusCode;
|
||||
use log::{info, warn};
|
||||
use reqwest::blocking::Response;
|
||||
use serde::Deserialize;
|
||||
use url::{ParseError, Url};
|
||||
|
||||
use crate::{AppState, AppStatus, DB};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RemoteAccessError {
|
||||
FetchError(Arc<reqwest::Error>),
|
||||
ParsingError(ParseError),
|
||||
InvalidCodeError(u16),
|
||||
InvalidEndpoint,
|
||||
HandshakeFailed(String),
|
||||
GameNotFound,
|
||||
InvalidResponse(DropServerError),
|
||||
InvalidRedirect,
|
||||
ManifestDownloadFailed(StatusCode, String),
|
||||
OutOfSync,
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
impl Display for RemoteAccessError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RemoteAccessError::FetchError(error) => write!(
|
||||
f,
|
||||
"{}: {}",
|
||||
error,
|
||||
error
|
||||
.source()
|
||||
.map(|e| e.to_string())
|
||||
.or_else(|| Some("Unknown error".to_string()))
|
||||
.unwrap()
|
||||
),
|
||||
RemoteAccessError::ParsingError(parse_error) => {
|
||||
write!(f, "{}", parse_error)
|
||||
}
|
||||
RemoteAccessError::InvalidCodeError(error) => write!(f, "Invalid HTTP code {}", error),
|
||||
RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
|
||||
RemoteAccessError::HandshakeFailed(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: {} {}",
|
||||
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::Generic(message) => write!(f, "{}", message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for RemoteAccessError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
RemoteAccessError::FetchError(Arc::new(err))
|
||||
}
|
||||
}
|
||||
impl From<ParseError> for RemoteAccessError {
|
||||
fn from(err: ParseError) -> Self {
|
||||
RemoteAccessError::ParsingError(err)
|
||||
}
|
||||
}
|
||||
impl From<u16> for RemoteAccessError {
|
||||
fn from(err: u16) -> Self {
|
||||
RemoteAccessError::InvalidCodeError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RemoteAccessError {}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DropServerError {
|
||||
pub status_code: usize,
|
||||
pub status_message: String,
|
||||
pub message: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DropHealthcheck {
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
async fn use_remote_logic<'a>(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'a>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
info!("connecting to url {}", url);
|
||||
let base_url = Url::parse(&url)?;
|
||||
|
||||
// Test Drop url
|
||||
let test_endpoint = base_url.join("/api/v1")?;
|
||||
let response = reqwest::get(test_endpoint.to_string()).await?;
|
||||
|
||||
let result = response.json::<DropHealthcheck>().await?;
|
||||
|
||||
if result.app_name != "Drop" {
|
||||
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
|
||||
return Err(RemoteAccessError::InvalidEndpoint);
|
||||
}
|
||||
|
||||
let mut app_state = state.lock().unwrap();
|
||||
app_state.status = AppStatus::SignedOut;
|
||||
drop(app_state);
|
||||
|
||||
let mut db_state = DB.borrow_data_mut().unwrap();
|
||||
db_state.base_url = base_url.to_string();
|
||||
drop(db_state);
|
||||
|
||||
DB.save().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn use_remote<'a>(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'a>>>,
|
||||
) -> Result<(), String> {
|
||||
let result = use_remote_logic(url, state).await;
|
||||
|
||||
if result.is_err() {
|
||||
return Err(result.err().unwrap().to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn gen_drop_url(path: String) -> Result<String, String> {
|
||||
let base_url = {
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
|
||||
if handle.base_url.is_empty() {
|
||||
return Ok("".to_string());
|
||||
};
|
||||
|
||||
Url::parse(&handle.base_url).unwrap()
|
||||
};
|
||||
|
||||
let url = base_url.join(&path).unwrap();
|
||||
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
AppState, AppStatus,
|
||||
};
|
||||
|
||||
use super::{
|
||||
auth::{auth_initiate_logic, recieve_handshake, setup},
|
||||
remote::use_remote_logic,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn use_remote(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
use_remote_logic(url, state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||
let base_url = {
|
||||
let handle = borrow_db_checked();
|
||||
|
||||
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
||||
};
|
||||
|
||||
let url = base_url.join(&path).unwrap();
|
||||
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sign_out(app: AppHandle) {
|
||||
// Clear auth from database
|
||||
{
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = None;
|
||||
drop(handle);
|
||||
save_db();
|
||||
}
|
||||
|
||||
// Update app state
|
||||
{
|
||||
let app_state = app.state::<Mutex<AppState>>();
|
||||
let mut app_state_handle = app_state.lock().unwrap();
|
||||
app_state_handle.status = AppStatus::SignedOut;
|
||||
app_state_handle.user = None;
|
||||
}
|
||||
|
||||
// Emit event for frontend
|
||||
app.emit("auth/signedout", ()).unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn auth_initiate() -> Result<(), RemoteAccessError> {
|
||||
auth_initiate_logic()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
|
||||
recieve_handshake(app, format!("handshake/{}", token));
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
pub mod auth;
|
||||
pub mod commands;
|
||||
pub mod remote;
|
||||
pub mod requests;
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use log::{debug, warn};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::{borrow_db_mut_checked, save_db},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
AppState, AppStatus,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DropHealthcheck {
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
pub fn use_remote_logic(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
debug!("connecting to url {}", url);
|
||||
let base_url = Url::parse(&url)?;
|
||||
|
||||
// Test Drop url
|
||||
let test_endpoint = base_url.join("/api/v1")?;
|
||||
let response = reqwest::blocking::get(test_endpoint.to_string())?;
|
||||
|
||||
let result: DropHealthcheck = response.json()?;
|
||||
|
||||
if result.app_name != "Drop" {
|
||||
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
|
||||
return Err(RemoteAccessError::InvalidEndpoint);
|
||||
}
|
||||
|
||||
let mut app_state = state.lock().unwrap();
|
||||
app_state.status = AppStatus::SignedOut;
|
||||
drop(app_state);
|
||||
|
||||
let mut db_state = borrow_db_mut_checked();
|
||||
db_state.base_url = base_url.to_string();
|
||||
drop(db_state);
|
||||
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
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
src-tauri/src/tests/mod.rs
Normal file
1
src-tauri/src/tests/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
mod progress_tests;
|
||||
29
src-tauri/src/tests/progress_tests.rs
Normal file
29
src-tauri/src/tests/progress_tests.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
use atomic_counter::RelaxedCounter;
|
||||
|
||||
use crate::downloads::progress::ProgressChecker;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_progress_sequentially() {
|
||||
let counter = Arc::new(RelaxedCounter::new(0));
|
||||
let callback = Arc::new(AtomicBool::new(false));
|
||||
let p = ProgressChecker::new(Box::new(test_fn), counter.clone(), callback, 100);
|
||||
p.run_contexts_sequentially((1..100).collect());
|
||||
println!("Progress: {}", p.get_progress_percentage());
|
||||
}
|
||||
#[test]
|
||||
fn test_progress_parallel() {
|
||||
let counter = Arc::new(RelaxedCounter::new(0));
|
||||
let callback = Arc::new(AtomicBool::new(false));
|
||||
let p = ProgressChecker::new(Box::new(test_fn), counter.clone(), callback, 100);
|
||||
p.run_contexts_parallel_background((1..100).collect(), 10);
|
||||
}
|
||||
|
||||
fn test_fn(int: usize, _callback: Arc<AtomicBool>, _counter: Arc<RelaxedCounter>) {
|
||||
println!("{}", int);
|
||||
}
|
||||
|
||||
*/
|
||||
3
src-tauri/src/tools/compatibility_layer.rs
Normal file
3
src-tauri/src/tools/compatibility_layer.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub struct CompatibilityLayer {
|
||||
|
||||
}
|
||||
4
src-tauri/src/tools/mod.rs
Normal file
4
src-tauri/src/tools/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod prefix;
|
||||
mod registry;
|
||||
mod tool;
|
||||
mod compatibility_layer;
|
||||
0
src-tauri/src/tools/prefix.rs
Normal file
0
src-tauri/src/tools/prefix.rs
Normal file
7
src-tauri/src/tools/registry.rs
Normal file
7
src-tauri/src/tools/registry.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::download_manager::downloadable::Downloadable;
|
||||
|
||||
pub struct Registry<T: Downloadable> {
|
||||
tools: HashMap<String, T>
|
||||
}
|
||||
54
src-tauri/src/tools/tool.rs
Normal file
54
src-tauri/src/tools/tool.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::download_manager::{application_download_error::ApplicationDownloadError, download_thread_control_flag::DownloadThreadControl, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject};
|
||||
|
||||
pub struct ToolDownloadAgent {
|
||||
id: String,
|
||||
version: String,
|
||||
location: String,
|
||||
control_flag: DownloadThreadControl,
|
||||
progress: Arc<ProgressObject>,
|
||||
}
|
||||
impl Downloadable for ToolDownloadAgent {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn progress(&self) -> Arc<ProgressObject> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn control_flag(&self) -> DownloadThreadControl {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn status(&self) -> crate::download_manager::download_manager::DownloadStatus {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> DownloadableMetadata {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_initialised(&self, app_handle: &tauri::AppHandle) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: crate::download_manager::application_download_error::ApplicationDownloadError) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||
"productName": "Drop Desktop Client",
|
||||
"version": "0.2.0-beta",
|
||||
"version": "0.1.0",
|
||||
"identifier": "dev.drop.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn dev --port 1432",
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis", "deb", "rpm", "dmg", "appimage"],
|
||||
"targets": ["nsis", "deb", "rpm", "dmg"],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "both"
|
||||
|
|
|
|||
5
types.ts
5
types.ts
|
|
@ -71,9 +71,4 @@ 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