Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ sysinfo = {version = "=0.39.5"}
trash = {version = "=5.2.6"}
url = {version = "=2.5.8", features = ["serde"]}
uuid = {version = "=1.23.1", features = ["serde", "v4"]}
windows-sys = {version = "=0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem"]}

# util (codegen/macro)
anyhow = {version = "=1.0.102"}
Expand Down
22 changes: 20 additions & 2 deletions packages/oneclient_app/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use tokio::sync::mpsc;

use crate::hooks::PumpSignal;
use crate::notifications::{MESSAGE_TOAST_TTL, PendingPromptView};
use crate::state::{AppChannel, AppState, LoginProgress};
use crate::state::{AppChannel, AppState, LoginProgress, StorageScanProgress};

/// Quiet period before log lines are written without it every line wakes the log view
const GAME_LOG_FLUSH: Duration = Duration::from_millis(120);
Expand Down Expand Up @@ -127,6 +127,7 @@ impl EventPump {
let mut logs: Vec<(i64, String)> = Vec::new();
let mut failed: Option<(i64, String)> = None;
let mut login: Option<Option<LoginProgress>> = None;
let mut storage_scan: Option<Option<StorageScanProgress>> = None;
let mut sync_complete = false;

for event in batch {
Expand All @@ -148,7 +149,6 @@ impl EventPump {
cluster_id,
message,
}) => failed = Some((cluster_id, message)),
// Lifted out so it never reaches the engine the sign-in modal renders it inline
Event::Progress(ProgressEvent::Update {
id,
ref label,
Expand All @@ -161,6 +161,18 @@ impl EventPump {
total,
}));
}
Event::Progress(ProgressEvent::Update {
id,
ref label,
current,
total,
}) if id == oneclient_core::storage::STORAGE_SCAN_PROGRESS => {
storage_scan = Some((current < total).then(|| StorageScanProgress {
label: label.clone(),
current,
total,
}));
}
other => engine_events.push(other),
}
}
Expand Down Expand Up @@ -197,6 +209,12 @@ impl EventPump {
.microsoft_login = progress;
}

if let Some(progress) = storage_scan {
self.station
.write_channel(AppChannel::StorageScan)
.storage_scan = progress;
}

