fix(library): tray-restore crash, local art/badges, LabelCard, live pins, rich list rows
- Fix the tray-restore wgpu crash (pre-existing, amplified by the app-wide background): clear ImmersiveState.shader-texture on RenderingTeardown so the first restored frame never hands a cross-instance stale texture to the new femtovg canvas. - Local/Plex feed covers now decode: route the "All" artwork jobs through spawn_search_loads (http / local fs / Plex thumb) at all four dispatch sites; show the source badge on the feed's album/track cards (AlbumCard maps plain "qobuz" to the Qobuz logo). - New card-sized LabelCard (200x246), used by the feed + the Favorites Labels tab (replaces the round FavLabelCard). - Artist pin badge updates live in the Favorites Artists grid: add is-pinned to FavoriteArtistItem, stamp it, bind it in the ArtistGridCard, and flip the favorites artist models in set_artist_row_pinned. - Rich "All" list view: type-aware subtitle, quality badge (album/track), source icon (local/Plex), favorite/follow indicator, and a per-kind context menu (⋯ button + right-click). cc
This commit is contained in:
6 files changed
+374
-54
No files matched your search
@@ -339,14 +339,14 @@ export component AlbumCard inherits Rectangle {
|
||||
Image {
|
||||
source: root.album.source == "plex"
|
||||
? @image-url("../assets/icons/plex-logo.svg")
|
||||
: (root.album.source == "qobuz_download" || root.album.source == "qobuz_purchase"
|
||||
: (root.album.source == "qobuz_download" || root.album.source == "qobuz_purchase" || root.album.source == "qobuz"
|
||||
? @image-url("../assets/icons/qobuz-logo-filled.svg")
|
||||
: @image-url("../assets/icons/hard-drive.svg"));
|
||||
colorize: root.album.source == "qobuz_purchase" ? #eab308
|
||||
: (root.album.source == "local" ? #ffffff : transparent);
|
||||
width: root.album.source == "plex" ? 16px
|
||||
: (root.album.source == "qobuz_download" ? 22px
|
||||
: (root.album.source == "qobuz_purchase" ? 18px : 14px));
|
||||
: (root.album.source == "qobuz_purchase" || root.album.source == "qobuz" ? 18px : 14px));
|
||||
height: self.width;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Label card — the album card's twin for record labels: a 200x200 square
|
||||
// cover (the label logo, contain-fit over an elevated placeholder, or an
|
||||
// indigo→violet gradient + disc glyph when no logo), then a title / album-count
|
||||
// stack. No hover overlay buttons (a label is opened, not played). Matches the
|
||||
// 200x246 footprint of AlbumCard / PlaylistCard so a mixed grid stays aligned.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { Spacing } from "../foundation/spacing.slint";
|
||||
import { FavoriteLabelItem } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
export component LabelCard inherits Rectangle {
|
||||
in property <FavoriteLabelItem> label;
|
||||
// (id, name) — opens the label landing page.
|
||||
callback clicked(string, string);
|
||||
|
||||
in property <length> card-height: 246px;
|
||||
width: 200px;
|
||||
height: root.card-height;
|
||||
|
||||
VerticalLayout {
|
||||
alignment: start;
|
||||
spacing: 0px;
|
||||
|
||||
// --- Logo -------------------------------------------------------
|
||||
Rectangle {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: Radius.sm;
|
||||
background: root.label.image-url == ""
|
||||
? @linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)
|
||||
: Theme.surface-elevated;
|
||||
clip: true;
|
||||
|
||||
// Logos have varied/transparent backgrounds — contain-fit so they
|
||||
// are never cropped (unlike album art, which is cover-fit).
|
||||
Image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
source: root.label.image;
|
||||
image-fit: contain;
|
||||
}
|
||||
if root.label.image-url == "": QbzIcon {
|
||||
source: @image-url("../assets/icons/disc-3.svg");
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
tint: #ffffff;
|
||||
}
|
||||
|
||||
// Hover scrim (subtle — signals the card is clickable).
|
||||
Rectangle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000000;
|
||||
opacity: ta.has-hover ? 0.25 : 0.0;
|
||||
animate opacity { duration: 150ms; }
|
||||
}
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.clicked(root.label.id, root.label.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 6px; }
|
||||
|
||||
// --- Name / album count -----------------------------------------
|
||||
HorizontalLayout {
|
||||
height: 40px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 2px;
|
||||
horizontal-stretch: 1;
|
||||
|
||||
title-area := TouchArea {
|
||||
height: 20px;
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.clicked(root.label.id, root.label.name);
|
||||
}
|
||||
Text {
|
||||
width: 100%;
|
||||
text: root.label.name;
|
||||
color: title-area.has-hover ? Theme.accent : Theme.text-primary;
|
||||
font-size: Typography.body - 2px;
|
||||
font-weight: Typography.medium;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
if root.label.albums-line != "": Text {
|
||||
width: 100%;
|
||||
text: root.label.albums-line;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.link - 1px;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { AlbumCard } from "../discover/AlbumCard.slint";
|
||||
import { TrackCard } from "../discover/TrackCard.slint";
|
||||
import { ArtistGridCard } from "../discover/ArtistGridCard.slint";
|
||||
import { PlaylistCard } from "../discover/PlaylistCard.slint";
|
||||
import { LabelCard } from "../discover/LabelCard.slint";
|
||||
import { GenreFilterPopup } from "../discover/GenreFilterPopup.slint";
|
||||
import { TrackRow } from "../primitives/TrackRow.slint";
|
||||
import { ExpandableSearch } from "../primitives/ExpandableSearch.slint";
|
||||
@@ -38,6 +39,9 @@ import { MultiSelectBar } from "../primitives/MultiSelectBar.slint";
|
||||
import { ToggleButton } from "../primitives/ToggleButton.slint";
|
||||
import { QbzSelect } from "../primitives/QbzSelect.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { QualityBadge } from "../primitives/QualityBadge.slint";
|
||||
import { ContextMenu } from "../primitives/ContextMenu.slint";
|
||||
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
|
||||
import { PlaylistListRow } from "../primitives/PlaylistListRow.slint";
|
||||
@@ -955,6 +959,7 @@ export component FavoritesView inherits Rectangle {
|
||||
height: all-grid.ch;
|
||||
|
||||
if item.kind == "track": TrackCard {
|
||||
show-source-badge: true;
|
||||
track: {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
@@ -976,6 +981,7 @@ export component FavoritesView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
if item.kind == "album": AlbumCard {
|
||||
show-source-badge: true;
|
||||
album: {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
@@ -984,6 +990,7 @@ export component FavoritesView inherits Rectangle {
|
||||
artwork: item.image,
|
||||
quality-tier: item.quality-tier,
|
||||
quality-label: item.quality-detail,
|
||||
source: item.source,
|
||||
};
|
||||
clicked(id) => { FavoritesActions.open-album(id); }
|
||||
media-action(kind, id, action) => {
|
||||
@@ -1021,8 +1028,8 @@ export component FavoritesView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
if item.kind == "label": FavLabelCard {
|
||||
item: {
|
||||
if item.kind == "label": LabelCard {
|
||||
label: {
|
||||
id: item.id,
|
||||
name: item.title,
|
||||
image: item.image,
|
||||
@@ -1043,47 +1050,13 @@ export component FavoritesView inherits Rectangle {
|
||||
width: parent.width - 64px;
|
||||
spacing: 2px;
|
||||
for item[i] in LibraryAllState.items-visible: Rectangle {
|
||||
height: 52px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
background: row-ta.has-hover ? Theme.surface-hover : transparent;
|
||||
HorizontalLayout {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
spacing: 12px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: item.kind == "artist" ? 20px : 4px;
|
||||
background: Theme.surface-elevated;
|
||||
clip: true;
|
||||
Image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
source: item.image;
|
||||
image-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
Text {
|
||||
text: item.title;
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.medium;
|
||||
overflow: elide;
|
||||
}
|
||||
Text {
|
||||
text: item.subtitle;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
background: row-ta.has-hover || menu-btn-ta.has-hover
|
||||
? Theme.surface-hover : transparent;
|
||||
// Row body — click plays/opens by kind; right-click
|
||||
// opens the context menu. Declared FIRST so the action
|
||||
// controls on the right win the pointer over it.
|
||||
row-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
@@ -1099,6 +1072,200 @@ export component FavoritesView inherits Rectangle {
|
||||
FavoritesActions.open-label(item.id, item.title);
|
||||
}
|
||||
}
|
||||
pointer-event(e) => {
|
||||
if (e.kind == PointerEventKind.down
|
||||
&& e.button == PointerEventButton.right) {
|
||||
row-menu.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
spacing: 12px;
|
||||
// Cover (round for artists, square otherwise).
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: item.kind == "artist" ? 22px : 6px;
|
||||
background: Theme.surface-elevated;
|
||||
clip: true;
|
||||
Image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
source: item.image;
|
||||
image-fit: item.kind == "label" ? contain : cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Title + type-aware subtitle.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
spacing: 2px;
|
||||
Text {
|
||||
text: item.title;
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.medium;
|
||||
overflow: elide;
|
||||
}
|
||||
Text {
|
||||
text: item.kind == "artist" ? @tr("Artist")
|
||||
: item.kind == "album"
|
||||
? (item.artist != "" ? @tr("Album") + " • " + item.artist : @tr("Album"))
|
||||
: item.kind == "track"
|
||||
? (item.artist != "" ? @tr("Track") + " • " + item.artist : @tr("Track"))
|
||||
: item.kind == "playlist"
|
||||
? (item.subtitle != "" ? @tr("Playlist") + " • " + item.subtitle : @tr("Playlist"))
|
||||
: (item.subtitle != "" ? @tr("Label") + " • " + item.subtitle : @tr("Label"));
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
// Quality badge (albums + tracks).
|
||||
if item.kind == "album" || item.kind == "track": VerticalLayout {
|
||||
alignment: center;
|
||||
QualityBadge {
|
||||
tier: item.quality-tier;
|
||||
label: item.quality-detail;
|
||||
detail: item.quality-detail;
|
||||
}
|
||||
}
|
||||
// Source badge (local / Plex).
|
||||
if item.source == "local" || item.source == "plex": VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: #000000b3;
|
||||
Image {
|
||||
source: item.source == "plex"
|
||||
? @image-url("../assets/icons/plex-logo.svg")
|
||||
: @image-url("../assets/icons/hard-drive.svg");
|
||||
colorize: item.source == "local" ? #ffffff : transparent;
|
||||
width: item.source == "plex" ? 15px : 14px;
|
||||
height: self.width;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
image-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Favorite (heart) / follow (check) indicator.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: item.kind == "artist"
|
||||
? @image-url("../assets/icons/user-plus.svg")
|
||||
: (item.is-favorite
|
||||
? @image-url("../assets/icons/heart-filled.svg")
|
||||
: @image-url("../assets/icons/heart.svg"));
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
tint: Theme.accent;
|
||||
}
|
||||
}
|
||||
// Context-menu button.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
menu-btn := Rectangle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 6px;
|
||||
background: menu-btn-ta.has-hover ? Theme.surface-elevated : transparent;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/ellipsis.svg");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
tint: Theme.text-secondary;
|
||||
}
|
||||
menu-btn-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { row-menu.show(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Per-kind context menu (⋯ button + right-click).
|
||||
row-menu := PopupWindow {
|
||||
x: parent.width - 206px;
|
||||
y: 48px;
|
||||
width: 196px;
|
||||
height: (item.kind == "track" ? 6
|
||||
: item.kind == "album" ? 6
|
||||
: item.kind == "playlist" ? 4
|
||||
: 1) * 33px + 10px;
|
||||
ContextMenu {
|
||||
if item.kind == "track" || item.kind == "album" || item.kind == "playlist": ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/play.svg");
|
||||
label: @tr("Play");
|
||||
clicked => {
|
||||
if (item.kind == "track") { FavoritesActions.track-action(item.id, "play"); }
|
||||
else if (item.kind == "album") { FavoritesActions.album-action(item.id, "play"); }
|
||||
else { FavoritesActions.playlist-action(item.id, "play"); }
|
||||
}
|
||||
}
|
||||
if item.kind == "track" || item.kind == "album" || item.kind == "playlist": ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/list-plus.svg");
|
||||
label: @tr("Play next");
|
||||
clicked => {
|
||||
if (item.kind == "track") { FavoritesActions.track-action(item.id, "play-next"); }
|
||||
else if (item.kind == "album") { FavoritesActions.album-action(item.id, "play-next"); }
|
||||
else { FavoritesActions.playlist-action(item.id, "play-next"); }
|
||||
}
|
||||
}
|
||||
if item.kind == "track" || item.kind == "album" || item.kind == "playlist": ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/list-end.svg");
|
||||
label: @tr("Add to queue");
|
||||
clicked => {
|
||||
if (item.kind == "track") { FavoritesActions.track-action(item.id, "queue"); }
|
||||
else if (item.kind == "album") { FavoritesActions.album-action(item.id, "queue"); }
|
||||
else { FavoritesActions.playlist-action(item.id, "queue"); }
|
||||
}
|
||||
}
|
||||
if item.kind == "album": ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/library-big.svg");
|
||||
label: @tr("Open album");
|
||||
clicked => { FavoritesActions.open-album(item.id); }
|
||||
}
|
||||
if item.kind == "playlist": ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/library-big.svg");
|
||||
label: @tr("Open playlist");
|
||||
clicked => { FavoritesActions.open-playlist(item.id); }
|
||||
}
|
||||
if (item.kind == "track" || item.kind == "album") && item.artist-id != "": ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/user.svg");
|
||||
label: @tr("Go to artist");
|
||||
clicked => { FavoritesActions.open-artist(item.artist-id); }
|
||||
}
|
||||
if item.kind == "artist": ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/user.svg");
|
||||
label: @tr("Go to artist");
|
||||
clicked => { FavoritesActions.open-artist(item.id); }
|
||||
}
|
||||
if item.kind == "label": ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/disc-3.svg");
|
||||
label: @tr("Go to label");
|
||||
clicked => { FavoritesActions.open-label(item.id, item.title); }
|
||||
}
|
||||
if item.kind == "track" || item.kind == "album": ContextMenuItem {
|
||||
icon: item.is-favorite
|
||||
? @image-url("../assets/icons/heart-filled.svg")
|
||||
: @image-url("../assets/icons/heart.svg");
|
||||
label: item.is-favorite ? @tr("Remove from Library") : @tr("Add to Library");
|
||||
clicked => {
|
||||
if (item.kind == "track") { FavoritesActions.track-action(item.id, "favorite"); }
|
||||
else { FavoritesActions.album-action(item.id, "favorite"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1354,6 +1521,7 @@ export component FavoritesView inherits Rectangle {
|
||||
artwork-url: artist.image-url,
|
||||
artwork: artist.image,
|
||||
following: true,
|
||||
is-pinned: artist.is-pinned,
|
||||
};
|
||||
follow-mode: "none";
|
||||
clicked(id) => { root.open-artist(id); }
|
||||
@@ -1387,6 +1555,7 @@ export component FavoritesView inherits Rectangle {
|
||||
artwork-url: artist.image-url,
|
||||
artwork: artist.image,
|
||||
following: true,
|
||||
is-pinned: artist.is-pinned,
|
||||
};
|
||||
follow-mode: "none";
|
||||
clicked(id) => { root.open-artist(id); }
|
||||
@@ -1478,10 +1647,10 @@ export component FavoritesView inherits Rectangle {
|
||||
if FavoritesState.active-tab == "labels": labels-grid := Rectangle {
|
||||
property <int> columns: Math.max(
|
||||
1,
|
||||
Math.floor((self.width + root.gap) / (root.artist-w + root.gap)));
|
||||
Math.floor((self.width + root.gap) / (root.artist-card-w + root.gap)));
|
||||
property <int> rows: Math.ceil(FavoritesState.labels-visible.length / self.columns);
|
||||
height: self.rows > 0
|
||||
? self.rows * root.artist-h + Math.max(0, self.rows - 1) * root.gap
|
||||
? self.rows * root.artist-card-h + Math.max(0, self.rows - 1) * root.gap
|
||||
: 0px;
|
||||
if FavoritesState.labels.length == 0: Text {
|
||||
text: @tr("No labels in your Library yet.");
|
||||
@@ -1494,10 +1663,10 @@ export component FavoritesView inherits Rectangle {
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
}
|
||||
for label[i] in FavoritesState.labels-visible: FavLabelCard {
|
||||
x: Math.mod(i, labels-grid.columns) * (root.artist-w + root.gap);
|
||||
y: Math.floor(i / labels-grid.columns) * (root.artist-h + root.gap);
|
||||
item: label;
|
||||
for lbl[i] in FavoritesState.labels-visible: LabelCard {
|
||||
x: Math.mod(i, labels-grid.columns) * (root.artist-card-w + root.gap);
|
||||
y: Math.floor(i / labels-grid.columns) * (root.artist-card-h + root.gap);
|
||||
label: lbl;
|
||||
clicked(id, name) => { FavoritesActions.open-label(id, name); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4830,6 +4830,9 @@ export struct FavoriteArtistItem {
|
||||
name: string,
|
||||
image-url: string,
|
||||
image: image,
|
||||
// Pin badge state — kept live by main::set_artist_row_pinned when a pin
|
||||
// toggles anywhere (mirrors SlimItem.is-pinned).
|
||||
is-pinned: bool,
|
||||
}
|
||||
|
||||
// An A-Z section of favorite artists (grid grouped mode + sidepanel left
|
||||
|
||||
@@ -577,6 +577,7 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
|
||||
let cards: Vec<FavoriteArtistItem> = items
|
||||
.into_iter()
|
||||
.map(|a| FavoriteArtistItem {
|
||||
is_pinned: crate::pinned::is_pinned("artist", &a.id),
|
||||
id: a.id.into(),
|
||||
name: a.name.into(),
|
||||
image_url: a.image_url.into(),
|
||||
|
||||
+44
-4
@@ -1802,6 +1802,27 @@ fn set_artist_row_pinned(window: &AppWindow, artist_id: &str, pinned: bool) {
|
||||
flip(&reco.get_rec_artists_recent());
|
||||
flip(&reco.get_top_artists());
|
||||
flip(&window.global::<LabelState>().get_artists());
|
||||
// Library > Favorites artists grid uses FavoriteArtistItem (not SlimItem);
|
||||
// flip it too so the pin badge updates live there (grid + A-Z sections).
|
||||
let flip_fav = |model: &slint::ModelRc<FavoriteArtistItem>| {
|
||||
for i in 0..model.row_count() {
|
||||
if let Some(mut item) = model.row_data(i) {
|
||||
if item.id == artist_id && item.is_pinned != pinned {
|
||||
item.is_pinned = pinned;
|
||||
model.set_row_data(i, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let favs = window.global::<FavoritesState>();
|
||||
flip_fav(&favs.get_artists());
|
||||
flip_fav(&favs.get_artists_visible());
|
||||
let grouped = favs.get_artists_grouped();
|
||||
for gi in 0..grouped.row_count() {
|
||||
if let Some(sec) = grouped.row_data(gi) {
|
||||
flip_fav(&sec.artists);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed Capa B (intelligent-search ranking) from a RESULTS-PAGE click, but only
|
||||
@@ -3691,7 +3712,10 @@ fn navigate_library_all(
|
||||
let _ = weak.upgrade_in_event_loop(move |w| {
|
||||
library_all::apply_library_all(&w, feed);
|
||||
let jobs = library_all::artwork_jobs(&w);
|
||||
artwork::spawn_loads(jobs, weak_j.clone(), ic.clone());
|
||||
// Mixed payload (Qobuz http / local fs / Plex /library/) —
|
||||
// route each cover by scheme so local/Plex covers decode.
|
||||
let plex = crate::plex_settings::get();
|
||||
artwork::spawn_search_loads(jobs, plex.base_url, plex.token, weak_j.clone(), ic.clone());
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -7820,6 +7844,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// mode there is no WGPU28 GraphicsAPI to hook, so registering it would be a no-op
|
||||
// at best — skip it so software mode carries zero wgpu machinery.
|
||||
if use_gpu_renderer {
|
||||
let weak_for_underlay = window.as_weak();
|
||||
if let Err(e) = window
|
||||
.window()
|
||||
.set_rendering_notifier(move |state, graphics_api| {
|
||||
@@ -7831,6 +7856,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
slint::RenderingState::RenderingTeardown => {
|
||||
crate::shader_underlay::teardown();
|
||||
// The Image in ImmersiveState.shader-texture wraps a wgpu
|
||||
// texture from the instance/device being destroyed. The
|
||||
// property survives a Wayland hide (tray restore); the
|
||||
// first frame after the next RenderingSetup would hand that
|
||||
// stale texture to the NEW femtovg canvas — a cross-instance
|
||||
// id lookup that panics in wgpu-core ("TextureView … is no
|
||||
// longer alive"). Clear it now; the 30 fps drain repopulates
|
||||
// it on the new device within a tick.
|
||||
if let Some(w) = weak_for_underlay.upgrade() {
|
||||
w.global::<ImmersiveState>()
|
||||
.set_shader_texture(slint::Image::default());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -20747,7 +20784,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
w.global::<LibraryAllState>().set_search(q);
|
||||
library_all::derive(&w);
|
||||
let jobs = library_all::artwork_jobs(&w);
|
||||
artwork::spawn_loads(jobs, weak.clone(), image_cache.clone());
|
||||
let plex = crate::plex_settings::get();
|
||||
artwork::spawn_search_loads(jobs, plex.base_url, plex.token, weak.clone(), image_cache.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -20759,7 +20797,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
w.global::<LibraryAllState>().set_sort_by(key);
|
||||
library_all::derive(&w);
|
||||
let jobs = library_all::artwork_jobs(&w);
|
||||
artwork::spawn_loads(jobs, weak.clone(), image_cache.clone());
|
||||
let plex = crate::plex_settings::get();
|
||||
artwork::spawn_search_loads(jobs, plex.base_url, plex.token, weak.clone(), image_cache.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -20788,7 +20827,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
library_all::derive(&w);
|
||||
let jobs = library_all::artwork_jobs(&w);
|
||||
artwork::spawn_loads(jobs, weak.clone(), image_cache.clone());
|
||||
let plex = crate::plex_settings::get();
|
||||
artwork::spawn_search_loads(jobs, plex.base_url, plex.token, weak.clone(), image_cache.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user