Files
AstralRinth/apps/app/src/main.rs
Prospector ff4c7f47b2 Direct World Joining (#3457)
* Begin work on worlds backend

* Finish implementing get_profile_worlds and get_server_status (except pinning)

* Create TS types and manually copy unparsed chat components

* Clippy fix

* Update types.d.ts

* Initial worlds UI work

* Fix api::get_profile_worlds to take in a relative path

* sanitize & security update

* Fix sanitizePotentialFileUrl

* Fix sanitizePotentialFileUrl (for real)

* Fix empty motd causing error

* Finally actually fix world icons

* Fix world icon not being visible on non-Windows

* Use the correct generics to take in AppHandle

* Implement start_join_singleplayer_world and start_join_server for modern versions

* Don't error if server has no cached icon

* Migrate to own server pinging

* Ignore missing server hidden field and missing saves dir

* Update world list frontend

* More frontend work

* Server status player sample can be absent

* Fix refresh state

* Add get_profile_protocol_version

* Add protocol_version column to database

* SQL INTEGER is i64 in sqlx

* sqlx prepare

* Cache protocol version in database

* Continue worlds UI work

* Fix motds being bold

* Remove legacy pinging and add a 30-second timeout

* Remove pinned for now and match world (and server) parsing closer to spec

* Move type ServerStatus to worlds.ts

* Implement add_server_to_profile

* Fix pack_status being ignored when joining from launcher

* Make World path field be relative

* Implement rename_world and reset_world_icon

* Clippy fix

* Fix rename_world

* UI enhancements

* Implement backup_world, which returns the backup size in bytes

* Clippy fix

* Return index when adding servers to profile

* Fix backup

* Implement delete_world

* Implement edit_server_in_profile and remove_server_from_profile

* Clippy fix

* Log server joins

* Add edit and delete support

* Fix ts errors

* Fix minecraft font

* Switch font out for non-monospaced.

* Fix font proper

* Some more world cleanup, handle play state, check quickplay compatibility

* Clear the cached protocol version when a profile's game version is changed

* Fix tint colors in navbar

* Fix server protocol version pinging

* UI fixes

* Fix protocol version handler

* Fix MOTD parsing

* Add worlds_updated profile event

* fix pkg

* Functional home screen with worlds

* lint

* Fix incorrect folder creation

* Make items clickable

* Add locked field to SingleplayerWorld indicating whether the world is locked by the game

* Implement locking frontend

* Fix locking condition

* Split worlds_updated profile event into servers_updated and world_updated

* Fix compile error

* Use port from resolve SRV record

* Fix serialization of ProfilePayload and ProfilePayloadType

* Individual singleplayer world refreshing

* Log when worlds are perceived to be updated

* Push logging + total refresh lock

* Unlisten fixes

* Highlight current world when clicked

* Launcher logs refactor (#3444)

* Switch live log to use STDOUT

* fix clippy, legacy logs support

* Fix lint

* Handle non-XML log messages in XML logging, and don't escape log messages into XML

---------

Co-authored-by: Josiah Glosson <soujournme@gmail.com>

* Update incompatibility text

* Home page fixes, and unlock after close

* Remove logging

* Add join log database migration

* Switch server join timing to being in the database instead of in a separate log file

* Create optimized get_recent_worlds function that takes in a limit

* Update dependencies and fix Cargo.lock

* temp disable overflow menus

* revert home page changes

* Enable overflow menus again

* Remove list

* Revert

* Push dev tools

* Remove default filter

* Disable debug renderer

* Fix random app errors

* Refactor

* Fix missing computed import

* Fix light mode issues

* Fix TS errors

* Lint

* Fix bad link in change modpack version modal

* fix lint

* fix intl

---------

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-04-26 18:09:58 -07:00

352 lines
12 KiB
Rust

#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use native_dialog::{MessageDialog, MessageType};
use std::env;
use tauri::{Listener, Manager};
use theseus::prelude::*;
mod api;
mod error;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
// Should be called in launcher initialization
#[tracing::instrument(skip_all)]
#[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
tracing::info!("Initializing app event state...");
theseus::EventState::init(app.clone()).await?;
#[cfg(feature = "updater")]
'updater: {
if env::var("MODRINTH_EXTERNAL_UPDATE_PROVIDER").is_ok() {
State::init().await?;
break 'updater;
}
use tauri_plugin_updater::UpdaterExt;
let updater = app.updater_builder().build()?;
let update_fut = updater.check();
tracing::info!("Initializing app state...");
State::init().await?;
let check_bar = theseus::init_loading(
theseus::LoadingBarType::CheckingForUpdates,
1.0,
"Checking for updates...",
)
.await?;
tracing::info!("Checking for updates...");
let update = update_fut.await;
drop(check_bar);
if let Some(update) = update.ok().flatten() {
tracing::info!("Update found: {:?}", update.download_url);
let loader_bar_id = theseus::init_loading(
theseus::LoadingBarType::LauncherUpdate {
version: update.version.clone(),
current_version: update.current_version.clone(),
},
1.0,
"Updating Modrinth App...",
)
.await?;
// 100 MiB
const DEFAULT_CONTENT_LENGTH: u64 = 1024 * 1024 * 100;
update
.download_and_install(
|chunk_length, content_length| {
let _ = theseus::emit_loading(
&loader_bar_id,
(chunk_length as f64)
/ (content_length
.unwrap_or(DEFAULT_CONTENT_LENGTH)
as f64),
None,
);
},
|| {},
)
.await?;
app.restart();
}
}
#[cfg(not(feature = "updater"))]
{
State::init().await?;
}
tracing::info!("Finished checking for updates!");
let state = State::get().await?;
app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir(), true)?;
app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir().join("icons"), true)?;
Ok(())
}
// Should be call once Vue has mounted the app
#[tracing::instrument(skip_all)]
#[tauri::command]
fn show_window(app: tauri::AppHandle) {
let win = app.get_window("main").unwrap();
if let Err(e) = win.show() {
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text(&format!(
"Cannot display application window due to an error:\n{}",
e
))
.show_alert()
.unwrap();
panic!("cannot display application window")
} else {
let _ = win.set_focus();
}
}
#[tauri::command]
fn is_dev() -> bool {
cfg!(debug_assertions)
}
// Toggles decorations
#[tauri::command]
async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
window.set_decorations(b).map_err(|e| {
theseus::Error::from(theseus::ErrorKind::OtherError(format!(
"Failed to toggle decorations: {}",
e
)))
})?;
Ok(())
}
#[tauri::command]
fn restart_app(app: tauri::AppHandle) {
app.restart();
}
// if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks
fn main() {
/*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
ERROR > WARN > INFO > DEBUG > TRACE
eg. RUST_LOG=info will show info, warn, and error logs
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
Error messages returned to Tauri will display as traced error logs if they return an error.
This will also include an attached span trace if the error is from a tracing error, and the level is set to info, debug, or trace
on unix:
RUST_LOG="theseus=trace" {run command}
*/
let _log_guard = theseus::start_logger();
tracing::info!("Initialized tracing subscriber. Loading Modrinth App!");
let mut builder = tauri::Builder::default();
#[cfg(feature = "updater")]
{
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
}
builder = builder
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
if let Some(payload) = args.get(1) {
tracing::info!("Handling deep link from arg {payload}");
let payload = payload.clone();
tauri::async_runtime::spawn(api::utils::handle_command(
payload,
));
}
if let Some(win) = app.get_window("main") {
let _ = win.set_focus();
}
}))
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_opener::init())
.plugin(
tauri_plugin_window_state::Builder::default()
.with_filename("app-window-state.json")
.build(),
)
.setup(|app| {
#[cfg(target_os = "macos")]
{
let payload = macos::deep_link::get_or_init_payload(app);
let mtx_copy = payload.payload.clone();
app.listen("deep-link://new-url", move |url| {
let mtx_copy_copy = mtx_copy.clone();
let request = url.payload().to_owned();
let actual_request =
serde_json::from_str::<Vec<String>>(&request)
.ok()
.map(|mut x| x.remove(0))
.unwrap_or(request);
tauri::async_runtime::spawn(async move {
tracing::info!("Handling deep link {actual_request}");
let mut payload = mtx_copy_copy.lock().await;
if payload.is_none() {
*payload = Some(actual_request.clone());
}
let _ =
api::utils::handle_command(actual_request).await;
});
});
};
#[cfg(not(target_os = "macos"))]
app.listen("deep-link://new-url", |url| {
let payload = url.payload().to_owned();
tracing::info!("Handling deep link {payload}");
tauri::async_runtime::spawn(api::utils::handle_command(
payload,
));
dbg!(url);
});
#[cfg(not(target_os = "linux"))]
{
if let Some(window) = app.get_window("main") {
window.set_shadow(true).unwrap();
}
}
Ok(())
});
builder = builder
.plugin(api::auth::init())
.plugin(api::mr_auth::init())
.plugin(api::import::init())
.plugin(api::logs::init())
.plugin(api::jre::init())
.plugin(api::metadata::init())
.plugin(api::pack::init())
.plugin(api::process::init())
.plugin(api::profile::init())
.plugin(api::profile_create::init())
.plugin(api::settings::init())
.plugin(api::tags::init())
.plugin(api::utils::init())
.plugin(api::cache::init())
.plugin(api::ads::init())
.plugin(api::friends::init())
.plugin(api::worlds::init())
.invoke_handler(tauri::generate_handler![
initialize_state,
is_dev,
toggle_decorations,
show_window,
restart_app,
]);
#[cfg(target_os = "macos")]
{
builder = builder.plugin(macos::window_ext::init());
}
tracing::info!("Initializing app...");
let app = builder.build(tauri::generate_context!());
match app {
Ok(app) => {
#[allow(unused_variables)]
app.run(|app, event| {
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = event {
tracing::info!("Handling webview open {urls:?}");
let file = urls
.into_iter()
.filter_map(|url| url.to_file_path().ok())
.next();
if let Some(file) = file {
let payload =
macos::deep_link::get_or_init_payload(app);
let mtx_copy = payload.payload.clone();
let request = file.to_string_lossy().to_string();
tauri::async_runtime::spawn(async move {
let mut payload = mtx_copy.lock().await;
if payload.is_none() {
*payload = Some(request.clone());
}
let _ = api::utils::handle_command(request).await;
});
}
}
});
}
Err(e) => {
#[cfg(target_os = "windows")]
{
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
if format!("{:?}", e).contains(
"Runtime(CreateWebview(WebView2Error(WindowsError",
) {
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text("Your Microsoft Edge WebView2 installation is corrupt.\n\nMicrosoft Edge WebView2 is required to run Modrinth App.\n\nLearn how to repair it at https://support.modrinth.com/en/articles/8797765-corrupted-microsoft-edge-webview2-installation")
.show_alert()
.unwrap();
panic!("webview2 initialization failed")
}
}
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text(&format!(
"Cannot initialize application due to an error:\n{:?}",
e
))
.show_alert()
.unwrap();
tracing::error!("Error while running tauri application: {:?}", e);
panic!("{1}: {:?}", e, "error while running tauri application")
}
}
}