// ArtistsByLocationView — "artists from the same place" scene // discovery, opened from the Origin section's location link. Mirrors // Tauri's ArtistsByLocationView.svelte core: a scene header // (location + genre summary) over a wrapping artist grid, with // load-more pagination. The progress bar / live event stream from // the Tauri view is replaced by a single loading flag. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { LocationViewState, LocationViewActions, LocationArtistItem, NavState } from "../state.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint"; import { LoadingSpinner } from "../primitives/LoadingSpinner.slint"; import { ListScrollbar } from "../primitives/ListScrollbar.slint"; import { NavButtons } from "../shell/NavButtons.slint"; component SceneArtistCard inherits Rectangle { in property item; callback clicked(string /* qobuz id */); width: 160px; height: 210px; VerticalLayout { spacing: 10px; alignment: start; // Round portrait. Rectangle { width: 140px; height: 140px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; border-radius: 70px; background: Theme.surface-elevated; drop-shadow-blur: 16px; drop-shadow-color: #00000050; drop-shadow-offset-y: 4px; clip: true; Image { width: 100%; height: 100%; source: root.item.image; image-fit: cover; } if root.item.image-url == "": QbzIcon { source: @image-url("../assets/icons/user.svg"); width: 48px; height: 48px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; tint: Theme.text-muted; } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(root.item.qobuz-id); } } } Text { text: root.item.name; color: Theme.text-primary; font-size: Typography.body; font-weight: Typography.medium; horizontal-alignment: center; overflow: elide; width: 160px; } if root.item.genres-line != "": Text { text: root.item.genres-line; color: Theme.text-muted; font-size: Typography.legal; horizontal-alignment: center; overflow: elide; width: 160px; } } } export component ArtistsByLocationView inherits Rectangle { callback open-artist(string); property card-width: 160px; property card-height: 210px; property card-gap: 20px; background: Theme.surface-main; flick := Flickable { viewport-width: self.width; viewport-height: page.preferred-height; // Scroll-position restore on back/forward (see nav::scope_for). property sr-armed: NavState.restore-scope; function sr-restore() { if (NavState.restore-scope == "location" && self.viewport-height > self.height) { self.viewport-y = NavState.scroll-restore; NavState.restore-scope = ""; } } init => { self.sr-restore(); } changed sr-armed => { self.sr-restore(); } changed viewport-height => { self.sr-restore(); } changed viewport-y => { NavState.report-scroll(self.viewport-y); } page := VerticalLayout { width: parent.width; padding-left: 32px; padding-right: 32px; padding-top: 11px; padding-bottom: 100px; spacing: 0px; alignment: start; HorizontalLayout { alignment: start; NavButtons { } } Rectangle { height: 22px; } // --- Scene header ------------------------------------------ VerticalLayout { spacing: 4px; HorizontalLayout { spacing: 10px; alignment: start; VerticalLayout { alignment: center; QbzIcon { source: @image-url("../assets/icons/map-pin.svg"); width: 22px; height: 22px; tint: Theme.accent; } } Text { text: LocationViewState.scene-label != "" ? LocationViewState.scene-label : @tr("Scene"); color: Theme.text-primary; font-size: Typography.title; font-weight: Typography.bold; vertical-alignment: center; } } if LocationViewState.genre-summary != "": Text { text: LocationViewState.genre-summary; color: Theme.text-muted; font-size: Typography.body; } } Rectangle { height: 28px; } if LocationViewState.loading && LocationViewState.artists.length == 0: HorizontalLayout { alignment: center; LoadingSpinner { size: 36px; } } if !LocationViewState.loading && LocationViewState.artists.length == 0: Text { text: @tr("No artists found for this scene."); color: Theme.text-muted; font-size: Typography.body; } // --- Artist grid ------------------------------------------- if LocationViewState.artists.length > 0: grid := Rectangle { property columns: Math.max( 1, Math.floor((self.width + root.card-gap) / (root.card-width + root.card-gap))); property rows: Math.ceil(LocationViewState.artists.length / self.columns); height: self.rows > 0 ? self.rows * root.card-height + Math.max(0, self.rows - 1) * root.card-gap : 0px; for item[i] in LocationViewState.artists: SceneArtistCard { x: Math.mod(i, grid.columns) * (root.card-width + root.card-gap); y: Math.floor(i / grid.columns) * (root.card-height + root.card-gap); item: item; clicked(id) => { if (id != "") { root.open-artist(id); } } } } if LocationViewState.artists.length > 0 && LocationViewState.artists.length < LocationViewState.total: HorizontalLayout { alignment: center; Rectangle { width: load-more-text.preferred-width + 24px; height: 32px; load-more-text := Text { text: LocationViewState.load-more-loading ? @tr("Loading…") : @tr("Load more"); color: load-more-ta.has-hover ? Theme.text-primary : Theme.text-secondary; font-size: 13px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; } load-more-ta := TouchArea { mouse-cursor: pointer; enabled: !LocationViewState.load-more-loading; clicked => { LocationViewActions.load-more(); } } } } } } // Shared scrollbar in the right gutter (homologated cross-view). ListScrollbar { x: parent.width - self.width - 4px; width: 14px; height: 100%; viewport-height: flick.viewport-height; visible-height: flick.height; viewport-y <=> flick.viewport-y; } }