mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
added settings for this device + quota in settings
This commit is contained in:
1 parent
bf625bf8d9
commit
a3d9d05777
7 files changed
+205
No files matched your search
@@ -310,6 +310,8 @@ enum Command {
|
|||||||
#[arg(short, long, default_value_t = 50)]
|
#[arg(short, long, default_value_t = 50)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
},
|
},
|
||||||
|
/// Show account storage usage (used of total, across all Proton products).
|
||||||
|
Quota,
|
||||||
/// Inspect and maintain the local metadata database and content cache.
|
/// Inspect and maintain the local metadata database and content cache.
|
||||||
Cache {
|
Cache {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@@ -547,6 +549,7 @@ fn main() -> Result<()> {
|
|||||||
Command::Invitations { action } => cmd_invitations(action),
|
Command::Invitations { action } => cmd_invitations(action),
|
||||||
Command::Bookmarks { action } => cmd_bookmarks(action),
|
Command::Bookmarks { action } => cmd_bookmarks(action),
|
||||||
Command::Activity { limit } => cmd_activity(limit),
|
Command::Activity { limit } => cmd_activity(limit),
|
||||||
|
Command::Quota => cmd_quota(),
|
||||||
Command::Cache { action } => cmd_cache(action),
|
Command::Cache { action } => cmd_cache(action),
|
||||||
Command::Diagnose => cmd_diagnose(),
|
Command::Diagnose => cmd_diagnose(),
|
||||||
}
|
}
|
||||||
@@ -925,6 +928,35 @@ fn cmd_shared_get(uid: String, dest: Option<PathBuf>) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_quota() -> Result<()> {
|
||||||
|
let response = control_request(CtlRequest::AccountQuota)?;
|
||||||
|
if emit_json(&response)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match response {
|
||||||
|
CtlResponse::AccountQuota {
|
||||||
|
max_space,
|
||||||
|
used_space,
|
||||||
|
} => {
|
||||||
|
let used = used_space.max(0) as u64;
|
||||||
|
if max_space > 0 {
|
||||||
|
let total = max_space as u64;
|
||||||
|
let pct = (used as f64 / total as f64 * 100.0).round() as u64;
|
||||||
|
println!(
|
||||||
|
"{} of {} used ({pct}%)",
|
||||||
|
human_bytes(used),
|
||||||
|
human_bytes(total)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("{} used", human_bytes(used));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CtlResponse::Error { message, kind } => bail!("{}", cli_error(kind, &message)),
|
||||||
|
other => bail!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn cmd_activity(limit: usize) -> Result<()> {
|
fn cmd_activity(limit: usize) -> Result<()> {
|
||||||
let response = control_request(CtlRequest::ListActivity { limit })?;
|
let response = control_request(CtlRequest::ListActivity { limit })?;
|
||||||
if emit_json(&response)? {
|
if emit_json(&response)? {
|
||||||
|
|||||||
@@ -307,6 +307,11 @@ pub enum Request {
|
|||||||
/// Remove a saved bookmark by its `token`. Replies with [`Response::Ok`].
|
/// Remove a saved bookmark by its `token`. Replies with [`Response::Ok`].
|
||||||
DeleteBookmark { token: String },
|
DeleteBookmark { token: String },
|
||||||
|
|
||||||
|
// ---- account ----------------------------------------------------------
|
||||||
|
/// Total account storage usage (used/total, all Proton products, not just
|
||||||
|
/// Drive). Replies with [`Response::AccountQuota`].
|
||||||
|
AccountQuota,
|
||||||
|
|
||||||
// ---- activity ---------------------------------------------------------
|
// ---- activity ---------------------------------------------------------
|
||||||
/// Fetch the daemon's recent activity log, newest first, capped at `limit`
|
/// Fetch the daemon's recent activity log, newest first, capped at `limit`
|
||||||
/// entries. Replies with [`Response::Activity`]. The log is persisted, so it
|
/// entries. Replies with [`Response::Activity`]. The log is persisted, so it
|
||||||
@@ -970,6 +975,11 @@ pub enum Response {
|
|||||||
Invitations { items: Vec<InvitationInfo> },
|
Invitations { items: Vec<InvitationInfo> },
|
||||||
/// Saved public links (reply to [`Request::ListBookmarks`]).
|
/// Saved public links (reply to [`Request::ListBookmarks`]).
|
||||||
Bookmarks { items: Vec<BookmarkInfo> },
|
Bookmarks { items: Vec<BookmarkInfo> },
|
||||||
|
/// Account storage usage in bytes (reply to [`Request::AccountQuota`]).
|
||||||
|
/// `max_space` is the total; `used_space` what is consumed. `max_space == 0`
|
||||||
|
/// means the total is unknown, which a front-end shows as a plain used figure
|
||||||
|
/// rather than a full bar.
|
||||||
|
AccountQuota { max_space: i64, used_space: i64 },
|
||||||
/// Nodes I have shared with others (reply to [`Request::ListSharedByMe`]).
|
/// Nodes I have shared with others (reply to [`Request::ListSharedByMe`]).
|
||||||
SharedByMe { items: Vec<SharedItem> },
|
SharedByMe { items: Vec<SharedItem> },
|
||||||
/// The daemon's recent activity, newest first (reply to
|
/// The daemon's recent activity, newest first (reply to
|
||||||
@@ -1283,6 +1293,7 @@ mod tests {
|
|||||||
Request::DeleteBookmark {
|
Request::DeleteBookmark {
|
||||||
token: "tok".into(),
|
token: "tok".into(),
|
||||||
},
|
},
|
||||||
|
Request::AccountQuota,
|
||||||
];
|
];
|
||||||
for req in reqs {
|
for req in reqs {
|
||||||
let line = serde_json::to_string(&req).unwrap();
|
let line = serde_json::to_string(&req).unwrap();
|
||||||
|
|||||||
@@ -680,6 +680,13 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
|||||||
Ok(entries) => CtlResponse::Entries { entries },
|
Ok(entries) => CtlResponse::Entries { entries },
|
||||||
Err(e) => CtlResponse::error(e),
|
Err(e) => CtlResponse::error(e),
|
||||||
},
|
},
|
||||||
|
Ok(CtlRequest::AccountQuota) => match core.account_quota() {
|
||||||
|
Ok((max_space, used_space)) => CtlResponse::AccountQuota {
|
||||||
|
max_space,
|
||||||
|
used_space,
|
||||||
|
},
|
||||||
|
Err(e) => CtlResponse::error(e),
|
||||||
|
},
|
||||||
Ok(CtlRequest::ListSharedFolder { uid }) => match core.list_shared_folder(&uid) {
|
Ok(CtlRequest::ListSharedFolder { uid }) => match core.list_shared_folder(&uid) {
|
||||||
Ok(entries) => CtlResponse::Entries { entries },
|
Ok(entries) => CtlResponse::Entries { entries },
|
||||||
Err(e) => CtlResponse::error(e),
|
Err(e) => CtlResponse::error(e),
|
||||||
|
|||||||
@@ -392,6 +392,19 @@ impl Core {
|
|||||||
.map_err(|e| CoreError::from_api(&e, "delete bookmark"))
|
.map_err(|e| CoreError::from_api(&e, "delete bookmark"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- account ----------------------------------------------------------
|
||||||
|
|
||||||
|
/// Total account storage usage `(max_space, used_space)` in bytes, across all
|
||||||
|
/// Proton products (not Drive-only). A remote round-trip; nothing here is
|
||||||
|
/// cached.
|
||||||
|
pub(crate) fn account_quota(&self) -> CoreResult<(i64, i64)> {
|
||||||
|
let q = self
|
||||||
|
.rt
|
||||||
|
.block_on(self.client.quota())
|
||||||
|
.map_err(|e| CoreError::from_api(&e, "account quota"))?;
|
||||||
|
Ok((q.max_space, q.used_space))
|
||||||
|
}
|
||||||
|
|
||||||
// ---- shared by me -----------------------------------------------------
|
// ---- shared by me -----------------------------------------------------
|
||||||
|
|
||||||
/// List the nodes I have shared with others, each with a summary of its share
|
/// List the nodes I have shared with others, each with a summary of its share
|
||||||
|
|||||||
@@ -400,6 +400,11 @@ fn build_window(app: &adw::Application) {
|
|||||||
transfers_inflight: Cell::new(false),
|
transfers_inflight: Cell::new(false),
|
||||||
cache_bar: main_widgets.cache_bar.clone(),
|
cache_bar: main_widgets.cache_bar.clone(),
|
||||||
cache_label: main_widgets.cache_label.clone(),
|
cache_label: main_widgets.cache_label.clone(),
|
||||||
|
quota_group: main_widgets.quota_group.clone(),
|
||||||
|
quota_bar: main_widgets.quota_bar.clone(),
|
||||||
|
quota_label: main_widgets.quota_label.clone(),
|
||||||
|
quota_inflight: Cell::new(false),
|
||||||
|
quota_checked_at: Cell::new(None),
|
||||||
autostart_row: main_widgets.autostart_row.clone(),
|
autostart_row: main_widgets.autostart_row.clone(),
|
||||||
budget_row: main_widgets.budget_row.clone(),
|
budget_row: main_widgets.budget_row.clone(),
|
||||||
mountpoint_row: main_widgets.mountpoint_row.clone(),
|
mountpoint_row: main_widgets.mountpoint_row.clone(),
|
||||||
@@ -501,6 +506,8 @@ fn build_window(app: &adw::Application) {
|
|||||||
rows: RefCell::new(Vec::new()),
|
rows: RefCell::new(Vec::new()),
|
||||||
sync_group: devices_widgets.sync_group.clone(),
|
sync_group: devices_widgets.sync_group.clone(),
|
||||||
sync_rows: RefCell::new(Vec::new()),
|
sync_rows: RefCell::new(Vec::new()),
|
||||||
|
rename_this: devices_widgets.rename_this.clone(),
|
||||||
|
this_device: RefCell::new(None),
|
||||||
inflight: Cell::new(false),
|
inflight: Cell::new(false),
|
||||||
loaded_at: Cell::new(None),
|
loaded_at: Cell::new(None),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ pub(crate) struct DevicesState {
|
|||||||
pub(crate) rows: RefCell<Vec<gtk4::Widget>>,
|
pub(crate) rows: RefCell<Vec<gtk4::Widget>>,
|
||||||
pub(crate) sync_group: adw::PreferencesGroup,
|
pub(crate) sync_group: adw::PreferencesGroup,
|
||||||
pub(crate) sync_rows: RefCell<Vec<gtk4::Widget>>,
|
pub(crate) sync_rows: RefCell<Vec<gtk4::Widget>>,
|
||||||
|
/// "Rename" action in the "This computer" header. Insensitive until the
|
||||||
|
/// device list identifies this machine's own device.
|
||||||
|
pub(crate) rename_this: gtk4::Button,
|
||||||
|
/// This machine's own device `(uid, name)`, from the last device list. The
|
||||||
|
/// current device is filtered out of the "Other computers" rows, so this is
|
||||||
|
/// where its rename target lives. `None` until identified.
|
||||||
|
pub(crate) this_device: RefCell<Option<(String, String)>>,
|
||||||
pub(crate) inflight: Cell<bool>,
|
pub(crate) inflight: Cell<bool>,
|
||||||
pub(crate) loaded_at: Cell<Option<Instant>>,
|
pub(crate) loaded_at: Cell<Option<Instant>>,
|
||||||
}
|
}
|
||||||
@@ -20,6 +27,8 @@ pub(crate) struct DevicesWidgets {
|
|||||||
pub(crate) status: adw::StatusPage,
|
pub(crate) status: adw::StatusPage,
|
||||||
/// "This computer" — the local folders synced to this machine's device.
|
/// "This computer" — the local folders synced to this machine's device.
|
||||||
pub(crate) sync_group: adw::PreferencesGroup,
|
pub(crate) sync_group: adw::PreferencesGroup,
|
||||||
|
/// "Rename" in the "This computer" header — renames this machine's device.
|
||||||
|
pub(crate) rename_this: gtk4::Button,
|
||||||
/// "Other computers" — the account's *other* registered devices. This
|
/// "Other computers" — the account's *other* registered devices. This
|
||||||
/// machine's own device is deliberately not among them; see
|
/// machine's own device is deliberately not among them; see
|
||||||
/// [`repaint_devices`].
|
/// [`repaint_devices`].
|
||||||
@@ -75,6 +84,17 @@ pub(crate) fn build_devices_page() -> (gtk4::Widget, DevicesWidgets) {
|
|||||||
them as you open them.",
|
them as you open them.",
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
|
// Rename control for *this* machine's device, in the section header. The
|
||||||
|
// current device is filtered out of "Other computers", so this is the only
|
||||||
|
// place it can be renamed; insensitive until the device list identifies it.
|
||||||
|
let rename_this = gtk4::Button::builder()
|
||||||
|
.icon_name("document-edit-symbolic")
|
||||||
|
.tooltip_text("Rename this computer")
|
||||||
|
.valign(gtk4::Align::Center)
|
||||||
|
.sensitive(false)
|
||||||
|
.build();
|
||||||
|
rename_this.add_css_class("flat");
|
||||||
|
sync_group.set_header_suffix(Some(&rename_this));
|
||||||
let group = adw::PreferencesGroup::builder()
|
let group = adw::PreferencesGroup::builder()
|
||||||
.title("Other computers")
|
.title("Other computers")
|
||||||
.description("Other devices backing up to this account.")
|
.description("Other devices backing up to this account.")
|
||||||
@@ -123,6 +143,7 @@ pub(crate) fn build_devices_page() -> (gtk4::Widget, DevicesWidgets) {
|
|||||||
content,
|
content,
|
||||||
status,
|
status,
|
||||||
sync_group,
|
sync_group,
|
||||||
|
rename_this,
|
||||||
group,
|
group,
|
||||||
add_folder,
|
add_folder,
|
||||||
restore,
|
restore,
|
||||||
@@ -148,6 +169,12 @@ pub(crate) fn wire_devices(
|
|||||||
add_folder.connect_clicked(move |_| prompt_add_sync_folder(&ui_add));
|
add_folder.connect_clicked(move |_| prompt_add_sync_folder(&ui_add));
|
||||||
let ui_restore = ui.clone();
|
let ui_restore = ui.clone();
|
||||||
restore.connect_clicked(move |_| prompt_restore_folders(&ui_restore));
|
restore.connect_clicked(move |_| prompt_restore_folders(&ui_restore));
|
||||||
|
let ui_ren = ui.clone();
|
||||||
|
ui.devices.rename_this.connect_clicked(move |_| {
|
||||||
|
if let Some((uid, name)) = ui_ren.devices.this_device.borrow().clone() {
|
||||||
|
prompt_rename_device(&ui_ren, &uid, &name);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a status page in place of the Devices list.
|
/// Show a status page in place of the Devices list.
|
||||||
@@ -282,6 +309,22 @@ pub(crate) fn repaint_devices(ui: &Rc<Ui>, devices: &[DeviceInfo]) {
|
|||||||
for row in ui.devices.rows.borrow_mut().drain(..) {
|
for row in ui.devices.rows.borrow_mut().drain(..) {
|
||||||
ui.devices.group.remove(&row);
|
ui.devices.group.remove(&row);
|
||||||
}
|
}
|
||||||
|
// Identify this machine's own device so the "This computer" header's Rename
|
||||||
|
// action has a target. It never appears in the rows below, so this is the
|
||||||
|
// only place its name/uid is captured.
|
||||||
|
match devices.iter().find(|d| d.this_device) {
|
||||||
|
Some(me) => {
|
||||||
|
*ui.devices.this_device.borrow_mut() = Some((me.uid.clone(), me.name.clone()));
|
||||||
|
ui.devices.rename_this.set_sensitive(true);
|
||||||
|
ui.devices
|
||||||
|
.rename_this
|
||||||
|
.set_tooltip_text(Some(&format!("Rename this computer ({})", me.name)));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
*ui.devices.this_device.borrow_mut() = None;
|
||||||
|
ui.devices.rename_this.set_sensitive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
let others: Vec<&DeviceInfo> = devices.iter().filter(|d| !d.this_device).collect();
|
let others: Vec<&DeviceInfo> = devices.iter().filter(|d| !d.this_device).collect();
|
||||||
if others.is_empty() {
|
if others.is_empty() {
|
||||||
let row = adw::ActionRow::builder()
|
let row = adw::ActionRow::builder()
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ pub(crate) struct StatusState {
|
|||||||
pub(crate) mount_row: adw::ActionRow,
|
pub(crate) mount_row: adw::ActionRow,
|
||||||
pub(crate) cache_bar: gtk4::ProgressBar,
|
pub(crate) cache_bar: gtk4::ProgressBar,
|
||||||
pub(crate) cache_label: gtk4::Label,
|
pub(crate) cache_label: gtk4::Label,
|
||||||
|
/// Account-quota group + its bar/label. Hidden until the first reading.
|
||||||
|
pub(crate) quota_group: adw::PreferencesGroup,
|
||||||
|
pub(crate) quota_bar: gtk4::ProgressBar,
|
||||||
|
pub(crate) quota_label: gtk4::Label,
|
||||||
|
/// One `AccountQuota` in flight at a time, and when it last succeeded — quota
|
||||||
|
/// barely moves, so [`refresh_quota`] refetches on a long TTL, not every tick.
|
||||||
|
pub(crate) quota_inflight: Cell<bool>,
|
||||||
|
pub(crate) quota_checked_at: Cell<Option<Instant>>,
|
||||||
/// "Start on login" toggle. [`Self::settings_suppress`] guards programmatic
|
/// "Start on login" toggle. [`Self::settings_suppress`] guards programmatic
|
||||||
/// sets so reflecting the systemd state doesn't fire the toggle handler.
|
/// sets so reflecting the systemd state doesn't fire the toggle handler.
|
||||||
pub(crate) autostart_row: adw::SwitchRow,
|
pub(crate) autostart_row: adw::SwitchRow,
|
||||||
@@ -78,6 +86,10 @@ pub(crate) struct MainWidgets {
|
|||||||
pub(crate) mount_row: adw::ActionRow,
|
pub(crate) mount_row: adw::ActionRow,
|
||||||
/// Live upload/download progress, populated by the refresh loop.
|
/// Live upload/download progress, populated by the refresh loop.
|
||||||
pub(crate) transfers_group: adw::PreferencesGroup,
|
pub(crate) transfers_group: adw::PreferencesGroup,
|
||||||
|
/// Account-quota group; hidden until `refresh_quota` gets a reading.
|
||||||
|
pub(crate) quota_group: adw::PreferencesGroup,
|
||||||
|
pub(crate) quota_bar: gtk4::ProgressBar,
|
||||||
|
pub(crate) quota_label: gtk4::Label,
|
||||||
pub(crate) cache_bar: gtk4::ProgressBar,
|
pub(crate) cache_bar: gtk4::ProgressBar,
|
||||||
pub(crate) cache_label: gtk4::Label,
|
pub(crate) cache_label: gtk4::Label,
|
||||||
pub(crate) pins_group: adw::PreferencesGroup,
|
pub(crate) pins_group: adw::PreferencesGroup,
|
||||||
@@ -111,6 +123,29 @@ pub(crate) fn build_main_page() -> (gtk4::Widget, MainWidgets) {
|
|||||||
account_row.add_suffix(&logout_button);
|
account_row.add_suffix(&logout_button);
|
||||||
account_group.add(&account_row);
|
account_group.add(&account_row);
|
||||||
|
|
||||||
|
// Account storage: the Proton account quota (used of total), distinct from
|
||||||
|
// the local content cache below. A progress bar + "X of Y used" line, painted
|
||||||
|
// by `refresh_quota`. Hidden until the first successful read so a cold start
|
||||||
|
// (or an account the API can't report) shows nothing rather than an empty bar.
|
||||||
|
let quota_group = adw::PreferencesGroup::builder()
|
||||||
|
.title("Account storage")
|
||||||
|
.description("Your Proton storage across all products.")
|
||||||
|
.visible(false)
|
||||||
|
.build();
|
||||||
|
let quota_box = gtk4::Box::new(gtk4::Orientation::Vertical, 6);
|
||||||
|
quota_box.set_margin_top(6);
|
||||||
|
quota_box.set_margin_bottom(6);
|
||||||
|
let quota_bar = gtk4::ProgressBar::new();
|
||||||
|
let quota_label = gtk4::Label::builder().halign(gtk4::Align::Start).build();
|
||||||
|
quota_label.add_css_class("dim-label");
|
||||||
|
quota_box.append("a_bar);
|
||||||
|
quota_box.append("a_label);
|
||||||
|
let quota_row = adw::PreferencesRow::builder()
|
||||||
|
.activatable(false)
|
||||||
|
.child("a_box)
|
||||||
|
.build();
|
||||||
|
quota_group.add("a_row);
|
||||||
|
|
||||||
// Mount group: a read-only status line. The mount is managed automatically
|
// Mount group: a read-only status line. The mount is managed automatically
|
||||||
// by the systemd user service; there is no toggle to fiddle with.
|
// by the systemd user service; there is no toggle to fiddle with.
|
||||||
let mount_group = adw::PreferencesGroup::builder().title("Drive").build();
|
let mount_group = adw::PreferencesGroup::builder().title("Drive").build();
|
||||||
@@ -219,6 +254,7 @@ pub(crate) fn build_main_page() -> (gtk4::Widget, MainWidgets) {
|
|||||||
inner.set_margin_start(12);
|
inner.set_margin_start(12);
|
||||||
inner.set_margin_end(12);
|
inner.set_margin_end(12);
|
||||||
inner.append(&account_group);
|
inner.append(&account_group);
|
||||||
|
inner.append("a_group);
|
||||||
inner.append(&mount_group);
|
inner.append(&mount_group);
|
||||||
inner.append(&transfers_group);
|
inner.append(&transfers_group);
|
||||||
inner.append(&storage_group);
|
inner.append(&storage_group);
|
||||||
@@ -238,6 +274,9 @@ pub(crate) fn build_main_page() -> (gtk4::Widget, MainWidgets) {
|
|||||||
account_row,
|
account_row,
|
||||||
mount_row,
|
mount_row,
|
||||||
transfers_group,
|
transfers_group,
|
||||||
|
quota_group,
|
||||||
|
quota_bar,
|
||||||
|
quota_label,
|
||||||
cache_bar,
|
cache_bar,
|
||||||
cache_label,
|
cache_label,
|
||||||
pins_group,
|
pins_group,
|
||||||
@@ -463,12 +502,65 @@ pub(crate) fn refresh(ui: &Rc<Ui>) {
|
|||||||
// Both of these pages show work as it happens, so they follow the tick while
|
// Both of these pages show work as it happens, so they follow the tick while
|
||||||
// they are on screen. Every other page loads on navigation only.
|
// they are on screen. Every other page loads on navigation only.
|
||||||
match ui.stack.visible_child_name().as_deref() {
|
match ui.stack.visible_child_name().as_deref() {
|
||||||
|
Some("main") => refresh_quota(ui),
|
||||||
Some("devices") => refresh_sync_folders(ui),
|
Some("devices") => refresh_sync_folders(ui),
|
||||||
Some("activity") => refresh_activity(ui),
|
Some("activity") => refresh_activity(ui),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How long a quota reading stays fresh. Account storage barely moves, so the
|
||||||
|
/// Settings tick refetches it only this often rather than every 2s.
|
||||||
|
const QUOTA_TTL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
|
/// Fetch the account quota (if the last reading is stale) and paint the Account
|
||||||
|
/// storage group. Runs only while Settings is on screen. The group stays hidden
|
||||||
|
/// until a reading lands, so an account the API can't report — or a still-starting
|
||||||
|
/// daemon — shows nothing rather than an empty bar. A failed fetch leaves the last
|
||||||
|
/// good reading in place.
|
||||||
|
pub(crate) fn refresh_quota(ui: &Rc<Ui>) {
|
||||||
|
if ui.status.quota_inflight.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(at) = ui.status.quota_checked_at.get()
|
||||||
|
&& at.elapsed() < QUOTA_TTL
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui.status.quota_inflight.set(true);
|
||||||
|
let rx = spawn_request(ui.dirs.control_socket(), Request::AccountQuota);
|
||||||
|
let ui = ui.clone();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
let result = rx.recv().await;
|
||||||
|
ui.status.quota_inflight.set(false);
|
||||||
|
if let Ok(Ok(Response::AccountQuota {
|
||||||
|
max_space,
|
||||||
|
used_space,
|
||||||
|
})) = result
|
||||||
|
{
|
||||||
|
ui.status.quota_checked_at.set(Some(Instant::now()));
|
||||||
|
let used = used_space.max(0) as u64;
|
||||||
|
if max_space > 0 {
|
||||||
|
let total = max_space as u64;
|
||||||
|
let fraction = (used as f64 / total as f64).min(1.0);
|
||||||
|
ui.status.quota_bar.set_fraction(fraction);
|
||||||
|
let pct = (fraction * 100.0).round() as u64;
|
||||||
|
ui.status.quota_label.set_text(&format!(
|
||||||
|
"{} of {} used ({pct}%)",
|
||||||
|
human_bytes(used),
|
||||||
|
human_bytes(total)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ui.status.quota_bar.set_fraction(0.0);
|
||||||
|
ui.status
|
||||||
|
.quota_label
|
||||||
|
.set_text(&format!("{} used", human_bytes(used)));
|
||||||
|
}
|
||||||
|
ui.status.quota_group.set_visible(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Record the mount state seen by the last status poll: gate every control that
|
/// Record the mount state seen by the last status poll: gate every control that
|
||||||
/// needs a live daemon, and notify the desktop when the state actually flips.
|
/// needs a live daemon, and notify the desktop when the state actually flips.
|
||||||
///
|
///
|
||||||
|
|||||||
Reference in new issue
Block a user