Compare commits

...

5 Commits
v0.3.2 ... main

Author SHA1 Message Date
7e70a17a43 Bump version to v0.3.3 2025-08-28 18:23:12 +10:00
8d61a68b8a Add placeholders to unfinished pages (#126)
* feat: add placeholders for community & news pages

* feat: add placeholder to interface in settings menu
2025-08-28 18:22:33 +10:00
44a1be6991 Fix for multi-version downloads (#125)
* fix: multi version downloads

* fix: remove debug utils

* fix: clippy
2025-08-28 18:05:05 +10:00
4f5fccf0c1 Add umu-run discovery (#122)
Signed-off-by: quexeky <git@quexeky.dev>
2025-08-28 18:05:05 +10:00
5eef2bf60f Fix Tauri builds (#119)
* fix: attempt to use local user keychain

* chore: tmp disable non-macos builds

* fix: windows process fix patch

* fix: re-enable windows

* fix: remove sudo

* fix: tmp disable windows again

* fix: windows build again

* chore: re-disable windows

* fix: pin to macos 14

* fix: re-enable other builds
2025-08-15 23:55:34 +10:00
16 changed files with 195 additions and 64 deletions

View File

@ -69,9 +69,9 @@ jobs:
security set-keychain-settings -t 3600 -u build.keychain
curl https://droposs.org/drop.crt --output drop.pem
sudo security authorizationdb write com.apple.trust-settings.admin allow
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
sudo security authorizationdb remove com.apple.trust-settings.admin
sudo security authorizationdb write com.apple.trust-settings.user allow
security add-trusted-cert -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
sudo security authorizationdb remove com.apple.trust-settings.user
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain

View File

@ -1,7 +1,7 @@
{
"name": "view",
"private": true,
"version": "0.3.2",
"version": "0.3.3",
"type": "module",
"scripts": {
"build": "nuxt generate",

25
main/pages/community.vue Normal file
View File

@ -0,0 +1,25 @@
<template>
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<WrenchScrewdriverIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Under construction
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
This page hasn't been implemented yet.
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
WrenchScrewdriverIcon,
} from "@heroicons/vue/20/solid";
</script>

25
main/pages/news.vue Normal file
View File

@ -0,0 +1,25 @@
<template>
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<WrenchScrewdriverIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Under construction
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
This page hasn't been implemented yet.
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
WrenchScrewdriverIcon,
} from "@heroicons/vue/20/solid";
</script>

View File

@ -1,7 +1,23 @@
<template>
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<WrenchScrewdriverIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Under construction
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
This page hasn't been implemented yet.
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { WrenchScrewdriverIcon } from "@heroicons/vue/20/solid";
</script>

2
src-tauri/Cargo.lock generated
View File

@ -1284,7 +1284,7 @@ dependencies = [
[[package]]
name = "drop-app"
version = "0.3.2"
version = "0.3.3"
dependencies = [
"atomic-instant-full",
"bitcode",

View File

@ -1,6 +1,6 @@
[package]
name = "drop-app"
version = "0.3.2"
version = "0.3.3"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
edition = "2024"

View File

@ -23,6 +23,7 @@ pub enum RemoteAccessError {
ManifestDownloadFailed(StatusCode, String),
OutOfSync,
Cache(std::io::Error),
CorruptedState,
}
impl Display for RemoteAccessError {
@ -81,6 +82,10 @@ impl Display for RemoteAccessError {
"server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"
),
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {error}"),
RemoteAccessError::CorruptedState => write!(
f,
"Drop encountered a corrupted internal state. Please report this to the developers, with details of reproduction."
),
}
}
}

View File

@ -22,8 +22,8 @@ use crate::remote::requests::generate_url;
use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
use log::{debug, error, info, warn};
use rayon::ThreadPoolBuilder;
use std::collections::HashMap;
use std::fs::{OpenOptions, create_dir_all};
use std::collections::{HashMap, HashSet};
use std::fs::{create_dir_all, OpenOptions};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
@ -242,12 +242,8 @@ impl GameDownloadAgent {
let mut buckets = Vec::new();
let mut current_bucket = DownloadBucket {
game_id: game_id.clone(),
version: self.version.clone(),
drops: Vec::new(),
};
let mut current_bucket_size = 0;
let mut current_buckets = HashMap::<String, DownloadBucket>::new();
let mut current_bucket_sizes = HashMap::<String, usize>::new();
for (raw_path, chunk) in manifest {
let path = base_path.join(Path::new(&raw_path));
@ -282,28 +278,41 @@ impl GameDownloadAgent {
buckets.push(DownloadBucket {
game_id: game_id.clone(),
version: self.version.clone(),
version: chunk.version_name.clone(),
drops: vec![drop],
});
continue;
}
if current_bucket_size + *length >= TARGET_BUCKET_SIZE
let current_bucket_size = current_bucket_sizes
.entry(chunk.version_name.clone())
.or_insert_with(|| 0);
let c_version_name = chunk.version_name.clone();
let c_game_id = game_id.clone();
let current_bucket = current_buckets
.entry(chunk.version_name.clone())
.or_insert_with(|| DownloadBucket {
game_id: c_game_id,
version: c_version_name,
drops: vec![],
});
if *current_bucket_size + length >= TARGET_BUCKET_SIZE
&& !current_bucket.drops.is_empty()
{
// Move current bucket into list and make a new one
buckets.push(current_bucket);
current_bucket = DownloadBucket {
buckets.push(current_bucket.clone());
*current_bucket = DownloadBucket {
game_id: game_id.clone(),
version: self.version.clone(),
drops: Vec::new(),
version: chunk.version_name.clone(),
drops: vec![],
};
current_bucket_size = 0;
*current_bucket_size = 0;
}
current_bucket.drops.push(drop);
current_bucket_size += *length;
*current_bucket_size += *length;
}
#[cfg(target_os = "linux")]
@ -312,8 +321,10 @@ impl GameDownloadAgent {
}
}
if !current_bucket.drops.is_empty() {
buckets.push(current_bucket);
for (_, bucket) in current_buckets.into_iter() {
if !bucket.drops.is_empty() {
buckets.push(bucket);
}
}
info!("buckets: {}", buckets.len());
@ -348,27 +359,46 @@ impl GameDownloadAgent {
.build()
.unwrap();
let buckets = self.buckets.lock().unwrap();
let mut download_contexts = HashMap::<String, DownloadContext>::new();
let versions = buckets
.iter()
.map(|e| &e.version)
.collect::<HashSet<_>>()
.into_iter().cloned()
.collect::<Vec<String>>();
info!("downloading across these versions: {versions:?}");
let completed_contexts = Arc::new(boxcar::Vec::new());
let completed_indexes_loop_arc = completed_contexts.clone();
let download_context = DROP_CLIENT_SYNC
.post(generate_url(&["/api/v2/client/context"], &[]).unwrap())
.json(&ManifestBody {
game: self.id.clone(),
version: self.version.clone(),
})
.header("Authorization", generate_authorization_header())
.send()?;
for version in versions {
let download_context = DROP_CLIENT_SYNC
.post(generate_url(&["/api/v2/client/context"], &[]).unwrap())
.json(&ManifestBody {
game: self.id.clone(),
version: version.clone(),
})
.header("Authorization", generate_authorization_header())
.send()?;
if download_context.status() != 200 {
return Err(RemoteAccessError::InvalidResponse(download_context.json()?));
if download_context.status() != 200 {
return Err(RemoteAccessError::InvalidResponse(download_context.json()?));
}
let download_context = download_context.json::<DownloadContext>()?;
info!(
"download context: ({}) {}",
&version, download_context.context
);
download_contexts.insert(version, download_context);
}
let download_context = &download_context.json::<DownloadContext>()?;
let download_contexts = &download_contexts;
info!("download context: {}", download_context.context);
let buckets = self.buckets.lock().unwrap();
pool.scope(|scope| {
let context_map = self.context_map.lock().unwrap();
for (index, bucket) in buckets.iter().enumerate() {
@ -400,6 +430,11 @@ impl GameDownloadAgent {
let sender = self.sender.clone();
let download_context = download_contexts
.get(&bucket.version)
.ok_or(RemoteAccessError::CorruptedState)
.unwrap();
scope.spawn(move |_| {
// 3 attempts
for i in 0..RETRY_COUNT {

View File

@ -172,7 +172,7 @@ pub fn download_game_bucket(
let raw_res = response.text().map_err(|e| {
ApplicationDownloadError::Communication(RemoteAccessError::FetchError(e.into()))
})?;
info!("{}", raw_res);
info!("{raw_res}");
if let Ok(err) = serde_json::from_str::<DropServerError>(&raw_res) {
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(err),
@ -196,8 +196,7 @@ pub fn download_game_bucket(
let length = raw_length.parse::<usize>().unwrap_or(0);
let Some(drop) = bucket.drops.get(i) else {
warn!(
"invalid number of Content-Lengths recieved: {}, {}",
i, lengths
"invalid number of Content-Lengths recieved: {i}, {lengths}"
);
return Err(ApplicationDownloadError::DownloadError);
};

View File

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
// Drops go in buckets
pub struct DownloadDrop {
pub index: usize,
@ -14,7 +14,7 @@ pub struct DownloadDrop {
pub permissions: u32,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct DownloadBucket {
pub game_id: String,
pub version: String,

View File

@ -65,7 +65,6 @@ use std::fs::File;
use std::io::Write;
use std::panic::PanicHookInfo;
use std::path::Path;
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;
@ -110,13 +109,7 @@ fn create_new_compat_info() -> Option<CompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE)
.stdout(Stdio::null())
.spawn();
if let Err(umu_error) = &has_umu_installed {
warn!("disabling windows support with error: {umu_error}");
}
let has_umu_installed = has_umu_installed.is_ok();
let has_umu_installed = UMU_LAUNCHER_EXECUTABLE.is_some();
Some(CompatInfo {
umu_installed: has_umu_installed,
})

View File

@ -1,4 +1,11 @@
use log::debug;
use std::{
ffi::OsStr,
path::PathBuf,
process::{Command, Stdio},
sync::LazyLock,
};
use log::{debug, info};
use crate::{
AppState,
@ -24,7 +31,31 @@ impl ProcessHandler for NativeGameLauncher {
}
}
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
pub static UMU_LAUNCHER_EXECUTABLE: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
let x = get_umu_executable();
info!("{:?}", &x);
x
});
const UMU_BASE_LAUNCHER_EXECUTABLE: &str = "umu-run";
const UMU_INSTALL_DIRS: [&str; 4] = ["/app/share", "/use/local/share", "/usr/share", "/opt"];
fn get_umu_executable() -> Option<PathBuf> {
if check_executable_exists(UMU_BASE_LAUNCHER_EXECUTABLE) {
return Some(PathBuf::from(UMU_BASE_LAUNCHER_EXECUTABLE));
}
for dir in UMU_INSTALL_DIRS {
let p = PathBuf::from(dir).join(UMU_BASE_LAUNCHER_EXECUTABLE);
if check_executable_exists(&p) {
return Some(p);
}
}
None
}
fn check_executable_exists<P: AsRef<OsStr>>(exec: P) -> bool {
let has_umu_installed = Command::new(exec).stdout(Stdio::null()).output();
has_umu_installed.is_ok()
}
pub struct UMULauncher;
impl ProcessHandler for UMULauncher {
fn create_launch_process(
@ -47,8 +78,8 @@ impl ProcessHandler for UMULauncher {
None => game_version.game_id.clone(),
};
format!(
"GAMEID={game_id} {umu} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE,
"GAMEID={game_id} {umu:?} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE.as_ref().unwrap(),
launch = launch_command,
args = args.join(" ")
)
@ -80,7 +111,10 @@ impl ProcessHandler for AsahiMuvmLauncher {
game_version,
current_dir,
);
let mut args_cmd = umu_string.split("umu-run").collect::<Vec<&str>>().into_iter();
let mut args_cmd = umu_string
.split("umu-run")
.collect::<Vec<&str>>()
.into_iter();
let args = args_cmd.next().unwrap().trim();
let cmd = format!("umu-run{}", args_cmd.next().unwrap());

View File

@ -347,11 +347,10 @@ impl ProcessManager<'_> {
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
let mut command = Command::new("start");
let mut command = Command::new("cmd");
#[cfg(target_os = "windows")]
command.raw_arg(format!("/min cmd /C \"{}\"", &launch_string));
command.raw_arg(format!("/C \"{}\"", &launch_string));
info!("launching (in {install_dir}): {launch_string}",);

View File

@ -130,7 +130,7 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
let code = auth_initiate_logic("code".to_string())?;
let header_code = code.clone();
println!("using code: {} to sign in", code);
println!("using code: {code} to sign in");
tauri::async_runtime::spawn(async move {
let load = async || -> Result<(), RemoteAccessError> {

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client",
"version": "0.3.2",
"version": "0.3.3",
"identifier": "dev.drop.client",
"build": {
"beforeDevCommand": "yarn --cwd main dev --port 1432",