Compare commits

..

2 Commits

Author SHA1 Message Date
96df57ac54 chore: Add "Under Construction" page for community and news
Signed-off-by: quexeky <git@quexeky.dev>
2025-08-19 12:17:41 +10:00
8069616f2b feat: add umu executable finding (#122)
Signed-off-by: quexeky <git@quexeky.dev>
2025-08-19 10:52:24 +10:00
16 changed files with 120 additions and 191 deletions

View File

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

View File

@ -1,25 +0,0 @@
<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

@ -0,0 +1,16 @@
<template>
<div class="mx-auto flex flex-col items-center gap-y-4 max-w-2xl py-32 sm:py-48 lg:py-56">
<div>
<Wordmark />
</div>
<div class="text-center">
<h1 class="text-balance text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-6xl">
Under construction
</h1>
<p class="mt-6 text-lg leading-8 text-zinc-400">
Yes, we know. We're working on it <a class="text-white" target="_blank"
href="https://github.com/Drop-OSS/drop-app/issues/52">here.</a>
</p>
</div>
</div>
</template>

View File

@ -1,25 +0,0 @@
<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>

16
main/pages/news/index.vue Normal file
View File

@ -0,0 +1,16 @@
<template>
<div class="mx-auto flex flex-col items-center gap-y-4 max-w-2xl py-32 sm:py-48 lg:py-56">
<div>
<Wordmark />
</div>
<div class="text-center">
<h1 class="text-balance text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-6xl">
Under construction
</h1>
<p class="mt-6 text-lg leading-8 text-zinc-400">
Yes, we know. We're working on it <a class="text-white" target="_blank"
href="https://github.com/Drop-OSS/drop-app/issues/52">here.</a>
</p>
</div>
</div>
</template>

View File

@ -1,23 +1,7 @@
<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>

14
src-tauri/Cargo.lock generated
View File

@ -1284,7 +1284,7 @@ dependencies = [
[[package]]
name = "drop-app"
version = "0.3.3"
version = "0.3.2"
dependencies = [
"atomic-instant-full",
"bitcode",
@ -3118,13 +3118,13 @@ dependencies = [
[[package]]
name = "native_model"
version = "0.6.4"
source = "git+https://github.com/Drop-OSS/native_model.git#a91b422cbd53116df1f20b2459fb3d8257458bfd"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7050d759e3da6673361dddda4f4a743492279dd2c6484a21fbee0a8278620df0"
dependencies = [
"anyhow",
"bincode",
"doc-comment",
"log",
"native_model_macro",
"rmp-serde",
"serde",
@ -3134,10 +3134,10 @@ dependencies = [
[[package]]
name = "native_model_macro"
version = "0.6.4"
source = "git+https://github.com/Drop-OSS/native_model.git#a91b422cbd53116df1f20b2459fb3d8257458bfd"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1577a0bebf5ed1754e240baf5d9b1845f51e598b20600aa894f55e11cd20cc6c"
dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.101",

View File

@ -1,6 +1,6 @@
[package]
name = "drop-app"
version = "0.3.3"
version = "0.3.2"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
edition = "2024"
@ -65,7 +65,7 @@ whoami = "1.6.0"
filetime = "0.2.25"
walkdir = "2.5.0"
known-folders = "1.2.0"
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6"
reqwest-websocket = "0.5.0"

View File

@ -8,6 +8,7 @@ use std::{
use chrono::Utc;
use log::{debug, error, info, warn};
use native_model::{Decode, Encode};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{Serialize, de::DeserializeOwned};
use url::Url;
@ -27,7 +28,7 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer
{
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::encode(val)
native_model::rmp_serde_1_3::RmpSerde::encode(val)
.map_err(|e| DeSerError::Internal(e.to_string()))
}
@ -35,7 +36,7 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
let mut buf = Vec::new();
s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let (val, _version) = native_model::decode(buf)
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
.map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val)
}

View File

@ -1,5 +1,10 @@
/**
* NEXT BREAKING CHANGE
*
* UPDATE DATABASE TO USE RPMSERDENAMED
*
* WE CAN'T DELETE ANY FIELDS
*/
pub mod data {
use std::path::PathBuf;
@ -7,9 +12,6 @@ pub mod data {
use native_model::native_model;
use serde::{Deserialize, Serialize};
// NOTE: Within each version, you should NEVER use these types.
// Declare it using the actual version that it is from, i.e. v1::Settings rather than just Settings from here
pub type GameVersion = v1::GameVersion;
pub type Database = v3::Database;
pub type Settings = v1::Settings;
@ -20,7 +22,7 @@ pub mod data {
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v2::DatabaseApplications;
// pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::collections::HashMap;
@ -178,15 +180,16 @@ pub mod data {
use serde_with::serde_as;
use super::{
Deserialize, Serialize, native_model, v1,
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
GameVersion, Serialize, Settings, native_model, v1,
};
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::Database)]
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: v1::Settings,
pub auth: Option<v1::DatabaseAuth>,
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: v1::DatabaseApplications,
#[serde(skip)]
@ -195,7 +198,7 @@ pub mod data {
pub compat_info: Option<DatabaseCompatInfo>,
}
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
@ -218,7 +221,7 @@ pub mod data {
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::GameDownloadStatus)]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
@ -258,17 +261,17 @@ pub mod data {
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from=v1::DatabaseApplications)]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, v1::GameVersion>>,
pub installed_game_version: HashMap<String, v1::DownloadableMetadata>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self {
@ -290,21 +293,21 @@ pub mod data {
use std::path::PathBuf;
use super::{
Deserialize, Serialize,
native_model, v2, v1,
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
Settings, native_model, v2,
};
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde, from = v2::Database)]
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: v1::Settings,
pub auth: Option<v1::DatabaseAuth>,
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: v2::DatabaseApplications,
pub applications: DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<v2::DatabaseCompatInfo>,
pub compat_info: Option<DatabaseCompatInfo>,
}
impl From<v2::Database> for Database {
@ -344,6 +347,5 @@ pub mod data {
compat_info: None,
}
}
}
}

View File

@ -23,7 +23,6 @@ pub enum RemoteAccessError {
ManifestDownloadFailed(StatusCode, String),
OutOfSync,
Cache(std::io::Error),
CorruptedState,
}
impl Display for RemoteAccessError {
@ -82,10 +81,6 @@ 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, HashSet};
use std::fs::{create_dir_all, OpenOptions};
use std::collections::HashMap;
use std::fs::{OpenOptions, create_dir_all};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
@ -242,8 +242,12 @@ impl GameDownloadAgent {
let mut buckets = Vec::new();
let mut current_buckets = HashMap::<String, DownloadBucket>::new();
let mut current_bucket_sizes = HashMap::<String, usize>::new();
let mut current_bucket = DownloadBucket {
game_id: game_id.clone(),
version: self.version.clone(),
drops: Vec::new(),
};
let mut current_bucket_size = 0;
for (raw_path, chunk) in manifest {
let path = base_path.join(Path::new(&raw_path));
@ -278,41 +282,28 @@ impl GameDownloadAgent {
buckets.push(DownloadBucket {
game_id: game_id.clone(),
version: chunk.version_name.clone(),
version: self.version.clone(),
drops: vec![drop],
});
continue;
}
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
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.clone());
*current_bucket = DownloadBucket {
buckets.push(current_bucket);
current_bucket = DownloadBucket {
game_id: game_id.clone(),
version: chunk.version_name.clone(),
drops: vec![],
version: self.version.clone(),
drops: Vec::new(),
};
*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")]
@ -321,10 +312,8 @@ impl GameDownloadAgent {
}
}
for (_, bucket) in current_buckets.into_iter() {
if !bucket.drops.is_empty() {
buckets.push(bucket);
}
if !current_bucket.drops.is_empty() {
buckets.push(current_bucket);
}
info!("buckets: {}", buckets.len());
@ -359,28 +348,14 @@ 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();
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(),
version: self.version.clone(),
})
.header("Authorization", generate_authorization_header())
.send()?;
@ -389,16 +364,11 @@ impl GameDownloadAgent {
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() {
@ -430,11 +400,6 @@ 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

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

View File

@ -64,7 +64,7 @@ use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Write;
use std::panic::PanicHookInfo;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;
@ -109,7 +109,7 @@ fn create_new_compat_info() -> Option<CompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = UMU_LAUNCHER_EXECUTABLE.is_some();
let has_umu_installed = *UMU_LAUNCHER_EXECUTABLE == PathBuf::new();
Some(CompatInfo {
umu_installed: has_umu_installed,
})

View File

@ -5,7 +5,7 @@ use std::{
sync::LazyLock,
};
use log::{debug, info};
use log::debug;
use crate::{
AppState,
@ -31,29 +31,29 @@ impl ProcessHandler for NativeGameLauncher {
}
}
pub static UMU_LAUNCHER_EXECUTABLE: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
pub static UMU_LAUNCHER_EXECUTABLE: LazyLock<PathBuf> = LazyLock::new(|| {
let x = get_umu_executable();
info!("{:?}", &x);
println!("{:?}", &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> {
fn get_umu_executable() -> PathBuf {
if check_executable_exists(UMU_BASE_LAUNCHER_EXECUTABLE) {
return Some(PathBuf::from(UMU_BASE_LAUNCHER_EXECUTABLE));
return 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);
return p;
}
}
None
PathBuf::new()
}
fn check_executable_exists<P: AsRef<OsStr>>(exec: P) -> bool {
let has_umu_installed = Command::new(exec).stdout(Stdio::null()).output();
let has_umu_installed = Command::new(exec).stdout(Stdio::null()).spawn();
has_umu_installed.is_ok()
}
pub struct UMULauncher;
@ -79,7 +79,7 @@ impl ProcessHandler for UMULauncher {
};
format!(
"GAMEID={game_id} {umu:?} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE.as_ref().unwrap(),
umu = &*UMU_LAUNCHER_EXECUTABLE,
launch = launch_command,
args = args.join(" ")
)

View File

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