mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 01:54:47 +00:00
* refactor: remove unused legacy onboarded flag * sqlx prepare * feat: add onboarding checklist app db and add vue provider * feat: add welcome page * feat: add onboarding checklist component * feat: move checklist visibility source of truth in app db * feat: split up import modpack button * feat-small: style tweak * feat: remove discover section from homepage and add library * refactor: move welcome screen to component * refactor: move grid display into /library * refactor: rename existing components * refactor: split up the current library page files * feat: add create group modal * feat: add instance group definitions * feat: update accordian for instance groups * feat: add delete group * feat: implement rename instance groups * refactor: move instance group api out * feat: add context menu for groups * feat: style instance items in library * feat: add selected instance item state * feat: implement action bar for selection * feat: drag and drop to move between groups * feat: show ungrouped with header * feat: improve edit group name * small fix * small style fixes * feat: improve selection behaviour * fix: dont toggle group when context menu open * feat: drag and drop multiple selected * small * feat: add remove from group in action bar * feat: add delay on icon hiding in inline input * feat: improve icon hiding delay * fix: bad routes.js * feat: implement sort and filter options * refactor: dont prefix filenames * feat: change open/collapse group target * feat: improve ungrouped behaviour * feat: add shift to select multiple instances * fix: icon disappear transition * feat: add esc while dragging to cancel * feat: update sorting and moved label * feat: update context menu * fix: multiple context menus can be opened * refactor: instance groups to be identified by ID instead of name, names become display data * feat: creating new group always adds instance * feat: create group button, fix instance selection, and increase group name max length * feat: strokes * fix: show getting started checklist in all sidebar * feat: set size on microsoft login screen * fix: use ButtonStyled in Chips component * feat: creating groups with selected instances and fix some moving selection and duplication bugs * pnpm prepr * feat: add tooltip for instance already in group * feat: create instance with content in creation flow modal - search modpack becomes search projects - creation flow modal context receives prepareProjectInstall and createProjectInstall callbacks, from content-install.ts (shared with ContentInstallModal) * feat: update instance card styles * refactor: separate out instance card content * update styles * refactor: instance card * fix: width of instance * feat: update empty state * feat: update sort and grouping dropdowns * feat: better context menu options * feat: add group action buttons * feat: add/remove favourites group * remove manual sort * feat: improve the "New group" jank * feat: add group by instance type and change group sorting * feat: welcome screen styles match figma * feat: not signed in skins demo * feat: new hosting empty state * feat: new instance card style * feat: add tooltip and spice up animation more * feat: improve the transitions for groups after dropping many instances * feat: set instance group membership for bulk moving * feat: remove fade out * fix: allow dragging from buttons inside instance card * fix: library page load in behaviour * small fix * feat: add group ordering and reordering * feat: add drag to reorder groups * fix: switching page not clearing selected instances * style fixes * style fix * fix spacing * feat: jump in changes * feat: add styled newly created instances * feat: improve jump in cards styles * fix: welcome screen style * fix: empty state styles * feat: max 5 jump in items * feat: max new instances to 3 * fix: search icon * feat: clear selection after move * feat: skins list use check icon styles * feat: list will be deleted instances and default sort last played * fix: stroke * remove: no more group settings in instance general settings * fix: resolving modpack server install * feat: icon editor * feat: add three hue shifted variants * fix: surprise me doesnt pick symbol * feat: add customize icon to create instance modal * feat: skins loading state style * fix: welcome logo * fix: copy * fix: left over usage of ButtonStyled from main's refactor * feat: some leaked merge stuff * feat: server empty state styles * remove: instance helper text * remove: auto link in set up stage * feat: auto focus on search when modal open * fix: delete instance modal style * remove: modpack meta repeating * fix: icon * fix: font weight * fix: dropdown filter preview heights * pnpm prepr * fix: sign into modrinth flashing change in position * feat: add avatar transparent corner gets padding * feat: drag and hold over group opens group * feat: add random icon for custom instance * feat: update loading icon * polish: empty state for search * polish: jump in cards * fix: avatar could skip checking for image if tauri loads it first * feat: add icon customize and randomizer in content install modal for add to new instances * fix: avatar cannot read image pixels due to cors * refactor: popup notifications toast type for instance sharing notifications * feat: apply random icons modal and add popup notification for it * polish: style * fix: set_webview_visible * QA * qa * feat: add inner white 15 opacity stroke around avatar * add to catalog * pnpm prepr * fix: app event import * feat: add new icons and backgrounds for icon editor * feat: recents list generate from randomized icons and save icon in editor + icon editor modal fixes * qa * refactor: recipe -> config * fix: event bus * polish * feat: add safe guard against randomizing empty icons * feat: navigate to instance after creation * fix: icon file names * i18n on welcome screen * i18n pass on PR * fix: inconsistent max group name length * refactor: use sqlx macro * feat: add instance icon background selection drag * feat: update catalog order * fix: remove gradient for welcome screen * remove: frog * fix: min width * feat: update corner size * fix: context menu styles * prepr * fix: instance name trimming * reorder * fix: instance card loading state stayed until hovering * feat: add icon randomizer blacklist * blog * prepr --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
423 lines
16 KiB
Rust
423 lines
16 KiB
Rust
#![cfg_attr(
|
|
all(not(debug_assertions), target_os = "windows"),
|
|
windows_subsystem = "windows"
|
|
)]
|
|
#![recursion_limit = "256"]
|
|
|
|
use native_dialog::{DialogBuilder, MessageLevel};
|
|
use std::env;
|
|
use std::sync::atomic::Ordering;
|
|
use tauri::{Listener, Manager};
|
|
use tauri_plugin_fs::FsExt;
|
|
use theseus::prelude::*;
|
|
|
|
mod api;
|
|
mod error;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
mod macos;
|
|
|
|
#[cfg(feature = "updater")]
|
|
mod updater_impl;
|
|
#[cfg(not(feature = "updater"))]
|
|
mod updater_impl_noop;
|
|
|
|
// Should be called in launcher initialization
|
|
#[tracing::instrument(skip_all)]
|
|
#[tauri::command]
|
|
async fn initialize_state(
|
|
app: tauri::AppHandle,
|
|
events: tauri::ipc::Channel<tauri::ipc::InvokeResponseBody>,
|
|
) -> api::Result<()> {
|
|
tracing::info!("Initializing app event state...");
|
|
theseus::EventState::init(app.clone(), events).await?;
|
|
|
|
tracing::info!("Initializing app state...");
|
|
State::init(app.config().identifier.clone()).await?;
|
|
|
|
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)?;
|
|
app.fs_scope()
|
|
.allow_directory(state.directories.instances_dir(), 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() {
|
|
DialogBuilder::message()
|
|
.set_level(MessageLevel::Error)
|
|
.set_title("Initialization error")
|
|
.set_text(format!(
|
|
"Cannot display application window due to an error:\n{e}"
|
|
))
|
|
.alert()
|
|
.show()
|
|
.unwrap();
|
|
panic!("cannot display application window")
|
|
} else {
|
|
let _ = win.set_focus();
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn is_dev() -> bool {
|
|
cfg!(debug_assertions)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn are_updates_enabled() -> bool {
|
|
cfg!(feature = "updater")
|
|
&& env::var("MODRINTH_EXTERNAL_UPDATE_PROVIDER").is_err()
|
|
}
|
|
|
|
#[cfg(feature = "updater")]
|
|
pub use updater_impl::*;
|
|
|
|
#[cfg(not(feature = "updater"))]
|
|
pub use updater_impl_noop::*;
|
|
|
|
// 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();
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn set_restart_after_pending_update(
|
|
should_restart: bool,
|
|
) -> api::Result<()> {
|
|
let state = State::get().await?;
|
|
state
|
|
.restart_after_pending_update
|
|
.store(should_restart, Ordering::Relaxed);
|
|
Ok(())
|
|
}
|
|
|
|
// if Tauri app is called with arguments, then those arguments will be treated as commands
|
|
// ie: deep links or filepaths for .mrpacks
|
|
fn main() {
|
|
#[cfg(feature = "export-app-events")]
|
|
theseus::export_app_event_bindings(
|
|
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../app-frontend/src/generated/app-events"),
|
|
)
|
|
.expect("failed to export app event TypeScript bindings");
|
|
|
|
/*
|
|
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 tauri_context = tauri::generate_context!();
|
|
|
|
let _log_guard = theseus::start_logger(&tauri_context.config().identifier);
|
|
|
|
tracing::info!("Initialized tracing subscriber. Loading Modrinth App!");
|
|
|
|
let mut builder = tauri::Builder::default();
|
|
|
|
#[cfg(feature = "updater")]
|
|
{
|
|
use tauri_plugin_http::reqwest::header::{HeaderValue, USER_AGENT};
|
|
use theseus::launcher_user_agent;
|
|
builder = builder.plugin(
|
|
tauri_plugin_updater::Builder::new()
|
|
.header(
|
|
USER_AGENT,
|
|
HeaderValue::from_str(&launcher_user_agent()).unwrap(),
|
|
)
|
|
.unwrap()
|
|
.build(),
|
|
);
|
|
}
|
|
|
|
builder = builder
|
|
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
|
if let Some(payload) = args.get(1) {
|
|
tracing::info!("Handling command-line deep link");
|
|
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_http::init())
|
|
.plugin(tauri_plugin_os::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
.plugin(tauri_plugin_fs::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(
|
|
tauri_plugin_window_state::Builder::default()
|
|
.with_filename("app-window-state.json")
|
|
.with_denylist(&["signin"])
|
|
// Use *only* POSITION and SIZE state flags, because saving VISIBLE causes the `visible: false` to not take effect
|
|
.with_state_flags(
|
|
tauri_plugin_window_state::StateFlags::POSITION
|
|
| tauri_plugin_window_state::StateFlags::SIZE
|
|
| tauri_plugin_window_state::StateFlags::MAXIMIZED,
|
|
)
|
|
.build(),
|
|
)
|
|
.setup(|app| {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let payload = macos::deep_link::get_or_init_payload(app);
|
|
|
|
let mtx_copy = payload.payload;
|
|
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 macOS deep link");
|
|
|
|
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");
|
|
tauri::async_runtime::spawn(api::utils::handle_command(
|
|
payload,
|
|
));
|
|
});
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
if let Some(window) = app.get_window("main")
|
|
&& let Err(e) = window.set_shadow(true)
|
|
{
|
|
tracing::warn!("Failed to set window shadow: {e}");
|
|
}
|
|
|
|
Ok(())
|
|
});
|
|
|
|
builder = builder
|
|
.plugin(api::auth::init())
|
|
.plugin(api::mr_auth::init())
|
|
.plugin(api::onboarding_checklist::init())
|
|
.plugin(api::import::init())
|
|
.plugin(api::install::init())
|
|
.plugin(api::instance::init())
|
|
.plugin(api::logs::init())
|
|
.plugin(api::jre::init())
|
|
.plugin(api::metadata::init())
|
|
.plugin(api::minecraft_skins::init())
|
|
.plugin(api::process::init())
|
|
.plugin(api::reports::init())
|
|
.plugin(api::settings::init())
|
|
.plugin(api::shortcuts::init())
|
|
.plugin(api::tags::init())
|
|
.plugin(api::users::init())
|
|
.plugin(api::utils::init())
|
|
.plugin(api::cache::init())
|
|
.plugin(api::files::init())
|
|
.plugin(api::ads::init())
|
|
.plugin(api::friends::init())
|
|
.plugin(api::worlds::init())
|
|
.manage(PendingUpdateData::default())
|
|
.invoke_handler(tauri::generate_handler![
|
|
initialize_state,
|
|
is_dev,
|
|
are_updates_enabled,
|
|
get_update_size,
|
|
enqueue_update_for_installation,
|
|
remove_enqueued_update,
|
|
set_restart_after_pending_update,
|
|
toggle_decorations,
|
|
show_window,
|
|
restart_app,
|
|
]);
|
|
|
|
tracing::info!("Initializing app...");
|
|
let app = builder.build(tauri_context);
|
|
|
|
match app {
|
|
Ok(app) => {
|
|
app.run(|app, event| {
|
|
#[cfg(not(any(feature = "updater", target_os = "macos")))]
|
|
let _ = app;
|
|
|
|
if matches!(&event, tauri::RunEvent::ExitRequested { .. })
|
|
&& let Err(error) = tauri::async_runtime::block_on(
|
|
theseus::minecraft_skins::flush_pending_skin_change(),
|
|
)
|
|
{
|
|
tracing::warn!(
|
|
"Failed to flush pending Minecraft skin change before exit: {error}"
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "updater")]
|
|
if matches!(&event, tauri::RunEvent::Exit) {
|
|
let update_data = app.state::<PendingUpdateData>().inner();
|
|
let should_restart = State::get_if_initialized()
|
|
.map(|s| {
|
|
s.restart_after_pending_update.load(Ordering::Relaxed)
|
|
})
|
|
.unwrap_or(false);
|
|
if let Some((update, data)) = &*update_data.0.lock().unwrap()
|
|
{
|
|
fn set_changelog_toast(version: Option<String>) {
|
|
let toast_result: theseus::Result<()> = tauri::async_runtime::block_on(async move {
|
|
let mut settings = settings::get().await?;
|
|
settings.pending_update_toast_for_version = version;
|
|
settings::set(settings).await?;
|
|
Ok(())
|
|
});
|
|
if let Err(e) = toast_result {
|
|
tracing::warn!(
|
|
"Failed to set pending_update_toast: {e}"
|
|
)
|
|
}
|
|
}
|
|
|
|
set_changelog_toast(Some(update.version.clone()));
|
|
let update = if should_restart {
|
|
(**update).clone()
|
|
} else {
|
|
(**update).clone().restart_after_install(false)
|
|
};
|
|
match update.install(data) {
|
|
Ok(()) => {
|
|
if should_restart {
|
|
tracing::info!(
|
|
"Pending update installed successfully (version {}); restarting because user requested reload",
|
|
update.version
|
|
);
|
|
app.restart();
|
|
} else {
|
|
tracing::info!(
|
|
"Pending update installed successfully (version {}); exiting without relaunch (user did not request reload)",
|
|
update.version
|
|
);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(
|
|
"Pending update install failed (version {}): {e}",
|
|
update.version
|
|
);
|
|
set_changelog_toast(None);
|
|
|
|
DialogBuilder::message()
|
|
.set_level(MessageLevel::Error)
|
|
.set_title("Update error")
|
|
.set_text(format!("Failed to install update due to an error:\n{e}"))
|
|
.alert()
|
|
.show()
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
if let tauri::RunEvent::Opened { urls } = event {
|
|
tracing::info!("Handling webview open {urls:?}");
|
|
|
|
let file = urls
|
|
.into_iter()
|
|
.find_map(|url| url.to_file_path().ok());
|
|
|
|
if let Some(file) = file {
|
|
let payload =
|
|
macos::deep_link::get_or_init_payload(app);
|
|
|
|
let mtx_copy = payload.payload;
|
|
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) => {
|
|
tracing::error!("Error while running tauri application: {:?}", 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",
|
|
) {
|
|
DialogBuilder::message()
|
|
.set_level(MessageLevel::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")
|
|
.alert()
|
|
.show()
|
|
.unwrap();
|
|
|
|
panic!("webview2 initialization failed")
|
|
}
|
|
}
|
|
|
|
DialogBuilder::message()
|
|
.set_level(MessageLevel::Error)
|
|
.set_title("Initialization error")
|
|
.set_text(format!(
|
|
"Cannot initialize application due to an error:\n{e:?}"
|
|
))
|
|
.alert()
|
|
.show()
|
|
.unwrap();
|
|
|
|
panic!("{1}: {:?}", e, "error while running tauri application")
|
|
}
|
|
}
|
|
}
|