diff --git a/crates/qbz-qt/build.rs b/crates/qbz-qt/build.rs index 1f030191e..0b3b7038d 100644 --- a/crates/qbz-qt/build.rs +++ b/crates/qbz-qt/build.rs @@ -43,7 +43,7 @@ fn main() { .qt_module("QuickControls2") .qml_module(QmlModule { uri: "com.blitzfc.qbz", - rust_files: &["src/bridge.rs", "src/session_bridge.rs", "src/shell_bridge.rs", "src/player_bridge.rs", "src/queue_bridge.rs", "src/home_bridge.rs", "src/viz_bridge.rs", "src/local_bridge.rs", "src/library_bridge.rs", "src/album_bridge.rs", "src/artist_bridge.rs", "src/lyrics_qt.rs", "src/icon_tint_qt.rs"], + rust_files: &["src/bridge.rs", "src/session_bridge.rs", "src/shell_bridge.rs", "src/player_bridge.rs", "src/queue_bridge.rs", "src/home_bridge.rs", "src/viz_bridge.rs", "src/local_bridge.rs", "src/library_bridge.rs", "src/album_bridge.rs", "src/artist_bridge.rs", "src/lyrics_qt.rs", "src/icon_tint_qt.rs", "src/cast_bridge.rs"], qml_files: &[ "qml/LoginScreen.qml", "qml/Main.qml", @@ -82,6 +82,7 @@ fn main() { "qml/settings/SettingsView.qml", "qml/shell/AmbientField.qml", "qml/shell/AppShell.qml", + "qml/shell/ArtPreviewOverlay.qml", "qml/shell/Cortinilla.qml", "qml/shell/HeaderBar.qml", "qml/shell/LyricsPanel.qml", @@ -105,6 +106,7 @@ fn main() { "qml/views/PlaylistView.qml", "qml/views/SearchView.qml", "qml/views/SectionRail.qml", + "qml/shell/CastPicker.qml", "qml/controls/BrowseGenreButton.qml", "qml/controls/QbzToggleButton.qml", "qml/controls/ViewModeToggle.qml", diff --git a/crates/qbz-qt/qml/shell/AppShell.qml b/crates/qbz-qt/qml/shell/AppShell.qml index 72c0f6764..53bb02001 100644 --- a/crates/qbz-qt/qml/shell/AppShell.qml +++ b/crates/qbz-qt/qml/shell/AppShell.qml @@ -317,4 +317,9 @@ Rectangle { } } } + + // LAST child = above every surface, exactly like ArtPreviewOverlay.slint's + // mount in AppShell.slint. Non-interactive, so it never steals the hover + // that is keeping it open (see the file header). + ArtPreviewOverlay { } } diff --git a/crates/qbz-qt/qml/shell/ArtPreviewOverlay.qml b/crates/qbz-qt/qml/shell/ArtPreviewOverlay.qml new file mode 100644 index 000000000..6d8a30f99 --- /dev/null +++ b/crates/qbz-qt/qml/shell/ArtPreviewOverlay.qml @@ -0,0 +1,71 @@ +// Global floating album-art preview — a 220x220 cover that floats ABOVE the +// now-playing bar's small cover while the pointer hovers it. 1:1 port of +// ArtPreviewOverlay.slint. +// +// Two properties of the original are load-bearing and kept exactly: +// +// 1. It is mounted LAST in AppShell and fills the window, so it paints over +// every surface without needing a z fight. +// 2. It is NON-INTERACTIVE — no MouseArea anywhere. The Slint tried a +// PopupWindow first and it STOLE THE HOVER from the trigger (the cover's +// has-hover flipped the moment the popup appeared, which closed it, which +// restored the hover…), so the preview flickered. An overlay that cannot +// receive events cannot do that; clicks also pass straight through to +// whatever is underneath. +// +// The state (anchor + visibility) rides QbzShell, like the drag ghost in the +// same file — the trigger is inside the bar and the overlay is a sibling of +// the whole shell, so they cannot see each other's ids. + +import QtQuick +import com.blitzfc.qbz +import "../theme" + +Item { + id: root + anchors.fill: parent + visible: QbzShell.artPreviewShow && QbzPlayer.npHasTrack + + QbzTheme { id: theme } + + // 232 = the 220 image plus a 6px frame on each side (ArtPreviewOverlay + // .slint:16 `card`). + readonly property int card: 232 + + Rectangle { + width: root.card + height: root.card + // Centred on the cover, clamped 8px inside the window, floating with + // its bottom edge 8px above the cover's top. + x: Math.max(8, Math.min(QbzShell.artPreviewX - width / 2, + Math.max(8, root.width - width - 8))) + y: QbzShell.artPreviewY - height - 8 + radius: 8 + color: theme.surfaceCard + border.width: 1 + border.color: theme.borderSubtle + clip: true + + // The .slint carries a 16px drop shadow; QtQuick's DropShadow is a + // shader effect and shader effects render NOTHING on this port's + // software path (the phase-2 finding that also killed ColorOverlay in + // QbzIcon), so the depth cue is a border instead of a fake blurred + // rectangle that would be wrong on both paths. + + Image { + x: 6 + y: 6 + width: 220 + height: 220 + fillMode: Image.PreserveAspectFit + // The .slint prefers `artwork-large` and falls back to `artwork`; + // this port publishes only one cover path (player_bridge + // .np_artwork_path), which is the cached original — 600px for a + // Qobuz cover, so the 220px preview is a downscale either way. + source: QbzPlayer.npArtworkPath + asynchronous: true + smooth: true + mipmap: true + } + } +} diff --git a/crates/qbz-qt/qml/shell/SongCard.qml b/crates/qbz-qt/qml/shell/SongCard.qml index 2405b0987..891329882 100644 --- a/crates/qbz-qt/qml/shell/SongCard.qml +++ b/crates/qbz-qt/qml/shell/SongCard.qml @@ -156,6 +156,28 @@ Rectangle { size: root.artSize * 0.5 } } + // Hover trigger for the floating 220px preview + // (SongCard.slint:296). Topmost so the cover, the idle glyph + // and the fetch scrim below it stay reachable; NoButton so it + // reports hover only and every click passes through to them. + // The anchor is published in WINDOW coordinates because the + // overlay lives at the shell root. + MouseArea { + id: artHover + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onContainsMouseChanged: { + if (containsMouse && QbzPlayer.npHasTrack) { + var pt = mapToItem(null, width / 2, 0) + QbzShell.artPreviewX = pt.x + QbzShell.artPreviewY = pt.y + QbzShell.artPreviewShow = true + } else if (!containsMouse) { + QbzShell.artPreviewShow = false + } + } + } } } Item { width: root.showArt ? root.artTextGap : 0; height: 1 } diff --git a/crates/qbz-qt/src/shell_bridge.rs b/crates/qbz-qt/src/shell_bridge.rs index b40c9fa40..3340e55a4 100644 --- a/crates/qbz-qt/src/shell_bridge.rs +++ b/crates/qbz-qt/src/shell_bridge.rs @@ -131,6 +131,15 @@ pub mod qbz_shell { #[qproperty(f32, drag_x)] #[qproperty(f32, drag_y)] #[qproperty(QString, drag_over_playlist_id)] + // Floating album-art preview over the now-playing bar's small cover + // (SongCard.slint:296 + ArtPreviewOverlay.slint). Anchor is in WINDOW + // coordinates: x = the cover's horizontal centre, y = its top edge. + // Carried here for the same reason the drag ghost is: the trigger + // lives inside the bar, the overlay must paint above every surface, and + // the two are in different .qml files. + #[qproperty(bool, art_preview_show)] + #[qproperty(f32, art_preview_x)] + #[qproperty(f32, art_preview_y)] type QbzShell = super::QbzShellRust; @@ -279,6 +288,9 @@ pub struct QbzShellRust { drag_x: f32, drag_y: f32, drag_over_playlist_id: QString, + art_preview_show: bool, + art_preview_x: f32, + art_preview_y: f32, } impl Default for QbzShellRust { @@ -322,6 +334,9 @@ impl Default for QbzShellRust { drag_x: 0.0, drag_y: 0.0, drag_over_playlist_id: QString::default(), + art_preview_show: false, + art_preview_x: 0.0, + art_preview_y: 0.0, } } }