MR App 0.9.5 - Big bugfix update (#3585)

* Add launcher_feature_version to Profile

* Misc fixes

- Add typing to theme and settings stuff
- Push instance route on creation from installing a modpack
- Fixed servers not reloading properly when first added

* Make old instances scan the logs folder for joined servers on launcher startup

* Create AttachedWorldData

* Change AttachedWorldData interface

* Rename WorldType::World to WorldType::Singleplayer

* Implement world display status system

* Fix Minecraft font

* Fix set_world_display_status Tauri error

* Add 'Play instance' option

* Add option to disable worlds showing in Home

* Fixes

- Fix available server filter only showing if there are some available
- Fixed server and singleplayer filters sometimes showing when there are only servers or singleplayer worlds
- Fixed new worlds not being automatically added when detected
- Rephrased Jump back into worlds option description

* Fixed sometimes more than 6 items showing up in Jump back in

* Fix servers.dat issue with instances you haven't played before

* Fix too large of bulk requests being made, limit max to 800 #3430

* Add hiding from home page, add types to Mods.vue

* Make recent worlds go into grid when display is huge

* Fix lint

* Remove redundant media query

* Fix protocol version on home page, and home page being blocked by pinging servers

* Clippy fix

* More Clippy fixes

* Fix Prettier lints

* Undo `from_string` changes

---------

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Co-authored-by: Alejandro González <me@alegon.dev>
This commit is contained in:
Prospector
2025-05-01 16:13:13 -07:00
committed by GitHub
parent 4a2605bc1e
commit 3dad6b317f
123 changed files with 1622 additions and 744 deletions

View File

@@ -42,8 +42,8 @@ impl CensoredString {
pub fn censor(mut s: String, credentials_set: &Vec<Credentials>) -> Self {
let username = whoami::username();
s = s
.replace(&format!("/{}/", username), "/{COMPUTER_USERNAME}/")
.replace(&format!("\\{}\\", username), "\\{COMPUTER_USERNAME}\\");
.replace(&format!("/{username}/"), "/{COMPUTER_USERNAME}/")
.replace(&format!("\\{username}\\"), "\\{COMPUTER_USERNAME}\\");
for credentials in credentials_set {
s = s
.replace(&credentials.access_token, "{MINECRAFT_ACCESS_TOKEN}")

View File

@@ -30,7 +30,7 @@ pub async fn get_loader_versions(loader: &str) -> crate::Result<Manifest> {
)
.await?
.ok_or_else(|| {
crate::ErrorKind::NoValueFor(format!("{} loader versions", loader))
crate::ErrorKind::NoValueFor(format!("{loader} loader versions"))
})?;
Ok(loaders.manifest)

View File

@@ -162,7 +162,7 @@ pub async fn import_atlauncher(
profile_path: profile_path.to_string(),
};
let backup_name = format!("ATLauncher-{}", instance_folder);
let backup_name = format!("ATLauncher-{instance_folder}");
let minecraft_folder = atlauncher_instance_path;
import_atlauncher_unmanaged(
@@ -190,8 +190,7 @@ async fn import_atlauncher_unmanaged(
let mod_loader: ModLoader = serde_json::from_str::<ModLoader>(&mod_loader)
.map_err(|_| {
crate::ErrorKind::InputError(format!(
"Could not parse mod loader type: {}",
mod_loader
"Could not parse mod loader type: {mod_loader}"
))
})?;

View File

@@ -266,10 +266,7 @@ pub async fn install_zipped_mrpack_files(
emit_loading(
&loading_bar,
30.0 / total_len as f64,
Some(&format!(
"Extracting override {}/{}",
index, total_len
)),
Some(&format!("Extracting override {index}/{total_len}")),
)?;
}
}

View File

@@ -1,7 +1,7 @@
//! Theseus profile management interface
use crate::launcher::get_loader_version_from_profile;
use crate::settings::Hooks;
use crate::state::{LinkedData, ProfileInstallStage};
use crate::state::{LauncherFeatureVersion, LinkedData, ProfileInstallStage};
use crate::util::io::{self, canonicalize};
use crate::{
event::{emit::emit_profile, ProfilePayloadType},
@@ -74,6 +74,7 @@ pub async fn profile_create(
let mut profile = Profile {
path: path.clone(),
install_stage: ProfileInstallStage::NotInstalled,
launcher_feature_version: LauncherFeatureVersion::MOST_RECENT,
name,
icon_path: None,
game_version,

View File

@@ -470,8 +470,7 @@ pub async fn export_mrpack(
state.io_semaphore.0.acquire().await?;
let profile = get(profile_path).await?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to export a nonexistent or unloaded profile at path {}!",
profile_path
"Tried to export a nonexistent or unloaded profile at path {profile_path}!"
))
})?;
@@ -617,8 +616,7 @@ fn pack_get_relative_path(
.strip_prefix(profile_path)
.map_err(|_| {
crate::ErrorKind::FSError(format!(
"Path {path:?} does not correspond to a profile",
path = path
"Path {path:?} does not correspond to a profile"
))
})?
.components()
@@ -656,8 +654,7 @@ pub async fn run_credentials(
let settings = Settings::get(&state.pool).await?;
let profile = get(path).await?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to run a nonexistent or unloaded profile at path {}!",
path
"Tried to run a nonexistent or unloaded profile at path {path}!"
))
})?;
@@ -753,8 +750,7 @@ pub async fn try_update_playtime(path: &str) -> crate::Result<()> {
let profile = get(path).await?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to update playtime for a nonexistent or unloaded profile at path {}!",
path
"Tried to update playtime for a nonexistent or unloaded profile at path {path}!"
))
})?;
let updated_recent_playtime = profile.recent_time_played;

