OneLauncher-Cracked/packages/oneclient_app/src/view/app/settings/storage.rs
2026-08-08 11:33:24 +07:00

327 lines
11 KiB
Rust

use freya::prelude::*;
use oneclient_core::storage::{ReclaimableEntry, StorageEntry, StorageReport, format_bytes};
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 crate::theme::colors;
/// Rows narrower than this would render as a sliver so they get a floor
const MIN_BAR_FRACTION: f32 = 0.015;
#[derive(PartialEq)]
pub struct SettingsStorage;
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 Some(report) = try_storage_report(&report_query) else {
return settings_page()
.child(hero_placeholder())
.into_element();
};
let refresh = Button::new()
.secondary()
.small()
.on_press(move |_| {
report_query.invalidate();
})
.child(label().text("Refresh"));
let mut page = settings_page()
.child(hero(&report, refresh.into_element()))
.child(section_header("FREE UP SPACE"))
.child(
ReclaimRow {
icon: IconType::FileX02,
title: "Unreferenced cache files",
description: unused_cache_description(&report.unreferenced_cache),
action: StorageAction::CleanUnreferencedCache,
empty: report.unreferenced_cache.is_empty(),
}
.into_element(),
)
.child(
ReclaimRow {
icon: IconType::Trash01,
title: "Leftover cluster content",
description: legacy_content_description(&report.legacy_cluster_content),
action: StorageAction::CleanLegacyClusterContent,
empty: report.legacy_cluster_content.is_empty(),
}
.into_element(),
);
page = page.child(section_header("WHAT'S USING SPACE"));
page = match largest(&report.categories) {
Some(largest) => {
let mut list = page;
for entry in report.categories.iter().filter(|e| e.bytes > 0) {
list = list.child(usage_row(entry, largest, colors::brand()));
}
list
}
None => page.child(empty_note("Nothing stored yet.")),
};
page = page.child(section_header("CLUSTERS"));
page = match largest(&report.clusters) {
Some(largest) => {
let mut list = page;
for entry in report.clusters.iter().filter(|e| e.bytes > 0) {
list = list.child(usage_row(entry, largest, colors::fg_secondary()));
}
list
}
None => page.child(empty_note("No cluster is using any space.")),
};
page.child(footnote()).into_element()
}
}
fn hero(report: &StorageReport, refresh: Element) -> impl IntoElement {
let reclaimable = report.unreferenced_cache.bytes + report.legacy_cluster_content.bytes;
let subtitle = if reclaimable > 0 {
format!("{} can be freed", format_bytes(reclaimable))
} else {
"Nothing to free up".to_string()
};
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.cross_align(Alignment::Center)
.spacing(16.)
.padding(Gaps::new_symmetric(20., 16.))
.corner_radius(CornerRadius::new_all(12.))
.background(colors::page_elevated())
.child(
rect()
.vertical()
.width(Size::flex(1.0))
.spacing(2.)
.child(
label()
.text(format_bytes(report.total_bytes))
.font_size(28.)
.font_weight(FontWeight::BOLD)
.color(colors::fg_primary()),
)
.child(
label()
.text(subtitle)
.font_size(12.)
.color(if reclaimable > 0 {
colors::brand()
} else {
colors::fg_secondary()
}),
),
)
.child(refresh)
.into_element()
}
fn hero_placeholder() -> impl IntoElement {
rect()
.width(Size::fill())
.padding(Gaps::new_symmetric(20., 16.))
.corner_radius(CornerRadius::new_all(12.))
.background(colors::page_elevated())
.child(
label()
.text("Measuring disk usage…")
.font_size(16.)
.color(colors::fg_secondary()),
)
.into_element()
}
/// A component rather than a function because it owns a hook each instance gets its own scope so a button tracks only its own progress
#[derive(PartialEq)]
struct ReclaimRow {
icon: IconType,
title: &'static str,
description: String,
action: StorageAction,
empty: bool,
}
impl Component for ReclaimRow {
fn render(&self) -> impl IntoElement {
let action = use_storage_action();
let running = mutation_is_running(&action);
let kind = self.action;
let button = Button::new()
.secondary()
.small()
.disabled(self.empty || running)
.on_press(move |_| {
action.mutate(kind);
})
.child(label().text(if running { "Cleaning…" } else { "Clean up" }));
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.cross_align(Alignment::Center)
.spacing(16.)
.padding(Gaps::new_symmetric(12., 16.))
.corner_radius(CornerRadius::new_all(12.))
.background(colors::page_elevated())
.maybe(self.empty, |el| el.opacity(0.55))
.child(Icon::new(self.icon))
.child(
rect()
.vertical()
.width(Size::flex(1.0))
.spacing(2.)
.child(
label()
.text(self.title)
.font_size(16.)
.font_weight(FontWeight::MEDIUM)
.color(colors::fg_primary()),
)
.child(
label()
.text(self.description.clone())
.font_size(12.)
.color(colors::fg_secondary()),
),
)
.child(button)
.into_element()
}
}
fn usage_row(entry: &StorageEntry, largest: u64, bar: Color) -> impl IntoElement {
let fraction = if largest == 0 {
0.0
} else {
(entry.bytes as f32 / largest as f32).max(MIN_BAR_FRACTION)
};
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.cross_align(Alignment::Center)
.spacing(16.)
.padding(Gaps::new_symmetric(12., 16.))
.corner_radius(CornerRadius::new_all(12.))
.background(colors::page_elevated())
.child(
rect()
.vertical()
.width(Size::flex(1.0))
.spacing(6.)
.child(
label()
.text(entry.label.clone())
.font_size(14.)
.font_weight(FontWeight::MEDIUM)
.color(colors::fg_primary()),
)
.child(proportion_bar(fraction, bar)),
)
.child(
label()
.text(format_bytes(entry.bytes))
.font_size(13.)
.color(colors::fg_secondary()),
)
.child(open_folder_button(entry.path.clone()))
.into_element()
}
fn proportion_bar(fraction: f32, fill: Color) -> impl IntoElement {
rect()
.width(Size::fill())
.height(Size::px(4.))
.corner_radius(CornerRadius::new_all(2.))
.background(colors::component_bg())
.child(
rect()
.width(Size::percent(fraction.clamp(0.0, 1.0) * 100.))
.height(Size::fill())
.corner_radius(CornerRadius::new_all(2.))
.background(fill),
)
.into_element()
}
fn largest(entries: &[StorageEntry]) -> Option<u64> {
entries.iter().map(|e| e.bytes).max().filter(|max| *max > 0)
}
fn unused_cache_description(entry: &ReclaimableEntry) -> String {
if entry.is_empty() {
return "Nothing here — every cached file belongs to an installed package.".to_string();
}
format!(
"{} across {} file{} that no installed package points at.",
format_bytes(entry.bytes),
entry.files,
plural(entry.files)
)
}
fn legacy_content_description(entry: &ReclaimableEntry) -> String {
if entry.is_empty() {
return "Nothing here — no cluster folder is holding old content.".to_string();
}
format!(
"{} across {} file{} left in cluster folders by an older version. Already ignored when \
you play; this just reclaims the space.",
format_bytes(entry.bytes),
entry.files,
plural(entry.files)
)
}
fn empty_note(text: &'static str) -> impl IntoElement {
rect()
.width(Size::fill())
.padding(Gaps::new_symmetric(14., 16.))
.corner_radius(CornerRadius::new_all(12.))
.background(colors::page_elevated())
.child(
label()
.text(text)
.font_size(13.)
.color(colors::fg_secondary()),
)
.into_element()
}
fn footnote() -> impl IntoElement {
rect()
.padding(Gaps::new(16., 4., 8., 4.))
.child(
label()
.text(
"Packages live in a shared cache and are copied into the game folder only \
while you play. Removing a package frees its cache entry automatically.",
)
.font_size(12.)
.color(colors::fg_secondary()),
)
.into_element()
}
fn plural(count: usize) -> &'static str {
if count == 1 { "" } else { "s" }
}