Discord and playtime (#462)
* initial * Fixed java thing * fixes * internet check change * some fix/test commit * Fix render issues on windows * bump version --------- Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Jai A <jai@modrinth.com>
This commit is contained in:
parent
5ee64f2705
commit
d968ad383c
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -4609,7 +4609,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
@ -4654,7 +4654,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus_cli"
|
name = "theseus_cli"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argh",
|
"argh",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
@ -4681,7 +4681,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ use std::path::PathBuf;
|
|||||||
use crate::event::emit::{emit_loading, init_loading};
|
use crate::event::emit::{emit_loading, init_loading};
|
||||||
use crate::state::CredentialsStore;
|
use crate::state::CredentialsStore;
|
||||||
use crate::util::fetch::{fetch_advanced, fetch_json};
|
use crate::util::fetch::{fetch_advanced, fetch_json};
|
||||||
use crate::util::io;
|
|
||||||
use crate::util::jre::extract_java_majorminor_version;
|
use crate::util::jre::extract_java_majorminor_version;
|
||||||
use crate::{
|
use crate::{
|
||||||
state::JavaGlobals,
|
state::JavaGlobals,
|
||||||
@ -117,10 +117,6 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
|||||||
|
|
||||||
let path = state.directories.java_versions_dir().await;
|
let path = state.directories.java_versions_dir().await;
|
||||||
|
|
||||||
if path.exists() {
|
|
||||||
io::remove_dir_all(&path).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
|
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::Error::from(crate::ErrorKind::InputError(
|
crate::Error::from(crate::ErrorKind::InputError(
|
||||||
|
|||||||
@ -273,9 +273,7 @@ pub async fn install_zipped_mrpack_files(
|
|||||||
profile::edit_icon(&profile_path, Some(&potential_icon)).await?;
|
profile::edit_icon(&profile_path, Some(&potential_icon)).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(profile_val) =
|
if let Some(profile_val) = profile::get(&profile_path, None).await? {
|
||||||
crate::api::profile::get(&profile_path, None).await?
|
|
||||||
{
|
|
||||||
crate::launcher::install_minecraft(&profile_val, Some(loading_bar))
|
crate::launcher::install_minecraft(&profile_val, Some(loading_bar))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ use crate::pack::install_from::{
|
|||||||
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
|
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
|
||||||
use crate::state::ProjectMetadata;
|
use crate::state::ProjectMetadata;
|
||||||
|
|
||||||
|
use crate::util::fetch;
|
||||||
use crate::util::io::{self, IOError};
|
use crate::util::io::{self, IOError};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{self, refresh},
|
auth::{self, refresh},
|
||||||
@ -22,6 +23,7 @@ pub use crate::{
|
|||||||
};
|
};
|
||||||
use async_zip::tokio::write::ZipFileWriter;
|
use async_zip::tokio::write::ZipFileWriter;
|
||||||
use async_zip::{Compression, ZipEntryBuilder};
|
use async_zip::{Compression, ZipEntryBuilder};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -878,6 +880,65 @@ pub async fn run_credentials(
|
|||||||
Ok(mc_process)
|
Ok(mc_process)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update playtime- sending a request to the server to update the playtime
|
||||||
|
#[tracing::instrument]
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
|
||||||
|
let profile = get(path, None).await?.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::OtherError(format!(
|
||||||
|
"Tried to update playtime for a nonexistent or unloaded profile at path {}!",
|
||||||
|
path
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let updated_recent_playtime = profile.metadata.recent_time_played;
|
||||||
|
|
||||||
|
let res = if updated_recent_playtime > 0 {
|
||||||
|
// Create update struct to send to Labrinth
|
||||||
|
let modrinth_pack_version_id =
|
||||||
|
profile.metadata.linked_data.and_then(|l| l.version_id);
|
||||||
|
let playtime_update_json = json!({
|
||||||
|
"seconds": updated_recent_playtime,
|
||||||
|
"loader": profile.metadata.loader.to_string(),
|
||||||
|
"game_version": profile.metadata.game_version,
|
||||||
|
"parent": modrinth_pack_version_id,
|
||||||
|
});
|
||||||
|
// Copy this struct for every Modrinth project in the profile
|
||||||
|
let mut hashmap: HashMap<String, serde_json::Value> = HashMap::new();
|
||||||
|
for (_, project) in profile.projects {
|
||||||
|
if let ProjectMetadata::Modrinth { version, .. } = project.metadata
|
||||||
|
{
|
||||||
|
hashmap.insert(version.id, playtime_update_json.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
|
fetch::post_json(
|
||||||
|
"https://api.modrinth.com/analytics/playtime",
|
||||||
|
serde_json::to_value(hashmap)?,
|
||||||
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
// If successful, update the profile metadata to match submitted
|
||||||
|
if res.is_ok() {
|
||||||
|
let mut profiles = state.profiles.write().await;
|
||||||
|
if let Some(profile) = profiles.0.get_mut(path) {
|
||||||
|
profile.metadata.submitted_time_played += updated_recent_playtime;
|
||||||
|
profile.metadata.recent_time_played = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sync either way
|
||||||
|
State::sync().await?;
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec<String> {
|
fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec<String> {
|
||||||
packfile
|
packfile
|
||||||
.files
|
.files
|
||||||
|
|||||||
@ -103,6 +103,7 @@ pub async fn install_minecraft(
|
|||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
existing_loading_bar: Option<LoadingBarId>,
|
existing_loading_bar: Option<LoadingBarId>,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
|
let sync_projects = existing_loading_bar.is_some();
|
||||||
let loading_bar = init_or_edit_loading(
|
let loading_bar = init_or_edit_loading(
|
||||||
existing_loading_bar,
|
existing_loading_bar,
|
||||||
LoadingBarType::MinecraftDownload {
|
LoadingBarType::MinecraftDownload {
|
||||||
@ -123,6 +124,10 @@ pub async fn install_minecraft(
|
|||||||
.await?;
|
.await?;
|
||||||
State::sync().await?;
|
State::sync().await?;
|
||||||
|
|
||||||
|
if sync_projects {
|
||||||
|
Profile::sync_projects_task(profile.profile_id(), true);
|
||||||
|
}
|
||||||
|
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let instance_path =
|
let instance_path =
|
||||||
&io::canonicalize(&profile.get_profile_full_path().await?)?;
|
&io::canonicalize(&profile.get_profile_full_path().await?)?;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use super::{Profile, ProfilePathId};
|
use super::{Profile, ProfilePathId};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::ExitStatus;
|
use std::process::ExitStatus;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
@ -12,6 +13,7 @@ use tracing::error;
|
|||||||
|
|
||||||
use crate::event::emit::emit_process;
|
use crate::event::emit::emit_process;
|
||||||
use crate::event::ProcessPayloadType;
|
use crate::event::ProcessPayloadType;
|
||||||
|
use crate::profile;
|
||||||
use crate::util::io::IOError;
|
use crate::util::io::IOError;
|
||||||
|
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
@ -29,6 +31,7 @@ pub struct MinecraftChild {
|
|||||||
pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled
|
pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled
|
||||||
pub current_child: Arc<RwLock<Child>>,
|
pub current_child: Arc<RwLock<Child>>,
|
||||||
pub output: SharedOutput,
|
pub output: SharedOutput,
|
||||||
|
pub last_updated_playtime: DateTime<Utc>, // The last time we updated the playtime for the associated profile
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Children {
|
impl Children {
|
||||||
@ -94,6 +97,7 @@ impl Children {
|
|||||||
post_command,
|
post_command,
|
||||||
pid,
|
pid,
|
||||||
current_child.clone(),
|
current_child.clone(),
|
||||||
|
profile_relative_path.clone(),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
emit_process(
|
emit_process(
|
||||||
@ -104,6 +108,8 @@ impl Children {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let last_updated_playtime = Utc::now();
|
||||||
|
|
||||||
// Create MinecraftChild
|
// Create MinecraftChild
|
||||||
let mchild = MinecraftChild {
|
let mchild = MinecraftChild {
|
||||||
uuid,
|
uuid,
|
||||||
@ -111,6 +117,7 @@ impl Children {
|
|||||||
current_child,
|
current_child,
|
||||||
output: shared_output,
|
output: shared_output,
|
||||||
manager,
|
manager,
|
||||||
|
last_updated_playtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mchild = Arc::new(RwLock::new(mchild));
|
let mchild = Arc::new(RwLock::new(mchild));
|
||||||
@ -128,11 +135,13 @@ impl Children {
|
|||||||
post_command: Option<Command>,
|
post_command: Option<Command>,
|
||||||
mut current_pid: u32,
|
mut current_pid: u32,
|
||||||
current_child: Arc<RwLock<Child>>,
|
current_child: Arc<RwLock<Child>>,
|
||||||
|
associated_profile: ProfilePathId,
|
||||||
) -> crate::Result<ExitStatus> {
|
) -> crate::Result<ExitStatus> {
|
||||||
let current_child = current_child.clone();
|
let current_child = current_child.clone();
|
||||||
|
|
||||||
// Wait on current Minecraft Child
|
// Wait on current Minecraft Child
|
||||||
let mut mc_exit_status;
|
let mut mc_exit_status;
|
||||||
|
let mut last_updated_playtime = Utc::now();
|
||||||
loop {
|
loop {
|
||||||
if let Some(t) = current_child
|
if let Some(t) = current_child
|
||||||
.write()
|
.write()
|
||||||
@ -145,8 +154,61 @@ impl Children {
|
|||||||
}
|
}
|
||||||
// sleep for 10ms
|
// sleep for 10ms
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||||
|
|
||||||
|
// Auto-update playtime every minute
|
||||||
|
let diff = Utc::now()
|
||||||
|
.signed_duration_since(last_updated_playtime)
|
||||||
|
.num_seconds();
|
||||||
|
if diff >= 60 {
|
||||||
|
if let Err(e) =
|
||||||
|
profile::edit(&associated_profile, |mut prof| {
|
||||||
|
prof.metadata.recent_time_played += diff as u64;
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to update playtime for profile {}: {}",
|
||||||
|
associated_profile,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
last_updated_playtime = Utc::now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now fully complete- update playtime one last time
|
||||||
|
let diff = Utc::now()
|
||||||
|
.signed_duration_since(last_updated_playtime)
|
||||||
|
.num_seconds();
|
||||||
|
if let Err(e) = profile::edit(&associated_profile, |mut prof| {
|
||||||
|
prof.metadata.recent_time_played += diff as u64;
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to update playtime for profile {}: {}",
|
||||||
|
associated_profile,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish play time update
|
||||||
|
// Allow failure, it will be stored locally and sent next time
|
||||||
|
// Sent in another thread as first call may take a couple seconds and hold up process ending
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) =
|
||||||
|
profile::try_update_playtime(&associated_profile).await
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to update playtime for profile {}: {}",
|
||||||
|
associated_profile,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
// Clear game played for Discord RPC
|
// Clear game played for Discord RPC
|
||||||
// May have other active processes, so we clear to the next running process
|
// May have other active processes, so we clear to the next running process
|
||||||
|
|||||||
@ -99,6 +99,7 @@ impl DiscordGuard {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
/// Clear the activity
|
/// Clear the activity
|
||||||
pub async fn clear_activity(
|
pub async fn clear_activity(
|
||||||
&self,
|
&self,
|
||||||
@ -137,7 +138,7 @@ impl DiscordGuard {
|
|||||||
res.map_err(could_not_clear_err)?;
|
res.map_err(could_not_clear_err)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}*/
|
||||||
|
|
||||||
/// Clear the activity, but if there is a running profile, set the activity to that instead
|
/// Clear the activity, but if there is a running profile, set the activity to that instead
|
||||||
pub async fn clear_to_default(
|
pub async fn clear_to_default(
|
||||||
@ -160,7 +161,7 @@ impl DiscordGuard {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
self.clear_activity(reconnect_if_fail).await?;
|
self.set_activity("Idling...", reconnect_if_fail).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,7 +156,7 @@ impl State {
|
|||||||
)));
|
)));
|
||||||
emit_loading(&loading_bar, 10.0, None).await?;
|
emit_loading(&loading_bar, 10.0, None).await?;
|
||||||
|
|
||||||
let is_offline = !fetch::check_internet(&fetch_semaphore, 3).await;
|
let is_offline = !fetch::check_internet(3).await;
|
||||||
|
|
||||||
let metadata_fut =
|
let metadata_fut =
|
||||||
Metadata::init(&directories, !is_offline, &io_semaphore);
|
Metadata::init(&directories, !is_offline, &io_semaphore);
|
||||||
@ -185,6 +185,10 @@ impl State {
|
|||||||
let safety_processes = SafeProcesses::new();
|
let safety_processes = SafeProcesses::new();
|
||||||
|
|
||||||
let discord_rpc = DiscordGuard::init().await?;
|
let discord_rpc = DiscordGuard::init().await?;
|
||||||
|
{
|
||||||
|
// Add default Idling to discord rich presence
|
||||||
|
let _ = discord_rpc.set_activity("Idling...", true).await;
|
||||||
|
}
|
||||||
|
|
||||||
// Starts a loop of checking if we are online, and updating
|
// Starts a loop of checking if we are online, and updating
|
||||||
Self::offine_check_loop();
|
Self::offine_check_loop();
|
||||||
@ -323,7 +327,7 @@ impl State {
|
|||||||
|
|
||||||
/// Refreshes whether or not the launcher should be offline, by whether or not there is an internet connection
|
/// Refreshes whether or not the launcher should be offline, by whether or not there is an internet connection
|
||||||
pub async fn refresh_offline(&self) -> crate::Result<()> {
|
pub async fn refresh_offline(&self) -> crate::Result<()> {
|
||||||
let is_online = fetch::check_internet(&self.fetch_semaphore, 3).await;
|
let is_online = fetch::check_internet(3).await;
|
||||||
|
|
||||||
let mut offline = self.offline.write().await;
|
let mut offline = self.offline.write().await;
|
||||||
|
|
||||||
@ -341,7 +345,7 @@ pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
|||||||
let (mut tx, mut rx) = channel(1);
|
let (mut tx, mut rx) = channel(1);
|
||||||
|
|
||||||
let file_watcher = new_debouncer(
|
let file_watcher = new_debouncer(
|
||||||
Duration::from_secs_f32(0.25),
|
Duration::from_secs_f32(2.0),
|
||||||
None,
|
None,
|
||||||
move |res: DebounceEventResult| {
|
move |res: DebounceEventResult| {
|
||||||
futures::executor::block_on(async {
|
futures::executor::block_on(async {
|
||||||
@ -394,7 +398,10 @@ pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
|||||||
Profile::crash_task(profile_path_id);
|
Profile::crash_task(profile_path_id);
|
||||||
} else if !visited_paths.contains(&new_path) {
|
} else if !visited_paths.contains(&new_path) {
|
||||||
if subfile {
|
if subfile {
|
||||||
Profile::sync_projects_task(profile_path_id);
|
Profile::sync_projects_task(
|
||||||
|
profile_path_id,
|
||||||
|
false,
|
||||||
|
);
|
||||||
visited_paths.push(new_path);
|
visited_paths.push(new_path);
|
||||||
} else {
|
} else {
|
||||||
Profiles::sync_available_profiles_task(
|
Profiles::sync_available_profiles_task(
|
||||||
|
|||||||
@ -222,7 +222,7 @@ pub async fn login_password(
|
|||||||
) -> crate::Result<ModrinthCredentialsResult> {
|
) -> crate::Result<ModrinthCredentialsResult> {
|
||||||
let resp = fetch_advanced(
|
let resp = fetch_advanced(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("https://{MODRINTH_API_URL}auth/login"),
|
&format!("{MODRINTH_API_URL}auth/login"),
|
||||||
None,
|
None,
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"username": username,
|
"username": username,
|
||||||
|
|||||||
@ -183,6 +183,10 @@ pub struct ProfileMetadata {
|
|||||||
pub date_modified: DateTime<Utc>,
|
pub date_modified: DateTime<Utc>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub last_played: Option<DateTime<Utc>>,
|
pub last_played: Option<DateTime<Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub submitted_time_played: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub recent_time_played: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@ -265,6 +269,8 @@ impl Profile {
|
|||||||
date_created: Utc::now(),
|
date_created: Utc::now(),
|
||||||
date_modified: Utc::now(),
|
date_modified: Utc::now(),
|
||||||
last_played: None,
|
last_played: None,
|
||||||
|
submitted_time_played: 0,
|
||||||
|
recent_time_played: 0,
|
||||||
},
|
},
|
||||||
projects: HashMap::new(),
|
projects: HashMap::new(),
|
||||||
java: None,
|
java: None,
|
||||||
@ -324,7 +330,7 @@ impl Profile {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_projects_task(profile_path_id: ProfilePathId) {
|
pub fn sync_projects_task(profile_path_id: ProfilePathId, force: bool) {
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let span =
|
let span =
|
||||||
tracing::span!(tracing::Level::INFO, "sync_projects_task");
|
tracing::span!(tracing::Level::INFO, "sync_projects_task");
|
||||||
@ -339,32 +345,34 @@ impl Profile {
|
|||||||
let profile = crate::api::profile::get(&profile_path_id, None).await?;
|
let profile = crate::api::profile::get(&profile_path_id, None).await?;
|
||||||
|
|
||||||
if let Some(profile) = profile {
|
if let Some(profile) = profile {
|
||||||
let paths = profile.get_profile_full_project_paths().await?;
|
if profile.install_stage != ProfileInstallStage::PackInstalling || force {
|
||||||
|
let paths = profile.get_profile_full_project_paths().await?;
|
||||||
|
|
||||||
let caches_dir = state.directories.caches_dir();
|
let caches_dir = state.directories.caches_dir();
|
||||||
let creds = state.credentials.read().await;
|
let creds = state.credentials.read().await;
|
||||||
let projects = crate::state::infer_data_from_files(
|
let projects = crate::state::infer_data_from_files(
|
||||||
profile.clone(),
|
profile.clone(),
|
||||||
paths,
|
paths,
|
||||||
caches_dir,
|
caches_dir,
|
||||||
&state.io_semaphore,
|
&state.io_semaphore,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
&creds,
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
drop(creds);
|
drop(creds);
|
||||||
|
|
||||||
let mut new_profiles = state.profiles.write().await;
|
let mut new_profiles = state.profiles.write().await;
|
||||||
if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
|
if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
|
||||||
profile.projects = projects;
|
profile.projects = projects;
|
||||||
|
}
|
||||||
|
emit_profile(
|
||||||
|
profile.uuid,
|
||||||
|
&profile_path_id,
|
||||||
|
&profile.metadata.name,
|
||||||
|
ProfilePayloadType::Synced,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
emit_profile(
|
|
||||||
profile.uuid,
|
|
||||||
&profile_path_id,
|
|
||||||
&profile.metadata.name,
|
|
||||||
ProfilePayloadType::Synced,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Unable to fetch single profile projects: path {profile_path_id} invalid",
|
"Unable to fetch single profile projects: path {profile_path_id} invalid",
|
||||||
@ -980,7 +988,7 @@ impl Profiles {
|
|||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Profile::sync_projects_task(profile_path_id);
|
Profile::sync_projects_task(profile_path_id, false);
|
||||||
}
|
}
|
||||||
Ok::<(), crate::Error>(())
|
Ok::<(), crate::Error>(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -213,18 +213,41 @@ pub async fn fetch_mirrors(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Using labrinth API, checks if an internet response can be found, with a timeout in seconds
|
/// Using labrinth API, checks if an internet response can be found, with a timeout in seconds
|
||||||
#[tracing::instrument(skip(semaphore))]
|
#[tracing::instrument]
|
||||||
#[theseus_macros::debug_pin]
|
#[theseus_macros::debug_pin]
|
||||||
pub async fn check_internet(semaphore: &FetchSemaphore, timeout: u64) -> bool {
|
pub async fn check_internet(timeout: u64) -> bool {
|
||||||
let result = fetch(
|
REQWEST_CLIENT
|
||||||
"https://api.modrinth.com",
|
.get("https://launcher-files.modrinth.com/detect.txt")
|
||||||
None,
|
.timeout(Duration::from_secs(timeout))
|
||||||
semaphore,
|
.send()
|
||||||
&CredentialsStore(None),
|
.await
|
||||||
);
|
.is_ok()
|
||||||
let result =
|
}
|
||||||
tokio::time::timeout(Duration::from_secs(timeout), result).await;
|
|
||||||
matches!(result, Ok(Ok(_)))
|
/// Posts a JSON to a URL
|
||||||
|
#[tracing::instrument(skip(json_body, semaphore))]
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
pub async fn post_json<T>(
|
||||||
|
url: &str,
|
||||||
|
json_body: serde_json::Value,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
credentials: &CredentialsStore,
|
||||||
|
) -> crate::Result<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let io_semaphore = semaphore.0.read().await;
|
||||||
|
let _permit = io_semaphore.acquire().await?;
|
||||||
|
|
||||||
|
let mut req = REQWEST_CLIENT.post(url).json(&json_body);
|
||||||
|
if let Some(creds) = &credentials.0 {
|
||||||
|
req = req.header("Authorization", &creds.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = req.send().await?.error_for_status()?;
|
||||||
|
|
||||||
|
let value = result.json().await?;
|
||||||
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read_json<T>(
|
pub async fn read_json<T>(
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_cli"
|
name = "theseus_cli"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "theseus_gui",
|
"name": "theseus_gui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@ -18,7 +18,7 @@
|
|||||||
"floating-vue": "^2.0.0-beta.20",
|
"floating-vue": "^2.0.0-beta.20",
|
||||||
"mixpanel-browser": "^2.47.0",
|
"mixpanel-browser": "^2.47.0",
|
||||||
"ofetch": "^1.0.1",
|
"ofetch": "^1.0.1",
|
||||||
"omorphia": "^0.4.34",
|
"omorphia": "^0.4.35",
|
||||||
"pinia": "^2.1.3",
|
"pinia": "^2.1.3",
|
||||||
"qrcode.vue": "^3.4.0",
|
"qrcode.vue": "^3.4.0",
|
||||||
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
|
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
|
||||||
|
|||||||
8
theseus_gui/pnpm-lock.yaml
generated
8
theseus_gui/pnpm-lock.yaml
generated
@ -21,8 +21,8 @@ dependencies:
|
|||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
omorphia:
|
omorphia:
|
||||||
specifier: ^0.4.34
|
specifier: ^0.4.35
|
||||||
version: 0.4.34
|
version: 0.4.35
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^2.1.3
|
specifier: ^2.1.3
|
||||||
version: 2.1.3(vue@3.3.4)
|
version: 2.1.3(vue@3.3.4)
|
||||||
@ -1348,8 +1348,8 @@ packages:
|
|||||||
ufo: 1.1.2
|
ufo: 1.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/omorphia@0.4.34:
|
/omorphia@0.4.35:
|
||||||
resolution: {integrity: sha512-6uAH1kgzbYYmJDM41Vy4/MhzT9kRj+s1t8IknHKeOQqmVft+wPtv/pbA7pqTMfCzBOarLKKO5s4sNlz8TeMmaQ==}
|
resolution: {integrity: sha512-ZxA6sJKWZbiG49l/gTG25cxAvTcIfVSLhuIV2e+LSY0nwkZO4EFvxhzGNz0exR3lVs+OdDCdJyb1U2QYMVbVrA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
dayjs: 1.11.7
|
dayjs: 1.11.7
|
||||||
floating-vue: 2.0.0-beta.20(vue@3.3.4)
|
floating-vue: 2.0.0-beta.20(vue@3.3.4)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Modrinth App",
|
"productName": "Modrinth App",
|
||||||
"version": "0.4.0"
|
"version": "0.5.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
@ -83,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; connect-src https://modrinth.com https://*.modrinth.com https://mixpanel.com https://*.mixpanel.com; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost"
|
"csp": "default-src 'self'; connect-src https://modrinth.com https://*.modrinth.com https://mixpanel.com https://*.mixpanel.com https://*.cloudflare.com; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com; frame-src https://*.cloudflare.com; style-src unsafe-inline"
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@ -96,11 +96,11 @@
|
|||||||
"titleBarStyle": "Overlay",
|
"titleBarStyle": "Overlay",
|
||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"height": 650,
|
"height": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Modrinth App",
|
"title": "Modrinth App",
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"minHeight": 630,
|
"minHeight": 700,
|
||||||
"minWidth": 1100,
|
"minWidth": 1100,
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"decorations": false
|
"decorations": false
|
||||||
|
|||||||
@ -5,11 +5,11 @@
|
|||||||
"titleBarStyle": "Overlay",
|
"titleBarStyle": "Overlay",
|
||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"height": 650,
|
"height": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Modrinth App",
|
"title": "Modrinth App",
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"minHeight": 630,
|
"minHeight": 700,
|
||||||
"minWidth": 1100,
|
"minWidth": 1100,
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"decorations": true
|
"decorations": true
|
||||||
|
|||||||
1
theseus_gui/src/assets/external/google.svg
vendored
1
theseus_gui/src/assets/external/google.svg
vendored
@ -1,6 +1,5 @@
|
|||||||
<svg
|
<svg
|
||||||
data-v-8c2610d6=""
|
data-v-8c2610d6=""
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2;"><circle cx="50" cy="50" r="50" style="fill:#fff;"
|
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2;"><circle cx="50" cy="50" r="50" style="fill:#fff;"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
@ -211,7 +211,7 @@ onMounted(() => {
|
|||||||
<div class="link-row">
|
<div class="link-row">
|
||||||
<a v-if="loggingIn" class="button-base" @click="loggingIn = false"> Create account </a>
|
<a v-if="loggingIn" class="button-base" @click="loggingIn = false"> Create account </a>
|
||||||
<a v-else class="button-base" @click="loggingIn = true">Sign in</a>
|
<a v-else class="button-base" @click="loggingIn = true">Sign in</a>
|
||||||
<a class="button-base" href="https://staging.modrinth.com/auth/reset-password">
|
<a class="button-base" href="https://modrinth.com/auth/reset-password">
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -83,15 +83,18 @@ const finishOnboarding = async () => {
|
|||||||
|
|
||||||
async function fetchSettings() {
|
async function fetchSettings() {
|
||||||
const fetchSettings = await get().catch(handleError)
|
const fetchSettings = await get().catch(handleError)
|
||||||
|
if (!fetchSettings.java_globals) {
|
||||||
|
fetchSettings.java_globals = {}
|
||||||
|
}
|
||||||
|
|
||||||
if (!fetchSettings.java_globals.JAVA_17) {
|
if (!fetchSettings.java_globals.JAVA_17) {
|
||||||
const path = await auto_install_java(17).catch(handleError)
|
const path1 = await auto_install_java(17).catch(handleError)
|
||||||
fetchSettings.java_globals.JAVA_17 = await get_jre(path).catch(handleError)
|
fetchSettings.java_globals.JAVA_17 = await get_jre(path1).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchSettings.java_globals.JAVA_8) {
|
if (!fetchSettings.java_globals.JAVA_8) {
|
||||||
const path = await auto_install_java(8).catch(handleError)
|
const path2 = await auto_install_java(8).catch(handleError)
|
||||||
fetchSettings.java_globals.JAVA_8 = await get_jre(path).catch(handleError)
|
fetchSettings.java_globals.JAVA_8 = await get_jre(path2).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
await set(fetchSettings).catch(handleError)
|
await set(fetchSettings).catch(handleError)
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { ofetch } from 'ofetch'
|
|||||||
import { handleError } from '@/store/state.js'
|
import { handleError } from '@/store/state.js'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
|
|
||||||
export const useFetch = async (url, item) => {
|
export const useFetch = async (url, item, isSilent) => {
|
||||||
try {
|
try {
|
||||||
const version = await getVersion()
|
const version = await getVersion()
|
||||||
|
|
||||||
@ -10,7 +10,9 @@ export const useFetch = async (url, item) => {
|
|||||||
headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` },
|
headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` },
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError({ message: `Error fetching ${item}` })
|
if (!isSilent) {
|
||||||
|
handleError({ message: `Error fetching ${item}` })
|
||||||
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -237,15 +237,22 @@ async function refreshSearch() {
|
|||||||
|
|
||||||
let val = `${base}${url}`
|
let val = `${base}${url}`
|
||||||
|
|
||||||
const rawResults = await useFetch(val, 'search results')
|
const rawResults = await useFetch(val, 'search results', offline.value)
|
||||||
|
results.value = rawResults
|
||||||
|
if (!rawResults) {
|
||||||
|
results.value = {
|
||||||
|
hits: [],
|
||||||
|
total_hits: 0,
|
||||||
|
limit: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
if (instanceContext.value) {
|
if (instanceContext.value) {
|
||||||
for (let val of rawResults.hits) {
|
for (let val of results.value) {
|
||||||
val.installed = await check_installed(instanceContext.value.path, val.project_id).then(
|
val.installed = await check_installed(instanceContext.value.path, val.project_id).then(
|
||||||
(x) => (val.installed = x)
|
(x) => (val.installed = x)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.value = rawResults
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSearchChange(newPageNumber) {
|
async function onSearchChange(newPageNumber) {
|
||||||
@ -510,7 +517,7 @@ onUnmounted(() => unlistenOffline())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!offline" ref="searchWrapper" class="search-container">
|
<div ref="searchWrapper" class="search-container">
|
||||||
<aside class="filter-panel">
|
<aside class="filter-panel">
|
||||||
<Card v-if="instanceContext" class="small-instance">
|
<Card v-if="instanceContext" class="small-instance">
|
||||||
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
|
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
|
||||||
@ -704,7 +711,10 @@ onUnmounted(() => unlistenOffline())
|
|||||||
class="pagination-before"
|
class="pagination-before"
|
||||||
@switch-page="onSearchChange"
|
@switch-page="onSearchChange"
|
||||||
/>
|
/>
|
||||||
<SplashScreen v-if="loading || offline" />
|
<SplashScreen v-if="loading" />
|
||||||
|
<section v-else-if="offline && results.total_hits == 0" class="offline">
|
||||||
|
You are currently offline. Connect to the internet to browse Modrinth!
|
||||||
|
</section>
|
||||||
<section v-else class="project-list display-mode--list instance-results" role="list">
|
<section v-else class="project-list display-mode--list instance-results" role="list">
|
||||||
<SearchCard
|
<SearchCard
|
||||||
v-for="result in results.hits"
|
v-for="result in results.hits"
|
||||||
@ -890,6 +900,11 @@ onUnmounted(() => unlistenOffline())
|
|||||||
margin: 0 1rem 0.5rem 20.5rem;
|
margin: 0 1rem 0.5rem 20.5rem;
|
||||||
width: calc(100% - 20.5rem);
|
width: calc(100% - 20.5rem);
|
||||||
|
|
||||||
|
.offline {
|
||||||
|
margin: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -41,23 +41,31 @@ const getInstances = async () => {
|
|||||||
const getFeaturedModpacks = async () => {
|
const getFeaturedModpacks = async () => {
|
||||||
const response = await useFetch(
|
const response = await useFetch(
|
||||||
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
|
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
|
||||||
'featured modpacks'
|
'featured modpacks',
|
||||||
|
offline.value
|
||||||
)
|
)
|
||||||
if (response) featuredModpacks.value = response.hits
|
if (response) {
|
||||||
|
featuredModpacks.value = response.hits
|
||||||
|
} else {
|
||||||
|
featuredModpacks.value = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const getFeaturedMods = async () => {
|
const getFeaturedMods = async () => {
|
||||||
const response = await useFetch(
|
const response = await useFetch(
|
||||||
'https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows',
|
'https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows',
|
||||||
'featured mods'
|
'featured mods',
|
||||||
|
offline.value
|
||||||
)
|
)
|
||||||
if (response) featuredMods.value = response.hits
|
if (response) {
|
||||||
|
featuredMods.value = response.hits
|
||||||
|
} else {
|
||||||
|
featuredModpacks.value = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await getInstances()
|
await getInstances()
|
||||||
|
|
||||||
if (!offline.value) {
|
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
|
||||||
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
|
|
||||||
}
|
|
||||||
|
|
||||||
const unlistenProfile = await profile_listener(async (e) => {
|
const unlistenProfile = await profile_listener(async (e) => {
|
||||||
await getInstances()
|
await getInstances()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user