View File

@@ -25,7 +25,7 @@ pub async fn update_managed_modrinth_version(
let unmanaged_err = || {
crate::ErrorKind::InputError(
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
format!("Profile at {profile_path} is not a managed modrinth pack, or has been disconnected."),
)
};
@@ -59,7 +59,7 @@ pub async fn repair_managed_modrinth(profile_path: &str) -> crate::Result<()> {
let unmanaged_err = || {
crate::ErrorKind::InputError(
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
format!("Profile at {profile_path} is not a managed modrinth pack, or has been disconnected."),
)
};

View File

@@ -1,7 +1,10 @@
use crate::data::ModLoader;
use crate::launcher::get_loader_version_from_profile;
use crate::profile::get_full_path;
use crate::state::{server_join_log, Profile, ProfileInstallStage};
use crate::state::attached_world_data::AttachedWorldData;
use crate::state::{
attached_world_data, server_join_log, Profile, ProfileInstallStage,
};
pub use crate::util::server_ping::{
ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion,
};
@@ -11,6 +14,7 @@ use async_walkdir::WalkDir;
use async_zip::{Compression, ZipEntryBuilder};
use chrono::{DateTime, Local, TimeZone, Utc};
use either::Either;
use enumset::{EnumSet, EnumSetType};
use fs4::tokio::AsyncFileExt;
use futures::StreamExt;
use lazy_static::lazy_static;
@@ -42,10 +46,83 @@ pub struct World {
with = "either::serde_untagged_optional"
)]
pub icon: Option<Either<PathBuf, Url>>,
pub display_status: DisplayStatus,
#[serde(flatten)]
pub details: WorldDetails,
}
impl World {
pub fn world_type(&self) -> WorldType {
match self.details {
WorldDetails::Singleplayer { .. } => WorldType::Singleplayer,
WorldDetails::Server { .. } => WorldType::Server,
}
}
pub fn world_id(&self) -> &str {
match &self.details {
WorldDetails::Singleplayer { path, .. } => path,
WorldDetails::Server { address, .. } => address,
}
}
}
#[derive(
Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum WorldType {
#[default]
Singleplayer,
Server,
}
impl WorldType {
pub fn as_str(self) -> &'static str {
match self {
Self::Singleplayer => "singleplayer",
Self::Server => "server",
}
}
pub fn from_string(string: &str) -> Self {
match string {
"singleplayer" => Self::Singleplayer,
"server" => Self::Server,
_ => Self::Singleplayer,
}
}
}
#[derive(Deserialize, Serialize, EnumSetType, Debug, Default)]
#[serde(rename_all = "snake_case")]
#[enumset(serialize_repr = "list")]
pub enum DisplayStatus {
#[default]
Normal,
Hidden,
Favorite,
}
impl DisplayStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Hidden => "hidden",
Self::Favorite => "favorite",
}
}
pub fn from_string(string: &str) -> Self {
match string {
"normal" => Self::Normal,
"hidden" => Self::Hidden,
"favorite" => Self::Favorite,
_ => Self::Normal,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WorldDetails {
@@ -101,7 +178,10 @@ impl From<ServerPackStatus> for Option<bool> {
}
}
pub async fn get_recent_worlds(limit: usize) -> Result<Vec<WorldWithProfile>> {
pub async fn get_recent_worlds(
limit: usize,
display_statuses: EnumSet<DisplayStatus>,
) -> Result<Vec<WorldWithProfile>> {
let state = State::get().await?;
let profiles_dir = state.directories.profiles_dir();
@@ -133,6 +213,9 @@ pub async fn get_recent_worlds(limit: usize) -> Result<Vec<WorldWithProfile>> {
if result.len() >= limit && is_older {
continue;
}
if !display_statuses.contains(world.display_status) {
continue;
}
if is_older {
least_recent_time = world.last_played;
}
@@ -166,6 +249,21 @@ async fn get_all_worlds_in_profile(
get_singleplayer_worlds_in_profile(profile_dir, &mut worlds).await?;
get_server_worlds_in_profile(profile_path, profile_dir, &mut worlds)
.await?;
let state = State::get().await?;
let attached_data =
AttachedWorldData::get_all_for_instance(profile_path, &state.pool)
.await?;
if !attached_data.is_empty() {
for world in worlds.iter_mut() {
if let Some(data) = attached_data
.get(&(world.world_type(), world.world_id().to_owned()))
{
attach_world_data_to_world(world, data);
}
}
}
Ok(worlds)
}
@@ -193,10 +291,25 @@ async fn get_singleplayer_worlds_in_profile(
}
pub async fn get_singleplayer_world(
profile_path: &Path,
instance: &str,
world: &str,
) -> Result<World> {
read_singleplayer_world(get_world_dir(profile_path, world)).await
let state = State::get().await?;
let profile_path = state.directories.profiles_dir().join(instance);
let mut world =
read_singleplayer_world(get_world_dir(&profile_path, world)).await?;
if let Some(data) = AttachedWorldData::get_for_world(
instance,
world.world_type(),
world.world_id(),
&state.pool,
)
.await?
{
attach_world_data_to_world(&mut world, &data);
}
Ok(world)
}
async fn read_singleplayer_world(world_path: PathBuf) -> Result<World> {
@@ -252,6 +365,7 @@ async fn read_singleplayer_world_maybe_locked(
name: level_data.level_name,
last_played: Utc.timestamp_millis_opt(level_data.last_played).single(),
icon: icon.map(Either::Left),
display_status: DisplayStatus::Normal,
details: WorldDetails::Singleplayer {
path: world_path
.file_name()
@@ -286,7 +400,7 @@ async fn get_server_worlds_in_profile(
continue;
}
let icon = server.icon.and_then(|icon| {
Url::parse(&format!("data:image/png;base64,{}", icon)).ok()
Url::parse(&format!("data:image/png;base64,{icon}")).ok()
});
let last_played = join_log
.as_ref()
@@ -299,6 +413,7 @@ async fn get_server_worlds_in_profile(
name: server.name,
last_played,
icon: icon.map(Either::Right),
display_status: DisplayStatus::Normal,
details: WorldDetails::Server {
index,
address: server.ip,
@@ -311,6 +426,28 @@ async fn get_server_worlds_in_profile(
Ok(())
}
fn attach_world_data_to_world(world: &mut World, data: &AttachedWorldData) {
world.display_status = data.display_status;
}
pub async fn set_world_display_status(
instance: &str,
world_type: WorldType,
world_id: &str,
display_status: DisplayStatus,
) -> Result<()> {
let state = State::get().await?;
attached_world_data::set_display_status(
instance,
world_type,
world_id,
display_status,
&state.pool,
)
.await?;
Ok(())
}
pub async fn rename_world(
instance: &Path,
world: &str,
@@ -365,7 +502,7 @@ pub async fn backup_world(instance: &Path, world: &str) -> Result<u64> {
let name_base = {
let now = Local::now();
let formatted_time = now.format("%Y-%m-%d_%H-%M-%S");
format!("{}_{}", formatted_time, world)
format!("{formatted_time}_{world}")
};
let output_path =
backups_dir.join(find_available_name(&backups_dir, &name_base, ".zip"));
@@ -671,8 +808,7 @@ pub async fn get_profile_protocol_version(
) -> Result<Option<i32>> {
let mut profile = super::profile::get(profile).await?.ok_or_else(|| {
ErrorKind::UnmanagedProfileError(format!(
"Could not find profile {}",
profile
"Could not find profile {profile}"
))
})?;
if profile.install_stage != ProfileInstallStage::Installed {
@@ -809,22 +945,21 @@ async fn resolve_server_address(
return Ok((host.to_owned(), port));
}
let resolver = hickory_resolver::TokioResolver::builder_tokio()?.build();
Ok(match resolver
.srv_lookup(format!("_minecraft._tcp.{}", host))
.await
{
Err(e)
if e.proto()
.filter(|x| x.kind().is_no_records_found())
.is_some() =>
{
None
Ok(
match resolver.srv_lookup(format!("_minecraft._tcp.{host}")).await {
Err(e)
if e.proto()
.filter(|x| x.kind().is_no_records_found())
.is_some() =>
{
None
}
Err(e) => return Err(e.into()),
Ok(lookup) => lookup
.into_iter()
.next()
.map(|r| (r.target().to_string(), r.port())),
}
Err(e) => return Err(e.into()),
Ok(lookup) => lookup
.into_iter()
.next()
.map(|r| (r.target().to_string(), r.port())),
}
.unwrap_or_else(|| (host.to_owned(), port)))
.unwrap_or_else(|| (host.to_owned(), port)),
)
}