You've already forked AstralRinth
forked from didirus/AstralRinth
* 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>
352 lines
12 KiB
Rust
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")
|
|
}
|
|
}
|
|
}
|