if !engine_events.is_empty() {
folded.touched_engine = true;
let mut guard = self.station.write_channel(AppChannel::Notifications);
Expand Down
8 changes: 8 additions & 0 deletions packages/oneclient_app/src/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub use queries::{
use crate::notifications::NotificationSnapshot;
use crate::state::{
AppChannel, GameState, InstallState, LauncherInit, LoginProgress, SettingsState,
StorageScanProgress,
};
use freya::prelude::*;
use freya::radio::use_radio;
Expand Down Expand Up @@ -107,6 +108,13 @@ pub fn use_installs_snapshot() -> InstallState {
use_radio(AppChannel::Installs).read().installs.clone()
}

pub fn use_storage_scan_progress() -> Option<StorageScanProgress> {
use_radio(AppChannel::StorageScan)
.read()
.storage_scan
.clone()
}

pub fn use_microsoft_login_status() -> Option<LoginProgress> {
use_radio(AppChannel::MicrosoftLogin)
.read()
Expand Down
4 changes: 3 additions & 1 deletion packages/oneclient_app/src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,11 @@ impl NotificationState {
);
self.push_ephemeral_toast(entry_id, MESSAGE_TOAST_TTL);
}
// The sign-in modal renders this progress itself it must not also become a toast

Event::Progress(ProgressEvent::Update { id, .. })
if id == oneclient_auth::MICROSOFT_LOGIN_PROGRESS => {}
Event::Progress(ProgressEvent::Update { id, .. })
if id == oneclient_core::storage::STORAGE_SCAN_PROGRESS => {}
Event::Progress(ProgressEvent::Update {
id,
label,
Expand Down
9 changes: 9 additions & 0 deletions packages/oneclient_app/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum AppChannel {
AccountSwitcher,
MicrosoftLogin,
Installs,
StorageScan,
}

impl RadioChannel<AppState> for AppChannel {}
Expand All @@ -40,6 +41,7 @@ pub struct AppState {
pub account_switcher_open: bool,
pub microsoft_login: Option<LoginProgress>,
pub installs: InstallState,
pub storage_scan: Option<StorageScanProgress>,
}

/// In-flight installs so the button that started one stays disabled until it lands
Expand Down Expand Up @@ -103,6 +105,13 @@ pub struct LoginProgress {
pub total: u64,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StorageScanProgress {
pub label: String,
pub current: u64,
pub total: u64,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct GameState {
pub stages: HashMap<i64, LaunchStage>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,11 @@ impl Component for PackageManager {
cluster_id,
package_type,
))
.maybe_child(
content_type
.is_global()
.then(|| views::global_notice(noun_plural)),
)
.maybe_child(session_live.then(|| views::running_notice(noun_plural)))
.child(ContentBox::new(
filtered,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,19 @@ pub(super) fn toolbar_bar(
.into_element()
}

/// Enabling still stores and applies at the next launch but the running session cannot pick it up
pub(super) fn running_notice(noun_plural: &'static str) -> Element {
notice_bar(format!(
"Minecraft is running. Changes to your {noun_plural} are saved, and take effect the next time you launch this version."
))
}

pub(super) fn global_notice(noun_plural: &'static str) -> Element {
notice_bar(format!(
"These {noun_plural} are shared across all your clusters. Adding one here makes it available everywhere, and turning one off removes it everywhere."
))
}

fn notice_bar(text: String) -> Element {
rect()
.horizontal()
.width(Size::fill())
Expand All @@ -233,9 +244,7 @@ pub(super) fn running_notice(noun_plural: &'static str) -> Element {
)
.child(
label()
.text(format!(
"Minecraft is running. Changes to your {noun_plural} are saved, and take effect the next time you launch this version."
))
.text(text)
.font_size(12.)
.width(Size::flex(1.0))
.color(colors::fg_secondary()),
Expand Down
59 changes: 48 additions & 11 deletions packages/oneclient_app/src/view/app/settings/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use super::{section_header, settings_page};
use crate::components::{Button, Icon, IconType, open_folder_button};
use crate::hooks::{
StorageAction, mutation_is_running, try_storage_report, use_storage_action, use_storage_report,
use_storage_scan_progress,
};
use crate::state::StorageScanProgress;
use crate::theme::colors;

/// Rows narrower than this would render as a sliver so they get a floor
Expand All @@ -18,10 +20,11 @@ impl Component for SettingsStorage {
fn render(&self) -> impl IntoElement {
// Every hook before any early return the report is absent on the first render and a later-only hook would change the hook order
let report_query = use_storage_report();
let scan = use_storage_scan_progress();

let Some(report) = try_storage_report(&report_query) else {
return settings_page()
.child(hero_placeholder())
.child(scan_card(scan.as_ref()))
.into_element();
};

Expand All @@ -33,8 +36,13 @@ impl Component for SettingsStorage {
})
.child(label().text("Refresh"));

let mut page = settings_page()
.child(hero(&report, refresh.into_element()))
let mut page = settings_page().child(hero(&report, refresh.into_element()));

if scan.is_some() {
page = page.child(scan_card(scan.as_ref()));
}

page = page
.child(section_header("FREE UP SPACE"))
.child(
ReclaimRow {
Expand Down Expand Up @@ -130,18 +138,47 @@ fn hero(report: &StorageReport, refresh: Element) -> impl IntoElement {
.into_element()
}

fn hero_placeholder() -> impl IntoElement {
rect()
// the first-load placeholder and the strip shown while a refresh rescans
fn scan_card(scan: Option<&StorageScanProgress>) -> impl IntoElement {
let counting = scan.filter(|scan| scan.total > 0);
let fraction = counting.map_or(0.0, |scan| scan.current as f32 / scan.total as f32);

let mut header = rect()
.horizontal()
.width(Size::fill())
.padding(Gaps::new_symmetric(20., 16.))
.corner_radius(CornerRadius::new_all(12.))
.background(colors::page_elevated())
.content(Content::Flex)
.cross_align(Alignment::Center)
.spacing(12.)
.child(
rect().width(Size::flex(1.0)).child(
label()
.text(scan.map_or_else(
|| "Measuring disk usage…".to_string(),
|scan| format!("{}…", scan.label),
))
.font_size(14.)
.color(colors::fg_secondary()),
),
);

if let Some(scan) = counting {
header = header.child(
label()
.text("Measuring disk usage…")
.font_size(16.)
.text(format!("{} / {}", scan.current, scan.total))
.font_size(12.)
.color(colors::fg_secondary()),
)
);
}

rect()
.vertical()
.width(Size::fill())
.spacing(10.)
.padding(Gaps::new_symmetric(16., 16.))
.corner_radius(CornerRadius::new_all(12.))
.background(colors::page_elevated())
.child(header)
.child(proportion_bar(fraction, colors::brand()))
.into_element()
}

Expand Down
17 changes: 17 additions & 0 deletions packages/oneclient_cluster/src/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ use crate::stage::ClusterStage;

pub use oneclient_common::paths::DEDICATED_MARKER;

// takes a cluster out of the shared `mods` folder
pub async fn remove_mods_link(folder_name: &str) {
let Ok(link) = paths::shared_mods_link(folder_name) else {
return;
};

match polyio::symlink_metadata(&link).await {
Ok(meta) if meta.file_type().is_symlink() => {
if let Err(err) = polyio::remove_symlink_dir(&link).await {
tracing::warn!(folder = folder_name, error = %err, "failed to clear cluster mods link");
}
}

_ => {}
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cluster {
pub id: ClusterId,
Expand Down
2 changes: 1 addition & 1 deletion packages/oneclient_cluster/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub mod logs;
pub mod profiles;
pub mod screenshots;

pub use cluster::{Cluster, ClusterLinkTarget};
pub use cluster::{Cluster, ClusterLinkTarget, remove_mods_link};
pub use error::{ClusterError, ClusterResult};
pub use manager::ClusterManager;
pub use options::{ClusterUpdate, CreateClusterOptions};
Expand Down
4 changes: 3 additions & 1 deletion packages/oneclient_cluster/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::error::ClusterResult;
use oneclient_db::DbPool;
use tokio::sync::Mutex;

use crate::cluster::Cluster;
use crate::cluster::{Cluster, remove_mods_link};
use crate::error::ClusterError;
use crate::options::{ClusterUpdate, CreateClusterOptions};
use crate::stage::ClusterStage;
Expand Down Expand Up @@ -160,6 +160,8 @@ impl ClusterManager {
return Err(ClusterError::NotFound(cluster_id));
}

remove_mods_link(&cluster.folder_name).await;

if remove_files {
let path = cluster.dir()?;
if path.exists() {
Expand Down
6 changes: 6 additions & 0 deletions packages/oneclient_common/src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ impl ContentType {
}
}

// installed once for the whole launcher rather than per cluster
#[must_use]
pub const fn is_global(self) -> bool {
matches!(self, Self::ResourcePack | Self::Shader)
}

pub fn from_folder_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"mods" | "mod" => Some(Self::Mod),
Expand Down
16 changes: 16 additions & 0 deletions packages/oneclient_common/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,22 @@ pub fn cluster_game_dir(folder_name: &str) -> PathsResult<PathBuf> {
}
}

pub fn cluster_mods_dir(folder_name: &str) -> PathsResult<PathBuf> {
Ok(cluster_dir(folder_name)?.join(ContentType::Mod.folder_name()))
}

pub fn shared_mods_dir() -> PathsResult<PathBuf> {
Ok(shared_minecraft_dir()?.join(ContentType::Mod.folder_name()))
}

pub fn global_content_dir(content_type: ContentType) -> PathsResult<PathBuf> {
Ok(shared_minecraft_dir()?.join(content_type.folder_name()))
}

pub fn shared_mods_link(folder_name: &str) -> PathsResult<PathBuf> {
Ok(shared_mods_dir()?.join(folder_name))
}

pub fn packages_cache_dir() -> PathsResult<PathBuf> {
Ok(launcher_dir()?.join("metadata").join("packages"))
}
Expand Down
Loading
Loading