max / audiofiles
2 files changed,
+164 insertions,
-252 deletions
| @@ -7594,6 +7594,10 @@ | |||
| 7594 | 7594 | name = "quasi-webview" | |
| 7595 | 7595 | version = "0.101.1" | |
| 7596 | 7596 | ||
| 7597 | + | [[patch.unused]] | |
| 7598 | + | name = "quasi-type" | |
| 7599 | + | version = "0.1.3" | |
| 7600 | + | ||
| 7597 | 7601 | [[patch.unused]] | |
| 7598 | 7602 | name = "kberg" | |
| 7599 | 7603 | version = "0.1.0" | |
| @@ -7605,7 +7609,3 @@ | |||
| 7605 | 7609 | [[patch.unused]] | |
| 7606 | 7610 | name = "painhours" | |
| 7607 | 7611 | version = "0.1.0" | |
| 7608 | - | ||
| 7609 | - | [[patch.unused]] | |
| 7610 | - | name = "quasi-type" | |
| 7611 | - | version = "0.1.3" |
| @@ -104,13 +104,11 @@ | |||
| 104 | 104 | //! [`Readiness::Pending`](quasi_router::layout::Readiness::Pending) and the | |
| 105 | 105 | //! renderer owns what waiting looks like. | |
| 106 | 106 | ||
| 107 | - | use quasi_router::layout::{FieldKind, Tone}; | |
| 108 | - | use quasi_router::{ | |
| 109 | - | Act, Action, Choice, Field, Node, RegionKind, Request, Response, RouteError, Router, Screen, | |
| 110 | - | Slot, | |
| 111 | - | }; | |
| 107 | + | use quasi_declare::declare; | |
| 108 | + | use quasi_router::layout::Tone; | |
| 109 | + | use quasi_router::{Action, Choice, Node, Request, Response, RouteError, Router}; | |
| 112 | 110 | ||
| 113 | - | use super::{State, Status, Subscription, Sync}; | |
| 111 | + | use super::{State, Subscription, Sync}; | |
| 114 | 112 | ||
| 115 | 113 | /// The region the whole screen answers into. | |
| 116 | 114 | const BODY: &str = "sync-body"; | |
| @@ -151,7 +149,7 @@ | |||
| 151 | 149 | ||
| 152 | 150 | /// `GET /sync` | |
| 153 | 151 | fn index(state: &super::Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 154 | - | Ok(screen(state.sync).into()) | |
| 152 | + | Ok(showing(state.sync)) | |
| 155 | 153 | } | |
| 156 | 154 | ||
| 157 | 155 | /// `POST /sync/connect` | |
| @@ -172,7 +170,7 @@ | |||
| 172 | 170 | /// `POST /sync/cancel` | |
| 173 | 171 | fn cancel(state: &super::Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 174 | 172 | state.sync.cancel(); | |
| 175 | - | Ok(screen(state.sync).into()) | |
| 173 | + | Ok(showing(state.sync)) | |
| 176 | 174 | } | |
| 177 | 175 | ||
| 178 | 176 | /// `POST /sync/encryption` | |
| @@ -183,8 +181,9 @@ | |||
| 183 | 181 | fn encryption(state: &super::Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 184 | 182 | let password = request.payload.get("password").unwrap_or_default(); | |
| 185 | 183 | if password.is_empty() { | |
| 186 | - | return Ok(Response::from(screen(state.sync)) | |
| 187 | - | .toast(Tone::Danger, "A password is needed to encrypt this vault.")); | |
| 184 | + | return Ok( | |
| 185 | + | showing(state.sync).toast(Tone::Danger, "A password is needed to encrypt this vault.") | |
| 186 | + | ); | |
| 188 | 187 | } | |
| 189 | 188 | let is_new = matches!( | |
| 190 | 189 | state.sync.status().state, | |
| @@ -193,20 +192,20 @@ | |||
| 193 | 192 | } | |
| 194 | 193 | ); | |
| 195 | 194 | state.sync.set_password(password, is_new); | |
| 196 | - | Ok(screen(state.sync).into()) | |
| 195 | + | Ok(showing(state.sync)) | |
| 197 | 196 | } | |
| 198 | 197 | ||
| 199 | 198 | /// `POST /sync/now` | |
| 200 | 199 | fn sync_now(state: &super::Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 201 | 200 | state.sync.sync_now(); | |
| 202 | - | Ok(screen(state.sync).into()) | |
| 201 | + | Ok(showing(state.sync)) | |
| 203 | 202 | } | |
| 204 | 203 | ||
| 205 | 204 | /// `POST /sync/auto` | |
| 206 | 205 | fn auto(state: &super::Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 207 | 206 | let on = !request.payload.get("auto").unwrap_or_default().is_empty(); | |
| 208 | 207 | state.sync.set_auto(on); | |
| 209 | - | Ok(screen(state.sync).into()) | |
| 208 | + | Ok(showing(state.sync)) | |
| 210 | 209 | } | |
| 211 | 210 | ||
| 212 | 211 | /// `POST /sync/interval` | |
| @@ -221,13 +220,13 @@ | |||
| 221 | 220 | return Err(RouteError::not_found("no such interval")); | |
| 222 | 221 | } | |
| 223 | 222 | state.sync.set_interval(minutes); | |
| 224 | - | Ok(screen(state.sync).into()) | |
| 223 | + | Ok(showing(state.sync)) | |
| 225 | 224 | } | |
| 226 | 225 | ||
| 227 | 226 | /// `POST /sync/error/clear` | |
| 228 | 227 | fn clear_error(state: &super::Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 229 | 228 | state.sync.clear_error(); | |
| 230 | - | Ok(screen(state.sync).into()) | |
| 229 | + | Ok(showing(state.sync)) | |
| 231 | 230 | } | |
| 232 | 231 | ||
| 233 | 232 | /// `POST /sync/subscription/refresh` | |
| @@ -236,7 +235,7 @@ | |||
| 236 | 235 | _request: Request, | |
| 237 | 236 | ) -> Result<Response, RouteError> { | |
| 238 | 237 | state.sync.refresh_subscription(); | |
| 239 | - | Ok(screen(state.sync).into()) | |
| 238 | + | Ok(showing(state.sync)) | |
| 240 | 239 | } | |
| 241 | 240 | ||
| 242 | 241 | /// `POST /sync/subscribe` | |
| @@ -249,15 +248,14 @@ | |||
| 249 | 248 | let cap = cap_from(state, &request)?; | |
| 250 | 249 | let annual = request.payload.get("cadence") == Some("annual"); | |
| 251 | 250 | state.sync.subscribe(cap, annual); | |
| 252 | - | Ok(Response::from(screen(state.sync)).toast(Tone::Info, "Opening checkout in your browser.")) | |
| 251 | + | Ok(showing(state.sync).toast(Tone::Info, "Opening checkout in your browser.")) | |
| 253 | 252 | } | |
| 254 | 253 | ||
| 255 | 254 | /// `POST /sync/cap` | |
| 256 | 255 | fn cap(state: &super::Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 257 | 256 | let cap = cap_from(state, &request)?; | |
| 258 | 257 | state.sync.queue_cap_change(cap); | |
| 259 | - | Ok(Response::from(screen(state.sync)) | |
| 260 | - | .toast(Tone::Success, "The cap changes at your next renewal.")) | |
| 258 | + | Ok(showing(state.sync).toast(Tone::Success, "The cap changes at your next renewal.")) | |
| 261 | 259 | } | |
| 262 | 260 | ||
| 263 | 261 | /// The cap a request asked for, in bytes, refused if it is outside what is sold. | |
| @@ -285,262 +283,229 @@ | |||
| 285 | 283 | /// `POST /sync/disconnect` | |
| 286 | 284 | fn disconnect(state: &super::Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 287 | 285 | state.sync.disconnect(); | |
| 288 | - | Ok(screen(state.sync).into()) | |
| 286 | + | Ok(showing(state.sync)) | |
| 289 | 287 | } | |
| 290 | 288 | ||
| 291 | - | /// The screen, which is a different screen per state. | |
| 292 | - | /// | |
| 293 | - | /// One route answering four shapes rather than four routes: the state is not an | |
| 294 | - | /// address, and a user cannot navigate to `Authenticating` — they arrive there | |
| 295 | - | /// because something happened. Four addresses would be four places you could | |
| 296 | - | /// bookmark into a lie. | |
| 297 | - | fn screen(sync: &dyn Sync) -> Screen { | |
| 298 | - | let status = sync.status(); | |
| 299 | - | // Live, which is the whole of what the finding in this module's header | |
| 300 | - | // asked for. The state moves without anyone acting: an OAuth callback lands | |
| 301 | - | // in another process and `Authenticating` becomes `NeedsEncryption`, and the | |
| 302 | - | // pending count falls while a sync runs. The description says the contents | |
| 303 | - | // move and says nothing about how often to look; the cadence is | |
| 304 | - | // `quasi_immediate::CADENCE`, and the host no longer owns a timer. | |
| 305 | - | let mut body = Slot::new(BODY, RegionKind::Pane) | |
| 306 | - | .live() | |
| 307 | - | .with(Node::page("Cloud Sync")) | |
| 308 | - | // The way off, for `settings::screen`'s reason: this screen replaces | |
| 309 | - | // the shell's and nothing described an exit from it. | |
| 310 | - | .with(Node::Act(Act::new("Close", Action::back()))); | |
| 311 | - | ||
| 312 | - | // Nothing to offer when there is nothing to offer it against. See | |
| 313 | - | // [`Sync::available`]: an unavailable manager is not a disconnected one, and | |
| 314 | - | // the flip found the described screen offering a `Connect` that refuses and | |
| 315 | - | // a `Dismiss` for an error `clear_error` cannot clear. The shipped panel | |
| 316 | - | // answered this state with a whole second window saying the same two | |
| 317 | - | // sentences and drawing no controls. | |
| 318 | - | if !sync.available() { | |
| 319 | - | return Screen::sidebar_content("Cloud Sync").with( | |
| 320 | - | body.with(Node::text("Cloud sync is unavailable.")) | |
| 321 | - | .with(Node::text("Open a vault to enable sync.")), | |
| 322 | - | ); | |
| 323 | - | } | |
| 324 | - | ||
| 325 | - | body = match status.state { | |
| 326 | - | State::Disconnected => disconnected(body), | |
| 327 | - | State::Authenticating => authenticating(body), | |
| 328 | - | State::NeedsEncryption { has_server_key } => needs_encryption(body, has_server_key), | |
| 329 | - | State::Ready | State::Syncing => ready(body, &status, sync), | |
| 330 | - | }; | |
| 331 | - | ||
| 332 | - | // The error banner, on every state, because a failure can arrive in any of | |
| 333 | - | // them. `Node::Notice` carries the tone and the text; what to do about it is | |
| 334 | - | // two controls, and Retry is only offered where retrying means anything. | |
| 335 | - | if let Some(error) = &status.last_error { | |
| 336 | - | body = body.with(Node::banner(Tone::Danger, error.clone())); | |
| 337 | - | if matches!(status.state, State::Ready | State::Syncing) { | |
| 338 | - | body = body.with(Node::Act(Act::new("Retry", Action::post("/sync/now")))); | |
| 339 | - | } | |
| 340 | - | body = body.with(Node::Act(Act::new( | |
| 341 | - | "Dismiss", | |
| 342 | - | Action::post("/sync/error/clear"), | |
| 343 | - | ))); | |
| 344 | - | } | |
| 345 | - | ||
| 346 | - | Screen::sidebar_content("Cloud Sync").with(body) | |
| 289 | + | /// The screen, read and then described. | |
| 290 | + | pub(super) fn showing(sync: &dyn Sync) -> Response { | |
| 291 | + | Response::from(screen(&read(sync))) | |
| 347 | 292 | } | |
| 348 | 293 | ||
| 349 | - | fn disconnected(body: Slot) -> Slot { | |
| 350 | - | body.with(Node::text( | |
| 351 | - | "Connect your audiofiles vault to Makenot.work for cross-device sync.", | |
| 352 | - | )) | |
| 353 | - | .with(Node::text( | |
| 354 | - | "Metadata (tags, vault structure, analysis) syncs automatically. Audio file sync is per-vault opt-in.", | |
| 355 | - | )) | |
| 356 | - | .with(Node::Act(Act::new("Connect", Action::post("/sync/connect")))) | |
| 357 | - | } | |
| 358 | - | ||
| 359 | - | /// Waiting on a browser, with a way out. | |
| 360 | - | /// | |
| 361 | - | /// The spinner needs no vocabulary: [`Readiness::Pending`] is what a region | |
| 362 | - | /// waiting on something says, and every renderer already answers it. That is one | |
| 363 | - | /// place this screen expected a finding and did not get one. | |
| 364 | - | fn authenticating(body: Slot) -> Slot { | |
| 365 | - | body.with(Node::text("Waiting for authentication in your browser...")) | |
| 366 | - | .with(Node::text( | |
| 367 | - | "The app will update automatically once you sign in.", | |
| 368 | - | )) | |
| 369 | - | .with(Node::Act(Act::new("Cancel", Action::post("/sync/cancel")))) | |
| 294 | + | /// What the panel draws, read off the manager. | |
| 295 | + | struct Cloud { | |
| 296 | + | /// Whether there is anything to offer this against. | |
| 297 | + | /// | |
| 298 | + | /// Nothing to offer when there is nothing to offer it against. See | |
| 299 | + | /// [`Sync::available`]: an unavailable manager is not a disconnected one, | |
| 300 | + | /// and the flip found the described screen offering a `Connect` that refuses | |
| 301 | + | /// and a `Dismiss` for an error `clear_error` cannot clear. The shipped | |
| 302 | + | /// panel answered this state with a whole second window saying the same two | |
| 303 | + | /// sentences and drawing no controls. | |
| 304 | + | away: bool, | |
| 305 | + | /// Whether there is nothing connected yet. | |
| 306 | + | disconnected: bool, | |
| 307 | + | /// Whether a browser is being waited on. | |
| 308 | + | authenticating: bool, | |
| 309 | + | /// The password step, while it is the step. | |
| 310 | + | encryption: Option<Encryption>, | |
| 311 | + | /// A connected panel, while it is connected. | |
| 312 | + | ready: Option<Ready>, | |
| 313 | + | /// What went wrong, if anything did. | |
| 314 | + | /// | |
| 315 | + | /// On every state that has one, because a failure can arrive in any of them, | |
| 316 | + | /// and on none of them when the manager is away: there is nothing there to | |
| 317 | + | /// have failed. | |
| 318 | + | error: Option<Failure>, | |
| 370 | 319 | } | |
| 371 | 320 | ||
| 372 | 321 | /// The password that encrypts this vault. | |
| 373 | 322 | /// | |
| 374 | - | /// [`FieldKind::Secret`] and nothing else: the description will not carry the | |
| 375 | - | /// typed value, which is `39057019`, so the runtime's buffer is the only place | |
| 376 | - | /// it has ever lived. Whether this is a new password or an existing one changes | |
| 377 | - | /// only what is said, which is why `has_server_key` reaches the prose and not | |
| 378 | - | /// the shape. | |
| 379 | - | fn needs_encryption(body: Slot, has_server_key: bool) -> Slot { | |
| 380 | - | let says = if has_server_key { | |
| 381 | - | "This vault is already encrypted. Enter its password to unlock it here." | |
| 382 | - | } else { | |
| 383 | - | "Choose a password. It encrypts everything before it leaves this machine, and it cannot be recovered." | |
| 384 | - | }; | |
| 385 | - | body.with(Node::text(says)).with(Node::Form { | |
| 386 | - | fields: vec![ | |
| 387 | - | Field::new(FieldKind::Secret, "password", "Password") | |
| 388 | - | .required() | |
| 389 | - | .hint(if has_server_key { | |
| 390 | - | "The password this vault was encrypted with." | |
| 391 | - | } else { | |
| 392 | - | "Nobody can reset this for you." | |
| 393 | - | }), | |
| 394 | - | ], | |
| 395 | - | submit: if has_server_key { | |
| 396 | - | "Unlock" | |
| 397 | - | } else { | |
| 398 | - | "Set password" | |
| 399 | - | } | |
| 400 | - | .to_owned(), | |
| 401 | - | action: Action::post("/sync/encryption"), | |
| 402 | - | }) | |
| 323 | + | /// Whether this is a new password or an existing one changes only what is said, | |
| 324 | + | /// which is why `has_server_key` reaches the prose and not the shape. | |
| 325 | + | struct Encryption { | |
| 326 | + | /// What the step says. | |
| 327 | + | said: &'static str, | |
| 328 | + | /// What the box says under itself. | |
| 329 | + | hint: &'static str, | |
| 330 | + | /// What the button reads. | |
| 331 | + | submit: &'static str, | |
| 403 | 332 | } | |
| 404 | 333 | ||
| 405 | 334 | /// Connected, and what it is doing. | |
| 406 | - | fn ready(body: Slot, status: &Status, sync: &dyn Sync) -> Slot { | |
| 407 | - | let syncing = matches!(status.state, State::Syncing); | |
| 408 | - | let mut body = body.with(Node::text(if syncing { "Syncing..." } else { "Connected" })); | |
| 409 | - | ||
| 410 | - | if let Some(last) = &status.last_sync_at { | |
| 411 | - | body = body.with(Node::text(format!("Last sync: {last}"))); | |
| 412 | - | } | |
| 413 | - | if status.pending_changes > 0 { | |
| 414 | - | // A count, not a proportion: nothing knows the total, and a `Meter` | |
| 415 | - | // handed a made-up denominator would draw a bar that means nothing. | |
| 416 | - | body = body.with(Node::Figure(quasi_router::Figure::new( | |
| 417 | - | status.pending_changes.to_string(), | |
| 418 | - | "pending changes", | |
| 419 | - | ))); | |
| 420 | - | } | |
| 421 | - | ||
| 422 | - | let mut now = Act::new("Sync now", Action::post("/sync/now")); | |
| 423 | - | if syncing { | |
| 424 | - | // Present, visible and not answering, which is what a control that is | |
| 425 | - | // already running should be. The shipped panel says the same thing with | |
| 426 | - | // `add_enabled(!syncing, ...)`. | |
| 427 | - | now = now.disabled(); | |
| 428 | - | } | |
| 429 | - | ||
| 430 | - | body.with(Node::Act(now)) | |
| 431 | - | .with(Node::section("Auto-sync")) | |
| 432 | - | .with(Node::Field(Box::new( | |
| 433 | - | Field::new(FieldKind::Checkbox, "auto", "Sync on a schedule") | |
| 434 | - | .value(if status.auto_sync_enabled { "on" } else { "" }) | |
| 435 | - | .writes(Action::post("/sync/auto")), | |
| 436 | - | ))) | |
| 437 | - | // A label at last: the "Auto-sync" heading above names the checkbox, so | |
| 438 | - | // as a bare strip this control asked its question without ever saying | |
| 439 | - | // what it was. A field cannot be unlabelled. | |
| 440 | - | .with(Node::Field(Box::new( | |
| 441 | - | Field::radio( | |
| 442 | - | INTERVAL, | |
| 443 | - | "Interval", | |
| 444 | - | INTERVALS | |
| 445 | - | .iter() | |
| 446 | - | .map(|minutes| Choice::new(minutes.to_string(), format!("{minutes} min"))) | |
| 447 | - | .collect(), | |
| 448 | - | ) | |
| 449 | - | .value(status.sync_interval_minutes.to_string()) | |
| 450 | - | .writes(Action::post("/sync/interval")), | |
| 451 | - | ))) | |
| 452 | - | .with(Node::section("Audio file sync")) | |
| 453 | - | .with(subscription(sync)) | |
| 454 | - | .with(Node::Act( | |
| 455 | - | Act::new("Disconnect", Action::post("/sync/disconnect")) | |
| 456 | - | .tone(Tone::Danger) | |
| 457 | - | .confirm("Disconnect this vault from cloud sync?"), | |
| 458 | - | )) | |
| 335 | + | struct Ready { | |
| 336 | + | /// Syncing, or merely connected. | |
| 337 | + | said: &'static str, | |
| 338 | + | /// Whether a sync is running, which is what deadens the control. | |
| 339 | + | syncing: bool, | |
| 340 | + | /// When the last sync finished, as the app formats it. | |
| 341 | + | last: Option<String>, | |
| 342 | + | /// How many local changes have not gone up, while any have not. | |
| 343 | + | /// | |
| 344 | + | /// A count, not a proportion: nothing knows the total, and a meter handed a | |
| 345 | + | /// made-up denominator would draw a bar that means nothing. | |
| 346 | + | pending: Option<String>, | |
| 347 | + | /// Whether the scheduler is running. | |
| 348 | + | auto: &'static str, | |
| 349 | + | /// The cadences, and which is chosen. | |
| 350 | + | intervals: Vec<Choice>, | |
| 351 | + | every: String, | |
| 352 | + | /// What is bought, or what may be. Exactly one of the three. | |
| 353 | + | waiting: Option<&'static str>, | |
| 354 | + | running: Option<Running>, | |
| 355 | + | offer: Option<Offer>, | |
| 459 | 356 | } | |
| 460 | 357 | ||
| 461 | - | /// What is bought, or what may be. | |
| 462 | - | /// | |
| 463 | - | /// Three shapes: not fetched yet, subscribed, or on offer. The first is a region | |
| 464 | - | /// rather than a sentence, because "not fetched yet" is exactly what | |
| 465 | - | /// [`Readiness::Pending`](quasi_router::layout::Readiness::Pending) says and the | |
| 466 | - | /// renderer already knows how to draw waiting. | |
| 467 | - | fn subscription(sync: &dyn Sync) -> Node { | |
| 468 | - | let Some(pricing) = sync.pricing() else { | |
| 469 | - | return waiting("Loading pricing..."); | |
| 470 | - | }; | |
| 471 | - | match sync.subscription() { | |
| 472 | - | None => waiting("Checking subscription..."), | |
| 473 | - | Some(sub) if sub.active => subscribed(sync, &sub, &pricing), | |
| 474 | - | Some(_) => on_offer(sync, &pricing), | |
| 475 | - | } | |
| 476 | - | } | |
| 477 | - | ||
| 478 | - | /// A region that is waiting on something. | |
| 479 | - | /// | |
| 480 | - | /// The whole of what the shipped panel spends two `Instant` fields, two | |
| 481 | - | /// thirty-second timeouts and a spinner on. How long to wait and what to draw | |
| 482 | - | /// while waiting are the renderer's, which is why neither is here. | |
| 483 | - | fn waiting(says: &str) -> Node { | |
| 484 | - | Node::Region( | |
| 485 | - | Slot::new("subscription", RegionKind::Pane) | |
| 486 | - | .pending() | |
| 487 | - | .with(Node::text(says)) | |
| 488 | - | .with(Node::Act(Act::new( | |
| 489 | - | "Retry", | |
| 490 | - | Action::post("/sync/subscription/refresh"), | |
| 491 | - | ))), | |
| 492 | - | ) | |
| 358 | + | /// What went wrong, and what can be done about it. | |
| 359 | + | struct Failure { | |
| 360 | + | /// What it says. | |
| 361 | + | said: String, | |
| 362 | + | /// Whether retrying means anything, which it does only once connected. | |
| 363 | + | retryable: bool, | |
| 493 | 364 | } | |
| 494 | 365 | ||
| 495 | 366 | /// A running subscription: what it holds, how full it is, and how to change it. | |
| 496 | - | fn subscribed(sync: &dyn Sync, sub: &Subscription, pricing: &super::Pricing) -> Node { | |
| 497 | - | let mut slot = Slot::new("subscription", RegionKind::Pane).with(Node::text(format!( | |
| 498 | - | "Subscribed: {} ({})", | |
| 499 | - | gib_of(sub.limit_bytes), | |
| 500 | - | sub.interval | |
| 501 | - | ))); | |
| 367 | + | struct Running { | |
| 368 | + | /// What is bought, and how often it is paid for. | |
| 369 | + | said: String, | |
| 370 | + | /// How full it is, where a cap was bought at all. | |
| 371 | + | gauge: Option<Gauge>, | |
| 372 | + | /// What to say about a cap that is filling, if anything. | |
| 373 | + | warning: Option<Warning>, | |
| 374 | + | /// A cap change already queued for the next renewal. | |
| 375 | + | queued: Option<String>, | |
| 376 | + | /// The cap control, defaulted to what is already bought. | |
| 377 | + | cap: Cap, | |
| 378 | + | } | |
| 502 | 379 | ||
| 503 | - | if sub.limit_bytes > 0 { | |
| 504 | - | // A real proportion, so a real `Meter`: used against bought, both known. | |
| 505 | - | // Counted in GiB rather than bytes because the bar is read by a person | |
| 506 | - | // and `Meter` takes two `u32`s. | |
| 507 | - | slot = slot.with(Node::Meter( | |
| 508 | - | quasi_router::Meter::new(gib_count(sub.used_bytes), gib_count(sub.limit_bytes).max(1)) | |
| 509 | - | .label("GiB used") | |
| 510 | - | // At ninety percent, not past it: this is a cap that stops syncing | |
| 511 | - | // when it fills, and the point of saying so is to say it before | |
| 512 | - | // that happens. | |
| 513 | - | .tone(if nearly_full(sub) { | |
| 514 | - | Tone::Warning | |
| 515 | - | } else { | |
| 516 | - | Tone::Neutral | |
| 517 | - | }), | |
| 518 | - | )); | |
| 380 | + | /// A proportion of a bought cap. | |
| 381 | + | /// | |
| 382 | + | /// Counted in GiB rather than bytes because the bar is read by a person and a | |
| 383 | + | /// meter takes two `u32`s. | |
| 384 | + | struct Gauge { | |
| 385 | + | used: u32, | |
| 386 | + | limit: u32, | |
| 387 | + | /// At ninety percent, not past it: this is a cap that stops syncing when it | |
| 388 | + | /// fills, and the point of saying so is to say it before that happens. | |
| 389 | + | tone: Tone, | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | /// What a filling cap says in words. | |
| 393 | + | /// | |
| 394 | + | /// Say it in words as well as in the bar, and say what happens next. A meter | |
| 395 | + | /// that has gone amber reports a quantity; the user needs the consequence, which | |
| 396 | + | /// is that uploads stop and metadata sync carries on. | |
| 397 | + | struct Warning { | |
| 398 | + | said: String, | |
| 399 | + | tone: Tone, | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | /// No subscription yet: the screen proposes a cap and says what it costs. | |
| 403 | + | struct Offer { | |
| 404 | + | /// The need, first, because it is the reason the rest of the screen says | |
| 405 | + | /// what it says. | |
| 406 | + | need: String, | |
| 407 | + | /// What is proposed, where there is anything to size it against. | |
| 408 | + | proposed: Option<String>, | |
| 409 | + | /// The cap control. | |
| 410 | + | cap: Cap, | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | /// The cap, as the two controls that set it. | |
| 414 | + | struct Cap { | |
| 415 | + | /// A few named sizes, each carrying what it costs. | |
| 416 | + | sizes: Vec<Choice>, | |
| 417 | + | /// The one chosen, in whole GiB. | |
| 418 | + | chosen: String, | |
| 419 | + | /// The bounds the exact figure is held to. | |
| 420 | + | min: String, | |
| 421 | + | max: String, | |
| 422 | + | /// What the exact box says under itself. | |
| 423 | + | hint: String, | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | /// What the panel draws, read off the manager. | |
| 427 | + | fn read(sync: &dyn Sync) -> Cloud { | |
| 428 | + | if !sync.available() { | |
| 429 | + | return Cloud { | |
| 430 | + | away: true, | |
| 431 | + | disconnected: false, | |
| 432 | + | authenticating: false, | |
| 433 | + | encryption: None, | |
| 434 | + | ready: None, | |
| 435 | + | error: None, | |
| 436 | + | }; | |
| 519 | 437 | } | |
| 520 | - | ||
| 521 | - | // Say it in words as well as in the bar, and say what happens next. A meter | |
| 522 | - | // that has gone amber reports a quantity; the user needs the consequence, | |
| 523 | - | // which is that uploads stop and metadata sync carries on. Without this the | |
| 524 | - | // first news of a full cap is a failed upload - the 402 from | |
| 525 | - | // `routes/synckit/blobs.rs`, which the user meets as a sync that broke. | |
| 526 | - | if let Some(warning) = cap_warning(sync, sub, pricing) { | |
| 527 | - | slot = slot.with(Node::banner( | |
| 528 | - | if sub.used_bytes >= sub.limit_bytes { | |
| 529 | - | Tone::Danger | |
| 530 | - | } else { | |
| 531 | - | Tone::Warning | |
| 532 | - | }, | |
| 533 | - | warning, | |
| 534 | - | )); | |
| 438 | + | let status = sync.status(); | |
| 439 | + | let connected = matches!(status.state, State::Ready | State::Syncing); | |
| 440 | + | Cloud { | |
| 441 | + | away: false, | |
| 442 | + | disconnected: matches!(status.state, State::Disconnected), | |
| 443 | + | authenticating: matches!(status.state, State::Authenticating), | |
| 444 | + | encryption: match status.state { | |
| 445 | + | State::NeedsEncryption { has_server_key } => Some(encrypting(has_server_key)), | |
| 446 | + | _ => None, |
Lines truncated