feat: shared loading state + cleanup loading state management (#5835)

* feat: implement shared loading bar component and polished loading states across the app

* feat: align loading states + ensureQueryData changes

* fix: lint + bugs

* fix: skeleton for manage servers page

* fix: merge conflict fix
This commit is contained in:
Calum H.
2026-04-18 19:46:39 +01:00
committed by GitHub
parent 3e32901737
commit 176d4301c3
47 changed files with 2063 additions and 1371 deletions

View File

@@ -31,6 +31,7 @@ use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::time::Instant;
use tokio::io::AsyncWriteExt;
use tokio::task::JoinSet;
use tokio_util::compat::FuturesAsyncWriteCompatExt;
use url::Url;
@@ -284,15 +285,24 @@ async fn get_singleplayer_worlds_in_profile(
if !saves_dir.exists() {
return Ok(());
}
let mut saves_dir = io::read_dir(saves_dir).await?;
while let Some(world_dir) = saves_dir.next_entry().await? {
let mut entries = io::read_dir(&saves_dir).await?;
let mut tasks = JoinSet::new();
while let Some(world_dir) = entries.next_entry().await? {
let world_path = world_dir.path();
let level_dat_path = world_path.join("level.dat");
if !level_dat_path.exists() {
if !world_path.join("level.dat").exists() {
continue;
}
if let Ok(world) = read_singleplayer_world(world_path).await {
worlds.push(world);
tasks.spawn(read_singleplayer_world(world_path));
}
while let Some(result) = tasks.join_next().await {
match result {
Ok(Ok(world)) => worlds.push(world),
Ok(Err(e)) => {
tracing::warn!("Skipping unreadable world: {e}");
}
Err(e) => {
tracing::warn!("World read task panicked: {e}");
}
}
}
@@ -333,36 +343,36 @@ async fn read_singleplayer_world_maybe_locked(
world_path: PathBuf,
locked: bool,
) -> Result<World> {
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct LevelDataRoot {
data: LevelData,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct LevelData {
#[serde(default)]
level_name: String,
#[serde(default)]
last_played: i64,
#[serde(default)]
game_type: i32,
#[serde(default, rename = "hardcore")]
hardcore: bool,
}
let level_data = io::read(world_path.join("level.dat")).await?;
let level_data: LevelDataRoot = quartz_nbt::serde::deserialize(
&level_data,
let raw = io::read(world_path.join("level.dat")).await?;
let (root, _) = quartz_nbt::io::read_nbt(
&mut Cursor::new(raw),
quartz_nbt::io::Flavor::GzCompressed,
)?
.0;
let level_data = level_data.data;
)?;
let icon = Some(world_path.join("icon.png")).filter(|i| i.exists());
let data = root.get::<_, &NbtCompound>("Data").map_err(|_| {
Error::from(ErrorKind::InputError(
"Missing Data tag in level.dat".into(),
))
})?;
let game_mode = match level_data.game_type {
let level_name = data
.get::<_, &str>("LevelName")
.unwrap_or_default()
.to_string();
let last_played = data.get::<_, i64>("LastPlayed").unwrap_or(0);
let game_type = data.get::<_, i32>("GameType").unwrap_or(0);
let hardcore = data.get::<_, i8>("hardcore").unwrap_or(0) != 0;
let icon = if tokio::fs::try_exists(world_path.join("icon.png"))
.await
.unwrap_or(false)
{
Some(Either::Left(world_path.join("icon.png")))
} else {
None
};
let game_mode = match game_type {
0 => SingleplayerGameMode::Survival,
1 => SingleplayerGameMode::Creative,
2 => SingleplayerGameMode::Adventure,
@@ -371,9 +381,9 @@ async fn read_singleplayer_world_maybe_locked(
};
Ok(World {
name: level_data.level_name,
last_played: Utc.timestamp_millis_opt(level_data.last_played).single(),
icon: icon.map(Either::Left),
name: level_name,
last_played: Utc.timestamp_millis_opt(last_played).single(),
icon,
display_status: DisplayStatus::Normal,
details: WorldDetails::Singleplayer {
path: world_path
@@ -382,7 +392,7 @@ async fn read_singleplayer_world_maybe_locked(
.to_string_lossy()
.to_string(),
game_mode,
hardcore: level_data.hardcore,
hardcore,
locked,
},
})