You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
))
|
||||
})?;
|
||||
|
||||
|
||||
@@ -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}")),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."),
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user