perf: viewport windowing for the Local Library albums grid (phase 1)
Covers and card elements now scale with the viewport instead of the library: the shared AlbumGrid gains opt-in windowing (row band = visible rows +/- one viewport, computed in .slint, row-granular change events), cards outside the band render as same-footprint placeholders (scroll geometry unchanged), and the Rust side dispatches artwork only for the band, evicting covers outside a retention span back to the placeholder. - New id-keyed ArtworkTarget::LocalAlbumById (replaces the index-keyed LocalAlbumCard): immune to derive re-sorts between dispatch and apply; reloads still drop stale covers via the generation guard. The enum loses Copy (String variant). - In-flight dedupe set freed on apply AND on fetch failure, cleared on generation bumps. - Flat list view and grouped mode keep the pre-windowing full dispatch (they don't fire window events); switching grid->list re-dispatches covers the window may have evicted. - Windowing is off by default in the shared grid: Discover, Labels and Favorites are untouched (phase 2). A 2,000-album library drops from ~540 MB of resident cover RGBA to a viewport-sized working set, and scrolling repaints ~40 mounted cards instead of the whole collection. cc
This commit is contained in:
5 files changed
+345
-47
No files matched your search
@@ -21,9 +21,23 @@ component AlbumGrid inherits Rectangle {
|
||||
in property <bool> show-favorite: true;
|
||||
in property <bool> show-source-badge: false;
|
||||
in property <bool> select-mode: false;
|
||||
// --- Viewport windowing (opt-in; default OFF = identical to before) ---
|
||||
// With `windowed` on, only cards within ~one viewport of the visible band
|
||||
// mount a real AlbumCard; the rest are same-footprint placeholders, so
|
||||
// the height formula below (the scroll geometry) never changes.
|
||||
// `visible-top`/`visible-height` arrive in the surrounding Flickable's
|
||||
// CONTENT coordinates; `content-offset` is this grid's approximate y
|
||||
// inside that content — small errors are absorbed by the margin.
|
||||
in property <bool> windowed: false;
|
||||
in property <length> visible-top: 0px;
|
||||
in property <length> visible-height: 0px;
|
||||
in property <length> content-offset: 0px;
|
||||
callback open-album(string);
|
||||
callback open-artist(string);
|
||||
callback media-action(string, string, string);
|
||||
// Fired (windowed only) when the row band changes — [first, last] item
|
||||
// indices, prefetch margin included. The consumer dispatches artwork.
|
||||
callback window-changed(int /* first index */, int /* last index */);
|
||||
|
||||
property <int> columns: Math.max(
|
||||
1,
|
||||
@@ -33,23 +47,58 @@ component AlbumGrid inherits Rectangle {
|
||||
? self.rows * root.card-height + Math.max(0, self.rows - 1) * root.card-gap
|
||||
: 0px;
|
||||
|
||||
for album[i] in root.albums: AlbumCard {
|
||||
// Row band = visible rows ± one full viewport. Row-granular ints, so the
|
||||
// `changed` handlers only fire when a row boundary is crossed (~every
|
||||
// 270px of scroll), not per scrolled pixel.
|
||||
property <length> row-stride: root.card-height + root.card-gap;
|
||||
property <length> local-top: root.visible-top - root.content-offset;
|
||||
property <int> first-window-row: root.windowed
|
||||
? Math.max(0, Math.floor((root.local-top - root.visible-height) / root.row-stride))
|
||||
: 0;
|
||||
property <int> last-window-row: root.windowed
|
||||
? Math.max(0, Math.floor((root.local-top + 2 * root.visible-height) / root.row-stride))
|
||||
: 0;
|
||||
changed first-window-row => { root.notify-window(); }
|
||||
changed last-window-row => { root.notify-window(); }
|
||||
// Column count changes on first layout and on window resize — the band's
|
||||
// ITEM indices depend on it, so re-notify.
|
||||
changed columns => { root.notify-window(); }
|
||||
init => { root.notify-window(); }
|
||||
function notify-window() {
|
||||
if (root.windowed) {
|
||||
root.window-changed(
|
||||
root.first-window-row * root.columns,
|
||||
(root.last-window-row + 1) * root.columns - 1);
|
||||
}
|
||||
}
|
||||
|
||||
for album[i] in root.albums: card-slot := Rectangle {
|
||||
x: Math.mod(i, root.columns) * (root.card-width + root.card-gap);
|
||||
y: Math.floor(i / root.columns) * (root.card-height + root.card-gap);
|
||||
width: root.card-width;
|
||||
height: root.card-height;
|
||||
album: album;
|
||||
show-favorite: root.show-favorite;
|
||||
show-source-badge: root.show-source-badge;
|
||||
select-mode: root.select-mode;
|
||||
clicked(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
if (kind == "artist") {
|
||||
root.open-artist(id);
|
||||
} else {
|
||||
root.media-action(kind, id, action);
|
||||
property <bool> in-window: !root.windowed
|
||||
|| (Math.floor(i / root.columns) >= root.first-window-row
|
||||
&& Math.floor(i / root.columns) <= root.last-window-row);
|
||||
// Placeholder tile while the real card is unmounted (fast scroll).
|
||||
border-radius: 8px;
|
||||
background: card-slot.in-window ? transparent : Theme.surface-elevated;
|
||||
if card-slot.in-window: AlbumCard {
|
||||
width: root.card-width;
|
||||
height: root.card-height;
|
||||
album: album;
|
||||
show-favorite: root.show-favorite;
|
||||
show-source-badge: root.show-source-badge;
|
||||
select-mode: root.select-mode;
|
||||
clicked(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
if (kind == "artist") {
|
||||
root.open-artist(id);
|
||||
} else {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +123,14 @@ export component AlbumCollectionView inherits VerticalLayout {
|
||||
in property <length> card-height: 246px;
|
||||
in property <length> card-gap: 24px;
|
||||
in property <length> list-row-gap: 4px;
|
||||
// Viewport windowing — forwarded to the FLAT grid only (grouped sections
|
||||
// keep full mounts; their per-section offsets are unknown here). OFF by
|
||||
// default so Discover Browse / Labels / Favorites are untouched.
|
||||
in property <bool> windowed: false;
|
||||
in property <length> visible-top: 0px;
|
||||
in property <length> visible-height: 0px;
|
||||
in property <length> content-offset: 0px;
|
||||
callback window-changed(int /* first index */, int /* last index */);
|
||||
callback open-album(string);
|
||||
callback open-artist(string);
|
||||
callback media-action(string, string, string);
|
||||
@@ -147,6 +204,13 @@ export component AlbumCollectionView inherits VerticalLayout {
|
||||
card-width: root.card-width;
|
||||
card-height: root.card-height;
|
||||
card-gap: root.card-gap;
|
||||
windowed: root.windowed;
|
||||
visible-top: root.visible-top;
|
||||
visible-height: root.visible-height;
|
||||
content-offset: root.content-offset;
|
||||
window-changed(first, last) => {
|
||||
root.window-changed(first, last);
|
||||
}
|
||||
open-album(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
|
||||
@@ -1169,6 +1169,18 @@ export component LocalLibraryView inherits Rectangle {
|
||||
show-favorite: false;
|
||||
// Always-visible source badge (local | qobuz-offline | plex).
|
||||
show-source-badge: true;
|
||||
// Viewport windowing (Phase 1): only cards near the
|
||||
// visible band mount + get artwork dispatched. The
|
||||
// offset is the page's padding-top (16px); the grid's
|
||||
// one-viewport margin absorbs the small extras
|
||||
// (multi-select bar) above the grid.
|
||||
windowed: true;
|
||||
visible-top: -albums-flick.viewport-y;
|
||||
visible-height: albums-flick.height;
|
||||
content-offset: 16px;
|
||||
window-changed(first, last) => {
|
||||
LocalLibraryActions.albums-window-changed(first, last);
|
||||
}
|
||||
open-album(id) => {
|
||||
LocalLibraryActions.open-album(id);
|
||||
}
|
||||
|
||||
+24
-13
@@ -31,7 +31,8 @@ pub const MAX_CACHE_BYTES: u64 = 200 * 1024 * 1024;
|
||||
pub type ImageCache = Arc<Mutex<Option<ImageCacheService>>>;
|
||||
|
||||
/// Which card an artwork download targets.
|
||||
#[derive(Clone, Copy)]
|
||||
/// (`Clone` only — `LocalAlbumById` carries a `String`, so no `Copy`.)
|
||||
#[derive(Clone)]
|
||||
pub enum ArtworkTarget {
|
||||
/// A card in a Discover descriptor list's embedded album section
|
||||
/// (`DiscoverState.home-sections` / `editor-sections`
|
||||
@@ -123,10 +124,11 @@ pub enum ArtworkTarget {
|
||||
FavoriteAlbum { index: usize },
|
||||
/// A card in DiscoverBrowseState.albums[index].
|
||||
DiscoverBrowseAlbum { index: usize },
|
||||
/// A card in LocalLibraryState.albums[index] (Local Library grid). `gen`
|
||||
/// is the albums generation at fetch time; a stale cover (the model was
|
||||
/// replaced by a search/sort/retry) is dropped on apply.
|
||||
LocalAlbumCard { index: usize, gen: u64 },
|
||||
/// A Local Library album cover, addressed BY ID (windowed dispatch over
|
||||
/// `albums-visible` — id-keyed delivery is immune to derive re-sorts
|
||||
/// between dispatch and apply). `gen` is the albums generation at fetch
|
||||
/// time; a stale cover (the model was replaced by a reload) is dropped.
|
||||
LocalAlbumById { id: String, gen: u64 },
|
||||
/// A card in LocalLibraryState.folders[index] (Folders-flat grid).
|
||||
LocalFolderCard { index: usize },
|
||||
/// A subfolder cover card in LocalLibraryState.folder-detail-subfolders[index]
|
||||
@@ -448,7 +450,17 @@ pub fn spawn_local_or_plex_loads(
|
||||
} else {
|
||||
ArtworkRef::LocalFile(job.url.clone())
|
||||
};
|
||||
let (pixels, width, height) = fetch_and_decode_ref(&art, &cache, decode_size).await?;
|
||||
let Some((pixels, width, height)) =
|
||||
fetch_and_decode_ref(&art, &cache, decode_size).await
|
||||
else {
|
||||
// Failed fetch/decode never reaches apply_artwork — free the
|
||||
// windowed-dispatch dedupe slot here so a later band pass can
|
||||
// retry this cover instead of skipping it for the session.
|
||||
if let ArtworkTarget::LocalAlbumById { id, .. } = &job.target {
|
||||
crate::local_library::album_artwork_job_done(id);
|
||||
}
|
||||
return None;
|
||||
};
|
||||
let target = job.target;
|
||||
let url = job.url;
|
||||
let _ = window.upgrade_in_event_loop(move |w| {
|
||||
@@ -1232,17 +1244,16 @@ fn apply_artwork(
|
||||
.global::<crate::PurchaseDetailState>()
|
||||
.set_artwork(image);
|
||||
}
|
||||
ArtworkTarget::LocalAlbumCard { index, gen } => {
|
||||
ArtworkTarget::LocalAlbumById { id, gen } => {
|
||||
// The job is done either way — free its in-flight slot so the
|
||||
// window dispatcher can re-request it after an eviction.
|
||||
crate::local_library::album_artwork_job_done(&id);
|
||||
// Drop the cover if a reload superseded the set it belongs to.
|
||||
if !crate::local_library::albums_gen_current(gen) {
|
||||
return;
|
||||
}
|
||||
let model = window.global::<crate::LocalLibraryState>().get_albums();
|
||||
if let Some(item) = model.row_data(index) {
|
||||
// Dual-set by id onto the full set + visible + grouped sections.
|
||||
let id = item.id.to_string();
|
||||
crate::local_library::set_local_album_artwork(window, &id, image);
|
||||
}
|
||||
// Set by id onto the full set + visible + grouped sections.
|
||||
crate::local_library::set_local_album_artwork(window, &id, image);
|
||||
}
|
||||
ArtworkTarget::LocalFolderCard { index } => {
|
||||
let model = window.global::<crate::LocalLibraryState>().get_folders();
|
||||
|
||||
+212
-21
@@ -273,17 +273,103 @@ fn album_matches_filters(a: &qbz_library::LocalAlbum, f: &AlbumFilter) -> bool {
|
||||
passes_q && passes_f && passes_s
|
||||
}
|
||||
|
||||
/// Build LocalAlbumCard artwork jobs for the full `albums` set (gen-stamped).
|
||||
fn album_artwork_jobs(cards: &[crate::album_map::AlbumCard], gen: u64) -> Vec<ArtworkJob> {
|
||||
cards
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| !c.artwork_url.is_empty())
|
||||
.map(|(i, c)| ArtworkJob {
|
||||
target: ArtworkTarget::LocalAlbumCard { index: i, gen },
|
||||
url: c.artwork_url.clone(),
|
||||
})
|
||||
.collect()
|
||||
/// Last row band reported by the windowed albums grid (item indices into
|
||||
/// `albums-visible`, prefetch margin already included by the grid). Kept so
|
||||
/// model rebuilds (load/derive) can re-dispatch — the grid only fires its
|
||||
/// callback when the band CHANGES, not when the rows under it change.
|
||||
static ALBUMS_WINDOW: std::sync::Mutex<(usize, usize)> = std::sync::Mutex::new((0, 59));
|
||||
|
||||
/// Cover ids currently in the artwork pipeline for the albums window
|
||||
/// (dedupe during fast scroll). Freed on apply; cleared on reloads.
|
||||
fn albums_inflight() -> &'static std::sync::Mutex<std::collections::HashSet<String>> {
|
||||
static S: std::sync::OnceLock<std::sync::Mutex<std::collections::HashSet<String>>> =
|
||||
std::sync::OnceLock::new();
|
||||
S.get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()))
|
||||
}
|
||||
|
||||
/// Plex params + image-cache handle captured at load time so the window
|
||||
/// dispatcher can spawn artwork jobs outside the load path.
|
||||
fn albums_dispatch_ctx() -> &'static std::sync::Mutex<Option<(String, String, ImageCache)>> {
|
||||
static S: std::sync::OnceLock<std::sync::Mutex<Option<(String, String, ImageCache)>>> =
|
||||
std::sync::OnceLock::new();
|
||||
S.get_or_init(|| std::sync::Mutex::new(None))
|
||||
}
|
||||
|
||||
/// A windowed artwork job finished (applied or dropped) — free its slot so
|
||||
/// the dispatcher can request the id again after an eviction.
|
||||
pub fn album_artwork_job_done(id: &str) {
|
||||
albums_inflight().lock().unwrap().remove(id);
|
||||
}
|
||||
|
||||
/// The windowed grid reported a new visible row band.
|
||||
pub fn albums_window_changed(window: &AppWindow, first: i32, last: i32) {
|
||||
let first = first.max(0) as usize;
|
||||
let last = last.max(first as i32) as usize;
|
||||
*ALBUMS_WINDOW.lock().unwrap() = (first, last);
|
||||
dispatch_albums_window(window);
|
||||
}
|
||||
|
||||
/// Dispatch covers for the current albums window (over `albums-visible`) and
|
||||
/// evict decoded covers far outside it back to the placeholder, so cover RAM
|
||||
/// scales with the viewport instead of the library. Delivery is id-keyed
|
||||
/// (`LocalAlbumById`), so a derive re-sort between dispatch and apply cannot
|
||||
/// land a cover on the wrong card; a full reload bumps `ALBUMS_GEN` and the
|
||||
/// apply arm drops the stale image.
|
||||
pub fn dispatch_albums_window(window: &AppWindow) {
|
||||
let (first, last) = *ALBUMS_WINDOW.lock().unwrap();
|
||||
let Some((base_url, token, image_cache)) = albums_dispatch_ctx().lock().unwrap().clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let gen = ALBUMS_GEN.load(Ordering::SeqCst);
|
||||
let s = window.global::<LocalLibraryState>();
|
||||
let visible = s.get_albums_visible();
|
||||
let len = visible.row_count();
|
||||
if len == 0 {
|
||||
return;
|
||||
}
|
||||
let last = last.min(len - 1);
|
||||
if first > last {
|
||||
return;
|
||||
}
|
||||
// Retention = the window plus one window-span on each side. Beyond it,
|
||||
// covers return to the placeholder; re-entry is cheap (byte-budgeted
|
||||
// decoded cache, else a bounded 264px re-decode from the source file).
|
||||
let span = last - first + 1;
|
||||
let keep_lo = first.saturating_sub(span);
|
||||
let keep_hi = (last + span).min(len - 1);
|
||||
let mut jobs = Vec::new();
|
||||
{
|
||||
let mut inflight = albums_inflight().lock().unwrap();
|
||||
for vi in first..=last {
|
||||
let Some(item) = visible.row_data(vi) else { continue };
|
||||
if item.artwork.size().width > 0 || item.artwork_url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let id = item.id.to_string();
|
||||
if inflight.insert(id.clone()) {
|
||||
jobs.push(ArtworkJob {
|
||||
target: ArtworkTarget::LocalAlbumById { id, gen },
|
||||
url: item.artwork_url.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for vi in (0..keep_lo).chain(keep_hi + 1..len) {
|
||||
let Some(item) = visible.row_data(vi) else { continue };
|
||||
if item.artwork.size().width > 0 {
|
||||
set_local_album_artwork(window, item.id.as_str(), slint::Image::default());
|
||||
}
|
||||
}
|
||||
if !jobs.is_empty() {
|
||||
crate::artwork::spawn_local_or_plex_loads(
|
||||
jobs,
|
||||
base_url,
|
||||
token,
|
||||
window.as_weak(),
|
||||
image_cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a freshly-decoded local-album cover (by id) on every rendered model:
|
||||
@@ -380,6 +466,16 @@ pub fn derive_albums(window: &AppWindow) {
|
||||
s.set_albums_visible(ModelRc::new(VecModel::from(filtered)));
|
||||
s.set_albums_grouped(empty_sections());
|
||||
s.set_albums_alpha(ModelRc::new(VecModel::from(Vec::<AlphaJump>::new())));
|
||||
if s.get_albums_view_mode() == "list" {
|
||||
// The LIST view renders the same albums-visible model but is NOT
|
||||
// windowed (only AlbumGrid fires window-changed) — dispatch every
|
||||
// missing cover and do no eviction, like pre-windowing.
|
||||
dispatch_albums_all_visible(window);
|
||||
} else {
|
||||
// The rows under the window band changed (search/sort/filter) but
|
||||
// the band itself didn't — the grid won't re-fire, so re-dispatch.
|
||||
dispatch_albums_window(window);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -426,6 +522,100 @@ pub fn derive_albums(window: &AppWindow) {
|
||||
s.set_albums_grouped(ModelRc::new(VecModel::from(sections)));
|
||||
s.set_albums_alpha(ModelRc::new(VecModel::from(jumps)));
|
||||
s.set_albums_visible(ModelRc::new(VecModel::from(Vec::<AlbumCardItem>::new())));
|
||||
dispatch_albums_all_grouped(window);
|
||||
}
|
||||
|
||||
/// Flat LIST-view artwork: same `albums-visible` model as the grid, but the
|
||||
/// list is not windowed (only `AlbumGrid` fires `window-changed`) — dispatch
|
||||
/// every missing cover, no eviction. Phase 1 limit.
|
||||
fn dispatch_albums_all_visible(window: &AppWindow) {
|
||||
let Some((base_url, token, image_cache)) = albums_dispatch_ctx().lock().unwrap().clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let gen = ALBUMS_GEN.load(Ordering::SeqCst);
|
||||
let s = window.global::<LocalLibraryState>();
|
||||
let visible = s.get_albums_visible();
|
||||
let mut jobs = Vec::new();
|
||||
{
|
||||
let mut inflight = albums_inflight().lock().unwrap();
|
||||
for vi in 0..visible.row_count() {
|
||||
let Some(item) = visible.row_data(vi) else { continue };
|
||||
if item.artwork.size().width > 0 || item.artwork_url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let id = item.id.to_string();
|
||||
if inflight.insert(id.clone()) {
|
||||
jobs.push(ArtworkJob {
|
||||
target: ArtworkTarget::LocalAlbumById { id, gen },
|
||||
url: item.artwork_url.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if !jobs.is_empty() {
|
||||
crate::artwork::spawn_local_or_plex_loads(
|
||||
jobs,
|
||||
base_url,
|
||||
token,
|
||||
window.as_weak(),
|
||||
image_cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Albums grid/list view-mode toggled. Switching TO the list needs a full
|
||||
/// dispatch (the list is not windowed and the grid's window may have evicted
|
||||
/// covers the list now shows); switching to the grid is handled by AlbumGrid's
|
||||
/// own `init => notify-window()` on mount.
|
||||
pub fn albums_view_mode_changed(window: &AppWindow) {
|
||||
let s = window.global::<LocalLibraryState>();
|
||||
if s.get_albums_group() == "off" && s.get_albums_view_mode() == "list" {
|
||||
dispatch_albums_all_visible(window);
|
||||
}
|
||||
}
|
||||
|
||||
/// Grouped-mode artwork: `albums-visible` is EMPTY there (the sections render
|
||||
/// from `albums-grouped`), so the viewport window doesn't apply — keep the
|
||||
/// pre-windowing behavior and dispatch every missing cover. Phase 1 limit;
|
||||
/// per-section windowing needs the sections' content offsets.
|
||||
fn dispatch_albums_all_grouped(window: &AppWindow) {
|
||||
let Some((base_url, token, image_cache)) = albums_dispatch_ctx().lock().unwrap().clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let gen = ALBUMS_GEN.load(Ordering::SeqCst);
|
||||
let s = window.global::<LocalLibraryState>();
|
||||
let grouped = s.get_albums_grouped();
|
||||
let mut jobs = Vec::new();
|
||||
{
|
||||
let mut inflight = albums_inflight().lock().unwrap();
|
||||
for gi in 0..grouped.row_count() {
|
||||
let Some(sec) = grouped.row_data(gi) else { continue };
|
||||
for i in 0..sec.albums.row_count() {
|
||||
let Some(item) = sec.albums.row_data(i) else { continue };
|
||||
if item.artwork.size().width > 0 || item.artwork_url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let id = item.id.to_string();
|
||||
if inflight.insert(id.clone()) {
|
||||
jobs.push(ArtworkJob {
|
||||
target: ArtworkTarget::LocalAlbumById { id, gen },
|
||||
url: item.artwork_url.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !jobs.is_empty() {
|
||||
crate::artwork::spawn_local_or_plex_loads(
|
||||
jobs,
|
||||
base_url,
|
||||
token,
|
||||
window.as_weak(),
|
||||
image_cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all quality/format/source filters, then re-derive.
|
||||
@@ -568,23 +758,21 @@ fn spawn_albums_load(
|
||||
match loaded {
|
||||
Some((albums, cards)) => {
|
||||
*local_albums() = albums;
|
||||
let jobs = album_artwork_jobs(&cards, gen);
|
||||
let items: Vec<AlbumCardItem> =
|
||||
cards.into_iter().map(crate::album_map::to_item).collect();
|
||||
s.set_albums(ModelRc::new(VecModel::from(items.clone())));
|
||||
s.set_album_count(items.len() as i32);
|
||||
s.set_albums_loading(false);
|
||||
s.set_albums_load_failed(false);
|
||||
// WINDOWED artwork (was: a job for every album in the
|
||||
// library). Stash the dispatch context BEFORE derive —
|
||||
// derive dispatches the covers itself (flat = viewport
|
||||
// band via dispatch_albums_window; grouped = full set).
|
||||
// The spawn is Plex-aware: /library/... → PlexThumb,
|
||||
// else LocalFile.
|
||||
*albums_dispatch_ctx().lock().unwrap() =
|
||||
Some((plex.base_url.clone(), plex.token.clone(), image_cache));
|
||||
derive_albums(&w);
|
||||
// Route covers through the Plex-aware spawn: Plex rows carry
|
||||
// a /library/... path → PlexThumb; local rows → LocalFile.
|
||||
crate::artwork::spawn_local_or_plex_loads(
|
||||
jobs,
|
||||
plex.base_url.clone(),
|
||||
plex.token.clone(),
|
||||
w.as_weak(),
|
||||
image_cache,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
s.set_albums_loading(false);
|
||||
@@ -603,6 +791,9 @@ pub fn reload_albums(
|
||||
) {
|
||||
let _ = weak.upgrade_in_event_loop(move |w| {
|
||||
let gen = ALBUMS_GEN.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
// The gen bump orphans every in-flight windowed job (dropped on
|
||||
// apply) — free their dedupe slots so the new set can re-request.
|
||||
albums_inflight().lock().unwrap().clear();
|
||||
let s = w.global::<LocalLibraryState>();
|
||||
s.set_albums_loading(true);
|
||||
s.set_albums_load_failed(false);
|
||||
|
||||
@@ -5829,6 +5829,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
log::warn!("[qbz-slint] select_bundled_translation('{lang}') failed: {e:?}");
|
||||
}
|
||||
reseed_i18n_labels(&window);
|
||||
// Shader scenes need the wgpu underlay: on the femtovg-GL / software tiers
|
||||
// they would render black, so hide them from the immersive picker + `g`.
|
||||
window
|
||||
.global::<ImmersiveState>()
|
||||
.set_shader_scenes_available(use_gpu_renderer);
|
||||
// Tell the user their renderer override was rolled back (set in
|
||||
// renderer_tier_from_prefs when the previous start died before painting).
|
||||
if RENDERER_REVERTED.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
@@ -14867,6 +14872,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
window
|
||||
.global::<LocalLibraryActions>()
|
||||
.on_albums_window_changed(move |first, last| {
|
||||
// Windowed albums grid: dispatch covers for the reported row
|
||||
// band and evict the ones far outside it.
|
||||
if let Some(w) = weak.upgrade() {
|
||||
local_library::albums_window_changed(&w, first, last);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
window
|
||||
@@ -14896,6 +14913,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.on_albums_set_view(move |mode| {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.global::<LocalLibraryState>().set_albums_view_mode(mode);
|
||||
// Switching to the (non-windowed) list view needs covers
|
||||
// the grid's window may have evicted.
|
||||
local_library::albums_view_mode_changed(&w);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user