Reworked app update flow (#3960)

* Make theseus capable of logging messages from the `log` crate

* Move update checking entirely into JS and open a modal if an update is available

* Fix formatjs on Windows and run formatjs

* Add in the buttons and body

* Fix lint

* Show update size in modal

* Fix update not being rechecked if the update modal was directly dismissed

* Slight UI tweaks

* Fix lint

* Implement skipping the update

* Implement the Update Now button

* Implement updating at next exit

* Turn download progress into an error bar on failure

* Restore 5 minute update check instead of 30 seconds

* Fix PendingUpdateData being seen as a unit struct

* Fix lint

* Make CI also lint updater code

* feat: create AppearingProgressBar component

* feat: polish update available modal

* feat: add error handling

* Open changelog with tauri-plugin-opener

* Run intl:extract

* Update completion toasts (#3978)

* Use single LAUNCHER_USER_AGENT constant for all user agents

* Fix build on Mac

* Request the update size with HEAD instead of GET

* UI tweaks

* lint

* Fix lint

* fix: hide modal header & add "Hide update reminder" button w/ tooltip

* Run intl:extract

* fix: lint issues

* fix: merge issues

* notifications.js no longer exists

* Add metered network checking

* Add a timeout to macOS is_network_metered

* Fix tauri.conf.json

* vibe debugging

* Set a dispatch queue

* Have a popup that asks you if you'd like to disable automatic file downloads if you're on a metered network

* Move UpdateModal to modal package

* Fix lint

* Add a toggle for automatic downloads

* Fix type

Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
Signed-off-by: Josiah Glosson <soujournme@gmail.com>

* Redo updating UI and experience

* lint

* fix unlistener issue

* remove unneeded translation keys

* Fix expose issue

* temp disable cranelift, tweak some messages

* change version back

* Clean up App.vue

* move toast to top right

* update reload icon

* Fixed the bug!!!!!!!!!!!!

* improve messages

* intl:extract

* Add liquid glass icon file

* not you!

* use dependency injection

* lint on apple icon

* Fix imports, move download size to button

* change update check back to 5 mins

* lint + move to providers

* intl:extract

---------

Signed-off-by: Cal H. <hendersoncal117@gmail.com>
Signed-off-by: Josiah Glosson <soujournme@gmail.com>
Co-authored-by: Calum <calum@modrinth.com>
Co-authored-by: Prospector <prospectordev@gmail.com>
Co-authored-by: Cal H. <hendersoncal117@gmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
This commit is contained in:
Josiah Glosson
2025-09-29 09:28:31 -06:00
committed by GitHub
parent f6f66a313f
commit a538b99c18
49 changed files with 1487 additions and 284 deletions

View File

@@ -37,7 +37,7 @@ pub mod prelude {
settings,
util::{
io::{IOError, canonicalize},
network::tcp_listen_any_loopback,
network::{is_network_metered, tcp_listen_any_loopback},
},
};
}

View File

@@ -166,6 +166,13 @@ pub enum ErrorKind {
#[error("RPC error: {0}")]
RpcError(String),
#[cfg(windows)]
#[error("Windows error: {0}")]
WindowsError(#[from] windows_core::Error),
#[error("zbus error: {0}")]
ZbusError(#[from] zbus::Error),
}
#[derive(Debug)]

View File

@@ -176,7 +176,6 @@ pub enum LoadingBarType {
import_location: PathBuf,
profile_name: String,
},
CheckingForUpdates,
LauncherUpdate {
version: String,
current_version: String,

View File

@@ -25,3 +25,9 @@ pub use event::{
};
pub use logger::start_logger;
pub use state::State;
pub const LAUNCHER_USER_AGENT: &str = concat!(
"modrinth/theseus/",
env!("CARGO_PKG_VERSION"),
" (support@modrinth.com)"
);

View File

@@ -25,12 +25,11 @@ pub fn start_logger() -> Option<()> {
.unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("theseus=info,theseus_gui=info")
});
let subscriber = tracing_subscriber::registry()
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
.with(tracing_error::ErrorLayer::default())
.init();
Some(())
}
@@ -76,7 +75,7 @@ pub fn start_logger() -> Option<()> {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
let subscriber = tracing_subscriber::registry()
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(file)
@@ -84,10 +83,8 @@ pub fn start_logger() -> Option<()> {
.with_timer(ChronoLocal::rfc_3339()),
)
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber)
.expect("Setting default subscriber failed");
.with(tracing_error::ErrorLayer::default())
.init();
Some(())
}

View File

@@ -1,4 +1,5 @@
use crate::ErrorKind;
use crate::LAUNCHER_USER_AGENT;
use crate::data::ModrinthCredentials;
use crate::event::FriendPayload;
use crate::event::emit::emit_friend;
@@ -82,13 +83,9 @@ impl FriendsSocket {
)
.into_client_request()?;
let user_agent = format!(
"modrinth/theseus/{} (support@modrinth.com)",
env!("CARGO_PKG_VERSION")
);
request.headers_mut().insert(
"User-Agent",
HeaderValue::from_str(&user_agent).unwrap(),
HeaderValue::from_str(LAUNCHER_USER_AGENT).unwrap(),
);
let res = connect_async(request).await;

View File

@@ -38,6 +38,10 @@ pub struct Settings {
pub developer_mode: bool,
pub feature_flags: HashMap<FeatureFlag, bool>,
pub skipped_update: Option<String>,
pub pending_update_toast_for_version: Option<String>,
pub auto_download_updates: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)]
@@ -63,7 +67,8 @@ impl Settings {
json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,
mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,
hook_pre_launch, hook_wrapper, hook_post_exit,
custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar
custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,
skipped_update, pending_update_toast_for_version, auto_download_updates
FROM settings
"
)
@@ -117,6 +122,10 @@ impl Settings {
.as_ref()
.and_then(|x| serde_json::from_str(x).ok())
.unwrap_or_default(),
skipped_update: res.skipped_update,
pending_update_toast_for_version: res
.pending_update_toast_for_version,
auto_download_updates: res.auto_download_updates.map(|x| x == 1),
})
}
@@ -170,7 +179,11 @@ impl Settings {
toggle_sidebar = $26,
feature_flags = $27,
hide_nametag_skins_page = $28
hide_nametag_skins_page = $28,
skipped_update = $29,
pending_update_toast_for_version = $30,
auto_download_updates = $31
",
max_concurrent_writes,
max_concurrent_downloads,
@@ -199,7 +212,10 @@ impl Settings {
self.migrated,
self.toggle_sidebar,
feature_flags,
self.hide_nametag_skins_page
self.hide_nametag_skins_page,
self.skipped_update,
self.pending_update_toast_for_version,
self.auto_download_updates,
)
.execute(exec)
.await?;

View File

@@ -1,6 +1,7 @@
//! Functions for fetching information from the Internet
use super::io::{self, IOError};
use crate::ErrorKind;
use crate::LAUNCHER_USER_AGENT;
use crate::event::LoadingBarId;
use crate::event::emit::emit_loading;
use bytes::Bytes;
@@ -20,11 +21,8 @@ pub struct FetchSemaphore(pub Semaphore);
pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
let mut headers = reqwest::header::HeaderMap::new();
let header = reqwest::header::HeaderValue::from_str(&format!(
"modrinth/theseus/{} (support@modrinth.com)",
env!("CARGO_PKG_VERSION")
))
.unwrap();
let header =
reqwest::header::HeaderValue::from_str(LAUNCHER_USER_AGENT).unwrap();
headers.insert(reqwest::header::USER_AGENT, header);
reqwest::Client::builder()
.tcp_keepalive(Some(time::Duration::from_secs(10)))

View File

@@ -1,3 +1,4 @@
use crate::Result;
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use tokio::net::TcpListener;
@@ -15,3 +16,78 @@ pub async fn tcp_listen_any_loopback() -> io::Result<TcpListener> {
TcpListener::bind(ANY_LOOPBACK_SOCKET).await
}
#[cfg(windows)]
pub async fn is_network_metered() -> Result<bool> {
use windows::Networking::Connectivity::{
NetworkCostType, NetworkInformation,
};
let cost_type = NetworkInformation::GetInternetConnectionProfile()?
.GetConnectionCost()?
.NetworkCostType()?;
Ok(matches!(
cost_type,
NetworkCostType::Fixed | NetworkCostType::Variable
))
}
#[cfg(target_os = "macos")]
pub async fn is_network_metered() -> Result<bool> {
use crate::ErrorKind;
use cidre::dispatch::Queue;
use cidre::nw::PathMonitor;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_util::future::FutureExt;
let (sender, mut receiver) = mpsc::channel(1);
let queue = Queue::new();
let mut monitor = PathMonitor::new();
monitor.set_queue(&queue);
monitor.set_update_handler(move |path| {
let _ = sender.try_send(path.is_constrained() || path.is_expensive());
});
monitor.start();
let result = receiver
.recv()
.timeout(Duration::from_millis(100))
.await
.ok()
.flatten();
monitor.cancel();
result.ok_or_else(|| {
ErrorKind::OtherError(
"NWPathMonitor didn't provide an NWPath in time".to_string(),
)
.into()
})
}
#[cfg(target_os = "linux")]
pub async fn is_network_metered() -> Result<bool> {
// Thanks to https://github.com/Hakanbaban53/rclone-manager for showing how to do this
use zbus::{Connection, Proxy};
let connection = Connection::system().await?;
let proxy = Proxy::new(
&connection,
"org.freedesktop.NetworkManager",
"/org/freedesktop/NetworkManager",
"org.freedesktop.NetworkManager",
)
.await?;
let metered = proxy.get_property("Metered").await?;
Ok(matches!(metered, 1 | 3))
}
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
pub async fn is_network_metered() -> Result<bool> {
tracing::warn!(
"is_network_metered called on unsupported platform. Assuming unmetered."
);
Ok(false)
}