fix(ui): version art gets properly prefetched
This commit is contained in:
parent
4aaff39abc
commit
e31d799e86
11 changed files with 161 additions and 25 deletions
|
|
@ -2,7 +2,7 @@ use freya::prelude::*;
|
|||
use oneclient_common::VersionKey;
|
||||
use oneclient_common::domain::GameLoader;
|
||||
|
||||
use crate::components::DynamicArt;
|
||||
use crate::components::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ impl ClusterLandscapeArt {
|
|||
selected: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
art: DynamicArt::for_version(major, key, loader).max_edge(512),
|
||||
art: DynamicArt::for_version(major, key, loader).max_edge(ART_PREVIEW_EDGE),
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,18 @@ use crate::AppAssets;
|
|||
use crate::hooks::{loaded_image, use_cached_image, use_version_metadata};
|
||||
use crate::layout::HOME_BACKGROUND_ASSET;
|
||||
|
||||
/// The size the cluster cards ask for, and so the one variant of a version's
|
||||
/// art that is usually already cached by the time anything else wants it.
|
||||
/// Full-screen art stands in with it while its own download is in flight.
|
||||
pub const ART_PREVIEW_EDGE: u32 = 512;
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct DynamicArt {
|
||||
major: Option<u32>,
|
||||
key: Option<VersionKey>,
|
||||
loader: Option<GameLoader>,
|
||||
max_edge: u32,
|
||||
preview_edge: Option<u32>,
|
||||
}
|
||||
|
||||
impl DynamicArt {
|
||||
|
|
@ -24,6 +30,7 @@ impl DynamicArt {
|
|||
key,
|
||||
loader,
|
||||
max_edge: DEFAULT_IMAGE_EDGE,
|
||||
preview_edge: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +41,7 @@ impl DynamicArt {
|
|||
key: parsed.and_then(|p| p.key()),
|
||||
loader: Some(cluster.mc_loader),
|
||||
max_edge: DEFAULT_IMAGE_EDGE,
|
||||
preview_edge: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +51,7 @@ impl DynamicArt {
|
|||
key: None,
|
||||
loader: None,
|
||||
max_edge: DEFAULT_IMAGE_EDGE,
|
||||
preview_edge: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,8 +61,27 @@ impl DynamicArt {
|
|||
self
|
||||
}
|
||||
|
||||
/// Stand in with a smaller variant of the same art until the full-size one
|
||||
/// has downloaded.
|
||||
///
|
||||
/// A full-size background is a few hundred KB and the cluster cards have
|
||||
/// usually already cached the small one, so this puts the right picture on
|
||||
/// screen right away and sharpens it later, instead of sitting on the
|
||||
/// built-in fallback for the length of a download.
|
||||
#[must_use]
|
||||
pub fn preview_edge(mut self, preview_edge: u32) -> Self {
|
||||
self.preview_edge = Some(preview_edge);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn use_bytes(&self) -> (String, Bytes) {
|
||||
use_art_bytes(self.major, self.key, self.loader, self.max_edge)
|
||||
use_art_bytes(
|
||||
self.major,
|
||||
self.key,
|
||||
self.loader,
|
||||
self.max_edge,
|
||||
self.preview_edge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +90,7 @@ pub fn use_art_bytes(
|
|||
key: Option<VersionKey>,
|
||||
loader: Option<GameLoader>,
|
||||
max_edge: u32,
|
||||
preview_edge: Option<u32>,
|
||||
) -> (String, Bytes) {
|
||||
let fallback = use_memo(|| AppAssets::get_bytes(HOME_BACKGROUND_ASSET).unwrap_or_default());
|
||||
|
||||
|
|
@ -69,13 +98,24 @@ pub fn use_art_bytes(
|
|||
|
||||
let image_query = use_cached_image(art_url.clone(), max_edge);
|
||||
|
||||
match loaded_image(art_url.as_deref(), &image_query) {
|
||||
Some((url, bytes)) => (format!("{max_edge}|{url}"), bytes),
|
||||
None => (
|
||||
// Subscribed unconditionally to keep the hook order stable. Without a
|
||||
// preview size it is the same query as the one above, so it costs nothing.
|
||||
let preview_edge = preview_edge.unwrap_or(max_edge);
|
||||
let preview_query = use_cached_image(art_url.clone(), preview_edge);
|
||||
|
||||
let full = loaded_image(art_url.as_deref(), &image_query)
|
||||
.map(|(url, bytes)| (format!("{max_edge}|{url}"), bytes));
|
||||
let preview = || {
|
||||
loaded_image(art_url.as_deref(), &preview_query)
|
||||
.map(|(url, bytes)| (format!("{preview_edge}|{url}"), bytes))
|
||||
};
|
||||
|
||||
full.or_else(preview).unwrap_or_else(|| {
|
||||
(
|
||||
format!("{max_edge}|{HOME_BACKGROUND_ASSET}"),
|
||||
fallback.read().clone(),
|
||||
),
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl Component for DynamicArt {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ pub use cluster_landscape_art::ClusterLandscapeArt;
|
|||
pub use cluster_update_popup::ClusterUpdatePopup;
|
||||
pub use context_menu::ContextMenu;
|
||||
pub use dropdown::Dropdown;
|
||||
pub use dynamic_art::DynamicArt;
|
||||
pub use dynamic_art::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
pub use file_drop::FileDropOverlay;
|
||||
pub use icons::{Icon, IconType};
|
||||
pub use java_install_manager::JavaInstallManager;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use freya::prelude::*;
|
|||
use freya::router::RouterContext;
|
||||
use oneclient_core::clusters::Cluster;
|
||||
|
||||
use crate::components::{DynamicArt, Icon, IconType};
|
||||
use crate::components::{ART_PREVIEW_EDGE, DynamicArt, Icon, IconType};
|
||||
use crate::hooks::{settled_or_loading, use_active_cluster_id, use_clusters};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -190,7 +190,7 @@ impl Component for ClusterCard {
|
|||
.height(Size::fill())
|
||||
.position(Position::new_absolute())
|
||||
.layer(Layer::Relative(1))
|
||||
.child(DynamicArt::for_cluster(&self.cluster).max_edge(512)),
|
||||
.child(DynamicArt::for_cluster(&self.cluster).max_edge(ART_PREVIEW_EDGE)),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ use skia_safe::sampling_options::CubicResampler;
|
|||
use crate::Route;
|
||||
use crate::components::Button;
|
||||
use crate::components::{
|
||||
AppNavbar, DynamicArt, FileDropOverlay, Icon, IconType, OverlayPopup, ScrollArea,
|
||||
ART_PREVIEW_EDGE, AppNavbar, DynamicArt, FileDropOverlay, Icon, IconType, OverlayPopup,
|
||||
ScrollArea,
|
||||
};
|
||||
use crate::layout::AnimatedAppOutlet;
|
||||
use crate::theme;
|
||||
use crate::use_settings_snapshot;
|
||||
use oneclient_core::clusters::Cluster;
|
||||
use oneclient_db::models::ClusterId;
|
||||
|
||||
use crate::hooks::{
|
||||
|
|
@ -259,6 +261,66 @@ pub(crate) fn appshell_overlay() -> Rect {
|
|||
|
||||
pub const HOME_BACKGROUND_ASSET: &str = "backgrounds/CavesAndCliffs.jpg";
|
||||
|
||||
/// The cluster the home background is drawn from: the active one, or the first
|
||||
/// in the list before anything is active.
|
||||
fn home_cluster(clusters: &[Cluster], active: Option<ClusterId>) -> Option<&Cluster> {
|
||||
active
|
||||
.and_then(|id| clusters.iter().find(|c| c.id == id))
|
||||
.or_else(|| clusters.first())
|
||||
}
|
||||
|
||||
fn home_art(cluster: Option<&Cluster>) -> DynamicArt {
|
||||
cluster
|
||||
.map_or_else(DynamicArt::fallback, DynamicArt::for_cluster)
|
||||
.preview_edge(ART_PREVIEW_EDGE)
|
||||
}
|
||||
|
||||
/// Loads the home background art as soon as the launcher is up, rather than
|
||||
/// when Home mounts.
|
||||
///
|
||||
/// The art is three async hops deep — the cluster list, then the version
|
||||
/// manifest holding its url, then the image itself — and none of them start
|
||||
/// until something subscribes. Mounted at the root, they overlap the rest of
|
||||
/// startup while the splash curtain is still up; left to Home, they run after
|
||||
/// it has lifted and show as the built-in fallback lingering on screen.
|
||||
///
|
||||
/// The queries are global and outlive this component's subscription, so Home
|
||||
/// finds them already settled and renders the real art on its first frame.
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub struct HomeArtPrefetch;
|
||||
|
||||
impl Component for HomeArtPrefetch {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
// Before the launcher is ready every query errors out, and a settled
|
||||
// error is not retried until a new subscriber mounts. Waiting means the
|
||||
// inner component's hooks run once, when they can actually succeed.
|
||||
if !use_launcher().ready {
|
||||
return rect().into_element();
|
||||
}
|
||||
HomeArtWarm.into_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
struct HomeArtWarm;
|
||||
|
||||
impl Component for HomeArtWarm {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let clusters_query = use_clusters();
|
||||
let art = {
|
||||
let reader = clusters_query.read();
|
||||
let state = reader.state();
|
||||
// No active cluster this early, so this is the first in the list —
|
||||
// the same one `AppHomeBackground` picks.
|
||||
let clusters = state.ok().map_or(&[][..], Vec::as_slice);
|
||||
home_art(home_cluster(clusters, None))
|
||||
};
|
||||
let _ = art.use_bytes();
|
||||
|
||||
rect().into_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub struct AppHomeBackground;
|
||||
|
||||
|
|
@ -290,12 +352,9 @@ impl Component for AppHomeBackground {
|
|||
let reader = clusters_query.read();
|
||||
let state = reader.state();
|
||||
let clusters = state.ok().map_or(&[][..], Vec::as_slice);
|
||||
let chosen = (*active_id.read())
|
||||
.and_then(|id| clusters.iter().find(|c| c.id == id))
|
||||
.or_else(|| clusters.first());
|
||||
let chosen = home_cluster(clusters, *active_id.read());
|
||||
let dep = chosen.map(|c| c.id);
|
||||
let art = chosen.map_or_else(DynamicArt::fallback, DynamicArt::for_cluster);
|
||||
(art, dep)
|
||||
(home_art(chosen), dep)
|
||||
};
|
||||
|
||||
let (_, art_bytes) = art.use_bytes();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ mod settings_shell;
|
|||
|
||||
pub use animated_outlet::AnimatedAppOutlet;
|
||||
pub(crate) use app_shell::gradient_overlay_radial;
|
||||
pub use app_shell::{AppShell, HOME_BACKGROUND_ASSET};
|
||||
pub use app_shell::{AppShell, HOME_BACKGROUND_ASSET, HomeArtPrefetch};
|
||||
pub use cluster_shell::{ClusterShell, cluster_content};
|
||||
pub use onboarding_shell::OnboardingShell;
|
||||
pub use root_layout::RootLayout;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::components::{
|
|||
StatusBar, Toasts, UpdatePromptOverlay,
|
||||
};
|
||||
use crate::hooks::{SplashState, use_provide_splash};
|
||||
use crate::layout::HomeArtPrefetch;
|
||||
use crate::routes::Route;
|
||||
use crate::theme;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -73,5 +74,6 @@ impl Component for RootLayout {
|
|||
.child(ClusterUpdatePopup)
|
||||
.child(StatusBar)
|
||||
.child(SplashCurtain)
|
||||
.child(HomeArtPrefetch)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::time::Duration;
|
|||
|
||||
use oneclient_core::clusters::Cluster;
|
||||
|
||||
use crate::components::DynamicArt;
|
||||
use crate::components::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
use crate::hooks::use_settings_snapshot;
|
||||
use crate::layout::gradient_overlay_radial;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -102,10 +102,15 @@ impl Component for LoadingBackdrop {
|
|||
let pan_x = cx * sw * PARALLAX_STRENGTH;
|
||||
let pan_y = cy * sh * PARALLAX_STRENGTH;
|
||||
|
||||
// Left at the default edge on purpose: this is the same full-screen art
|
||||
// the home background shows next, so asking for the same variant means
|
||||
// the onboarding download is the one home renders instead of it having
|
||||
// to fetch the same picture again at a different size.
|
||||
let art = match clusters.get(current) {
|
||||
Some(cluster) => DynamicArt::for_cluster(cluster).max_edge(1280),
|
||||
None => DynamicArt::fallback().max_edge(1280),
|
||||
};
|
||||
Some(cluster) => DynamicArt::for_cluster(cluster),
|
||||
None => DynamicArt::fallback(),
|
||||
}
|
||||
.preview_edge(ART_PREVIEW_EDGE);
|
||||
|
||||
let fade = use_animation_with_dependencies(¤t, |conf, _| {
|
||||
conf.on_creation(OnCreation::Run);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use super::*;
|
|||
|
||||
use freya::router::RouterContext;
|
||||
|
||||
use crate::components::{Button, DynamicArt, Icon, IconType, ScrollArea};
|
||||
use crate::components::{ART_PREVIEW_EDGE, Button, DynamicArt, Icon, IconType, ScrollArea};
|
||||
use crate::hooks::ClusterBundles;
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -189,7 +189,7 @@ fn prefetch_art(items: &[ClusterBundles]) -> impl IntoElement {
|
|||
rect()
|
||||
.width(Size::px(1.))
|
||||
.height(Size::px(1.))
|
||||
.child(DynamicArt::for_cluster(&cb.cluster).max_edge(512))
|
||||
.child(DynamicArt::for_cluster(&cb.cluster).max_edge(ART_PREVIEW_EDGE))
|
||||
.into_element()
|
||||
}))
|
||||
.into_element()
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ impl LauncherState {
|
|||
clusters,
|
||||
metadata: Mutex::new(MetadataStore::new()),
|
||||
bundles: Arc::new(BundlesManager::new()),
|
||||
versions: Arc::new(VersionsManager::new()),
|
||||
versions: Arc::new(VersionsManager::from_cache().await),
|
||||
images: ImageCacheStore::new(),
|
||||
games: crate::game::GameProcessManager::new(),
|
||||
discord,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,36 @@ impl VersionsManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Seeds the manifest from the last synced copy on disk.
|
||||
///
|
||||
/// Without this the manifest is empty until the network answers, and
|
||||
/// everything read off it — a version's art, name, description — waits on a
|
||||
/// request that, for a returning user, ends in a 304 and the very same file.
|
||||
/// The UI would rather render slightly stale metadata now and pick up the
|
||||
/// sync when it lands.
|
||||
#[must_use]
|
||||
pub async fn from_cache() -> Self {
|
||||
let manifest = Self::cached_manifest()
|
||||
.await
|
||||
.unwrap_or_else(VersionsManifest::default);
|
||||
|
||||
Self {
|
||||
manifest: RwLock::new(manifest),
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_manifest() -> Option<VersionsManifest> {
|
||||
let path = paths::caches_dir().ok()?.join("versions.json");
|
||||
let bytes = polyio::read(&path).await.ok()?;
|
||||
match serde_json::from_slice(&bytes) {
|
||||
Ok(manifest) => Some(manifest),
|
||||
Err(err) => {
|
||||
tracing::warn!("cached versions manifest is unreadable: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, services))]
|
||||
pub async fn sync(&self, services: &LauncherServices) -> LauncherResult<bool> {
|
||||
let Some((manifest, changed)) = Self::fetch_manifest(services).await? else {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue