feat(qbz-slint): real Discover Home view
After sign-in the app loads the real Qobuz discover index and renders the Home view: a time-based greeting with the user's name, the tab pill (Home / Editor's Picks / For You), and discover sections — New Releases, Press Accolades, Ideal Discography, Qobuzissimes, Albums of the Week — each a horizontally scrolling row of album cards. - state.slint: AlbumCardItem / DiscoverSection structs, HomeState global. - discover/: AlbumCard (artwork area, award ribbon, title, artist, quality badge), SectionRow, HomeView. - home.rs: discover-index fetch and mapping (award-ribbon priority, quality string), greeting, and event-loop model application. Unit tests for ribbon, quality, and greeting logic. - AppShell content region is now the HomeView. Album artwork images load in the next increment (artwork pipeline); cards currently show the elevated placeholder. No fixtures — all data is the live discover index. cc
This commit is contained in:
11 files changed
+576
-40
No files matched your search
@@ -8,6 +8,9 @@ import { Theme } from "foundation/semantic-colors.slint";
|
||||
import { LoginScreen } from "login/LoginScreen.slint";
|
||||
import { AppShell } from "shell/AppShell.slint";
|
||||
|
||||
// Re-export the HomeState global so the Rust layer can populate it.
|
||||
export { HomeState } from "state.slint";
|
||||
|
||||
// Which top-level screen is shown. During M1 the screen switches on a
|
||||
// login-button click without real authentication; M2 ties the switch
|
||||
// to a real session.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Album card for the Discover sections.
|
||||
//
|
||||
// Mirrors the Svelte AlbumCardLite: 220px column, square artwork with a
|
||||
// bottom-left award ribbon, single-line title, artist, and a compact
|
||||
// quality badge. Artwork image loading lands in the artwork-pipeline
|
||||
// increment; for now the artwork area is the same elevated placeholder
|
||||
// the real card shows before its image resolves.
|
||||
|
||||
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 { AlbumCardItem } from "../state.slint";
|
||||
|
||||
export component AlbumCard inherits Rectangle {
|
||||
in property <AlbumCardItem> album;
|
||||
|
||||
width: 220px;
|
||||
height: 282px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: Spacing.xs;
|
||||
|
||||
// --- Artwork ------------------------------------------------------
|
||||
Rectangle {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
border-radius: Radius.sm;
|
||||
background: Theme.surface-elevated;
|
||||
clip: true;
|
||||
|
||||
if root.album.ribbon != "": Rectangle {
|
||||
x: 0px;
|
||||
y: parent.height - self.height - 8px;
|
||||
height: 20px;
|
||||
background: root.album.ribbon-kind == "press"
|
||||
? #d49511
|
||||
: #000000e0;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
|
||||
// Left accent stripe (not on the gold press ribbon).
|
||||
Rectangle {
|
||||
x: 0px;
|
||||
width: root.album.ribbon-kind == "press" ? 0px : 3px;
|
||||
background: root.album.ribbon-kind == "qobuzissime"
|
||||
? #8b5cf6
|
||||
: #eab308;
|
||||
}
|
||||
HorizontalLayout {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
Text {
|
||||
text: root.album.ribbon;
|
||||
color: root.album.ribbon-kind == "press"
|
||||
? #1f1407
|
||||
: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: Typography.semibold;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Title --------------------------------------------------------
|
||||
Text {
|
||||
text: root.album.title;
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.medium;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
// --- Artist + quality --------------------------------------------
|
||||
HorizontalLayout {
|
||||
spacing: Spacing.sm;
|
||||
|
||||
Text {
|
||||
text: root.album.artist;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.link;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
if root.album.quality != "": Rectangle {
|
||||
background: Theme.surface-elevated;
|
||||
border-radius: 3px;
|
||||
HorizontalLayout {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
Text {
|
||||
text: root.album.quality;
|
||||
color: Theme.text-secondary;
|
||||
font-size: 9px;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Discover — Home view: greeting, tab pill, and the discover sections.
|
||||
//
|
||||
// Section data is supplied by the Rust home controller through the
|
||||
// HomeState global. The Editor's Picks / For You tabs are visual only
|
||||
// for now; switching their data is a later increment.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { HomeState } from "../state.slint";
|
||||
import { SectionRow } from "SectionRow.slint";
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
|
||||
component Tab inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> active: false;
|
||||
callback clicked();
|
||||
|
||||
border-radius: 4px;
|
||||
background: root.active ? Theme.surface-main
|
||||
: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
Text {
|
||||
text: root.label;
|
||||
color: root.active ? Theme.text-primary : Theme.text-muted;
|
||||
font-size: 13px;
|
||||
font-weight: Typography.medium;
|
||||
}
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component HomeView inherits Rectangle {
|
||||
background: Theme.surface-main;
|
||||
|
||||
ScrollView {
|
||||
VerticalLayout {
|
||||
padding: 32px;
|
||||
spacing: 28px;
|
||||
alignment: start;
|
||||
|
||||
Text {
|
||||
text: HomeState.greeting;
|
||||
visible: HomeState.greeting != "";
|
||||
color: Theme.text-primary;
|
||||
font-size: 26px;
|
||||
font-weight: Typography.semibold;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
alignment: start;
|
||||
Rectangle {
|
||||
background: Theme.surface-elevated;
|
||||
border-radius: 6px;
|
||||
HorizontalLayout {
|
||||
padding: 3px;
|
||||
spacing: 4px;
|
||||
Tab { label: @tr("Home"); active: true; }
|
||||
Tab { label: @tr("Editor's Picks"); }
|
||||
Tab { label: @tr("For You"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if HomeState.loading && HomeState.sections.length == 0: Text {
|
||||
text: @tr("Loading your music...");
|
||||
color: Theme.text-muted;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
for section in HomeState.sections: SectionRow {
|
||||
section: section;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// One Discover section — a title and a horizontally scrollable row of
|
||||
// album cards.
|
||||
//
|
||||
// The Svelte original paginates by slice; the MVP uses a horizontal
|
||||
// scroll, which is simpler and behaviorally equivalent for browsing.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { DiscoverSection } from "../state.slint";
|
||||
import { AlbumCard } from "AlbumCard.slint";
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
|
||||
export component SectionRow inherits VerticalLayout {
|
||||
in property <DiscoverSection> section;
|
||||
|
||||
spacing: 12px;
|
||||
|
||||
Text {
|
||||
text: root.section.title;
|
||||
color: Theme.text-primary;
|
||||
font-size: 18px;
|
||||
font-weight: Typography.semibold;
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
height: 290px;
|
||||
HorizontalLayout {
|
||||
spacing: 32px;
|
||||
for album in root.section.albums: AlbumCard {
|
||||
album: album;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,10 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Layout } from "../foundation/layout.slint";
|
||||
import { Spacing } from "../foundation/spacing.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Sidebar } from "Sidebar.slint";
|
||||
import { HeaderBar } from "HeaderBar.slint";
|
||||
import { PlayerBar } from "PlayerBar.slint";
|
||||
import { HomeView } from "../discover/HomeView.slint";
|
||||
|
||||
export component AppShell inherits Rectangle {
|
||||
background: Theme.surface-main;
|
||||
@@ -29,34 +28,12 @@ export component AppShell inherits Rectangle {
|
||||
spacing: 0px;
|
||||
|
||||
Sidebar { }
|
||||
|
||||
// Content region — placeholder until the Discover/Home view.
|
||||
Rectangle {
|
||||
width: Layout.hairline;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
HomeView {
|
||||
horizontal-stretch: 1;
|
||||
background: Theme.surface-main;
|
||||
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
VerticalLayout {
|
||||
spacing: Spacing.sm;
|
||||
Text {
|
||||
text: "Discover — Home";
|
||||
color: Theme.text-primary;
|
||||
font-size: 22px;
|
||||
font-weight: Typography.semibold;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
Text {
|
||||
text: "Content view lands in the next increment";
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Shared UI state — structs and globals the Rust controllers populate.
|
||||
//
|
||||
// The Rust side builds these models on a worker thread, then sets them
|
||||
// on the global from the Slint event loop (see src/home.rs, src/main.rs).
|
||||
|
||||
export struct AlbumCardItem {
|
||||
id: string,
|
||||
title: string,
|
||||
artist: string,
|
||||
quality: string,
|
||||
ribbon: string,
|
||||
ribbon-kind: string, // "press" | "qobuzissime" | "albumOfTheWeek" | ""
|
||||
artwork-url: string,
|
||||
}
|
||||
|
||||
export struct DiscoverSection {
|
||||
title: string,
|
||||
albums: [AlbumCardItem],
|
||||
}
|
||||
|
||||
// Discover / Home screen state.
|
||||
export global HomeState {
|
||||
in property <string> greeting: "";
|
||||
in property <[DiscoverSection]> sections: [];
|
||||
in property <bool> loading: false;
|
||||
}
|
||||
Reference in new issue
Block a user