feat(artist): sort for the in-library albums and a split play button on every album section
The "In library" Albums section gains the release buckets' sort select (same five keys, persisted by artist_prefs under the pseudo release type "library"; rows sorted in QML off the document's seed). Every album section — each catalog bucket and the library grid — gains a split button right of its sort (controls/QbzSplitButton.qml, sized like the sm select): the body plays the whole section in its sort order as one queue replacing the current one (`QbzPlayer.playAlbums` -> `playback_qt::play_albums`, each album resolved through the single-album path, failures skipped), the chevron offers Play random (one album of the section) and Play selected, which arms a per-section select mode: the cards show their check bottom-right inside the art (`AlbumCard.selectMarkBottom`), the button reads "Play selected (n)" and plays the checked albums in sort order, and the menu swaps that row for Cancel selection. One section is armed at a time; changing artist or tab disarms. No queue options on purpose. cc
This commit is contained in:
7 files changed
+391
-8
No files matched your search
@@ -578,6 +578,7 @@ fn main() {
|
||||
"qml/controls/ScopePanel.qml",
|
||||
"qml/controls/QbzSegToggle.qml",
|
||||
"qml/controls/QbzSelect.qml",
|
||||
"qml/controls/QbzSplitButton.qml",
|
||||
"qml/controls/QbzSlider.qml",
|
||||
"qml/controls/QbzTabBar.qml",
|
||||
"qml/controls/QbzTextArea.qml",
|
||||
|
||||
@@ -198,6 +198,8 @@ Rectangle {
|
||||
// there is no host-painted tick left in the tree, and a new one is a
|
||||
// regression, not a shortcut.
|
||||
property bool selectMode: false
|
||||
/// Park the select indicator bottom-right of the art instead of top-right.
|
||||
property bool selectMarkBottom: false
|
||||
property bool selected: false
|
||||
signal selectToggled()
|
||||
|
||||
@@ -551,7 +553,10 @@ Rectangle {
|
||||
Rectangle {
|
||||
visible: root.selectMode
|
||||
x: parent.width - width - 8
|
||||
y: 8
|
||||
// Top-right by default (the Library bulk mode); the artist
|
||||
// page's album-section "Play selected" parks it bottom-right
|
||||
// inside the art (`selectMarkBottom`).
|
||||
y: root.selectMarkBottom ? parent.height - height - 8 : 8
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// QbzSplitButton — a toolbar button that LOOKS like a select: the body runs
|
||||
// the primary action, the chevron cell opens a menu of alternatives.
|
||||
//
|
||||
// +--------------+-+
|
||||
// | Play all |v|
|
||||
// +--------------+-+
|
||||
//
|
||||
// Sized and painted like `QbzSelect { sm: true }` so it sits flush beside one
|
||||
// in a section header (30 tall, r6, elevated fill, no outline, 12px label,
|
||||
// 14px chevron): the album-section "Play all / Play random / Play selected"
|
||||
// control on the artist page is the first host. The menu is the shared
|
||||
// controls/QbzContextMenu opened below-right of the whole control, the
|
||||
// QbzSelect / sort-menu convention.
|
||||
//
|
||||
// `menuItems` is `[{ label, action, icon? }]`; a pick closes the menu and
|
||||
// emits `picked(action)`. The body emits `clicked()` and never opens the
|
||||
// menu — the two halves are separate hit zones with their own hover fill,
|
||||
// and the 1px hairline between them is the split.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../theme"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property var menuItems: []
|
||||
property int menuWidth: 180
|
||||
property bool btnEnabled: true
|
||||
signal clicked()
|
||||
signal picked(string action)
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
readonly property int chevronWidth: 24
|
||||
|
||||
height: 30
|
||||
radius: 6
|
||||
implicitWidth: bodyLabel.implicitWidth + 24 + root.chevronWidth
|
||||
width: implicitWidth
|
||||
// Same resting fill as QbzSelect sm (translucent under the dynamic
|
||||
// background); hover is painted per zone below.
|
||||
color: theme.ambientOn ? theme.surfaceElevatedA50 : theme.surfaceElevated
|
||||
opacity: root.btnEnabled ? 1.0 : 0.4
|
||||
|
||||
// --- Body: the primary action ------------------------------------------
|
||||
Rectangle {
|
||||
id: body
|
||||
x: 0
|
||||
y: 0
|
||||
width: parent.width - root.chevronWidth
|
||||
height: parent.height
|
||||
radius: 6
|
||||
color: bodyArea.containsMouse && root.btnEnabled ? theme.surfaceHover : "transparent"
|
||||
// Square the inner corners so the hover fill meets the hairline.
|
||||
Rectangle {
|
||||
x: parent.width - 6
|
||||
y: 0
|
||||
width: 6
|
||||
height: parent.height
|
||||
color: parent.color
|
||||
}
|
||||
Text {
|
||||
id: bodyLabel
|
||||
anchors.centerIn: parent
|
||||
text: root.label
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 12
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
MouseArea {
|
||||
id: bodyArea
|
||||
anchors.fill: parent
|
||||
enabled: root.btnEnabled
|
||||
hoverEnabled: true
|
||||
cursorShape: root.btnEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
// --- The split ----------------------------------------------------------
|
||||
Rectangle {
|
||||
x: parent.width - root.chevronWidth
|
||||
y: 7
|
||||
width: 1
|
||||
height: parent.height - 14
|
||||
color: theme.borderSubtle
|
||||
}
|
||||
|
||||
// --- Chevron: the menu --------------------------------------------------
|
||||
Rectangle {
|
||||
id: chevron
|
||||
x: parent.width - root.chevronWidth
|
||||
y: 0
|
||||
width: root.chevronWidth
|
||||
height: parent.height
|
||||
radius: 6
|
||||
color: chevronArea.containsMouse && root.btnEnabled ? theme.surfaceHover : "transparent"
|
||||
Rectangle {
|
||||
x: 0
|
||||
y: 0
|
||||
width: 6
|
||||
height: parent.height
|
||||
color: parent.color
|
||||
}
|
||||
QbzIcon {
|
||||
name: "chevron-down"
|
||||
width: 14
|
||||
height: 14
|
||||
anchors.centerIn: parent
|
||||
tintName: chevronArea.containsMouse ? "textPrimary" : "secondary"
|
||||
}
|
||||
MouseArea {
|
||||
id: chevronArea
|
||||
anchors.fill: parent
|
||||
enabled: root.btnEnabled
|
||||
hoverEnabled: true
|
||||
cursorShape: root.btnEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: menu.openBelowRight(root)
|
||||
}
|
||||
}
|
||||
|
||||
QbzContextMenu {
|
||||
id: menu
|
||||
menuWidth: root.menuWidth
|
||||
Repeater {
|
||||
model: root.menuItems
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
width: parent ? parent.width : 0
|
||||
height: 33
|
||||
radius: 5
|
||||
color: itemArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
spacing: 8
|
||||
QbzIcon {
|
||||
visible: (modelData.icon || "") !== ""
|
||||
name: modelData.icon || ""
|
||||
width: visible ? 15 : 0
|
||||
height: 15
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: "secondary"
|
||||
}
|
||||
Text {
|
||||
height: parent.height
|
||||
width: parent.width - 23
|
||||
text: modelData.label
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 13
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: itemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
menu.close()
|
||||
root.picked(modelData.action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,6 +327,92 @@ Rectangle {
|
||||
for (var i = 0; i < root.libTracks.length; i++) ids.push(String(root.libTracks[i].id))
|
||||
return ids
|
||||
}
|
||||
/// The library Albums section's sort — the release buckets' five keys,
|
||||
/// seeded from the document (`artist.librarySort`, artist_prefs "library")
|
||||
/// and persisted through the same invokable the buckets use. Sorted here,
|
||||
/// not in Rust: the rows already live in QML (`sortReleaseCards` reads
|
||||
/// `title` / `year`, which a FeedItem carries).
|
||||
property string libSort: "default"
|
||||
readonly property var libAlbumsSorted: root.sortReleaseCards(root.libAlbums, root.libSort)
|
||||
function setLibSort(key) {
|
||||
root.libSort = key
|
||||
QbzArtist.setSectionSort("library", key)
|
||||
}
|
||||
|
||||
// ---- Album-section play (the split button) -------------------------------
|
||||
// Every album section — each catalog bucket and the library Albums grid —
|
||||
// carries a QbzSplitButton right of its sort: the body plays the whole
|
||||
// section in its SORT order as one queue (`QbzPlayer.playAlbums`), the
|
||||
// chevron offers Play random (ONE album of the section, replacing the
|
||||
// queue) and Play selected, which arms a per-section select mode: the
|
||||
// cards grow a check bottom-right inside the art, the button reads
|
||||
// "Play selected (n)" and plays the checked albums in sort order, and the
|
||||
// menu swaps that row for Cancel selection. One section is armed at a
|
||||
// time (`albumSelectKey` is the bucket's releaseType or "library").
|
||||
// No queue options (next / later / queue) on purpose.
|
||||
property string albumSelectKey: ""
|
||||
property var albumSelected: ({})
|
||||
readonly property int albumSelectedCount: Object.keys(root.albumSelected).length
|
||||
function albumSelectToggle(id) {
|
||||
var m = Object.assign({}, root.albumSelected)
|
||||
if (m[id]) delete m[id]
|
||||
else m[id] = true
|
||||
root.albumSelected = m
|
||||
}
|
||||
function albumSelectClear() {
|
||||
root.albumSelectKey = ""
|
||||
root.albumSelected = ({})
|
||||
}
|
||||
function albumSectionLabel(key) {
|
||||
return root.albumSelectKey === key
|
||||
? QbzSession.tr("Play selected", QbzSession.trRev) + " (" + root.albumSelectedCount + ")"
|
||||
: QbzSession.tr("Play all", QbzSession.trRev)
|
||||
}
|
||||
function albumSectionMenu(key) {
|
||||
var m = [
|
||||
{ "label": QbzSession.tr("Play all", QbzSession.trRev), "icon": "play-fill", "action": "play-all" },
|
||||
{ "label": QbzSession.tr("Play random", QbzSession.trRev), "icon": "shuffle", "action": "play-random" }
|
||||
]
|
||||
if (root.albumSelectKey === key)
|
||||
m.push({ "label": QbzSession.tr("Cancel selection", QbzSession.trRev), "icon": "x", "action": "cancel-selection" })
|
||||
else
|
||||
m.push({ "label": QbzSession.tr("Play selected", QbzSession.trRev), "icon": "square-check-big", "action": "play-selected" })
|
||||
return m
|
||||
}
|
||||
function albumSectionIds(cards) {
|
||||
var ids = []
|
||||
for (var i = 0; i < cards.length; i++) ids.push(String(cards[i].id))
|
||||
return ids
|
||||
}
|
||||
/// `cards` is the section's list in its CURRENT sort order.
|
||||
function albumSectionAction(key, cards, action) {
|
||||
var ids = root.albumSectionIds(cards)
|
||||
if (action === "play-all") {
|
||||
root.albumSelectClear()
|
||||
if (ids.length > 0) QbzPlayer.playAlbums(JSON.stringify(ids))
|
||||
} else if (action === "play-random") {
|
||||
root.albumSelectClear()
|
||||
if (ids.length > 0) QbzPlayer.playAlbum(ids[Math.floor(Math.random() * ids.length)])
|
||||
} else if (action === "play-selected") {
|
||||
root.albumSelectKey = key
|
||||
root.albumSelected = ({})
|
||||
} else if (action === "cancel-selection") {
|
||||
root.albumSelectClear()
|
||||
}
|
||||
}
|
||||
/// The button body: Play all, or — armed — the checked albums in sort
|
||||
/// order, then disarm.
|
||||
function albumSectionPrimary(key, cards) {
|
||||
if (root.albumSelectKey !== key) {
|
||||
root.albumSectionAction(key, cards, "play-all")
|
||||
return
|
||||
}
|
||||
var ids = []
|
||||
for (var i = 0; i < cards.length; i++)
|
||||
if (root.albumSelected[cards[i].id] === true) ids.push(String(cards[i].id))
|
||||
root.albumSelectClear()
|
||||
if (ids.length > 0) QbzPlayer.playAlbums(JSON.stringify(ids))
|
||||
}
|
||||
|
||||
// ---- Header atmosphere (ArtistPageView.slint:120-147) ----------------
|
||||
// Same three-line rule as AlbumView (the .slint says "same rule as
|
||||
@@ -882,6 +968,7 @@ Rectangle {
|
||||
// switch. The strip re-anchors on the first section the new mode
|
||||
// draws (catalog keeps its Popular Tracks default).
|
||||
setMultiSelect(false)
|
||||
albumSelectClear()
|
||||
// The first SECTION, not "About" (back-to-top) when a section exists.
|
||||
var first = ""
|
||||
for (var i = 0; i < root.jumpTabs.length; i++) {
|
||||
@@ -979,6 +1066,8 @@ Rectangle {
|
||||
libTracksExpanded = false
|
||||
libShowPurchases = false
|
||||
libShowFavorites = false
|
||||
libSort = artist.librarySort || "default"
|
||||
albumSelectClear()
|
||||
// Slint opens a fresh artist on Network, or on Magazine when
|
||||
// MusicBrainz is off (an empty Network tab is worse than none).
|
||||
netTab = (artist.network && artist.network.mbAvailable) ? "network" : "magazine"
|
||||
@@ -1950,7 +2039,7 @@ Rectangle {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
Text {
|
||||
width: parent.width - seeAll.width - sortBtn.width - 24
|
||||
width: parent.width - seeAll.width - sortBtn.width - playBtn.width - 36
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: section.title
|
||||
color: theme.textPrimary
|
||||
@@ -2062,6 +2151,21 @@ Rectangle {
|
||||
: i === 3 ? "title-asc" : i === 4 ? "title-desc" : "default")
|
||||
}
|
||||
}
|
||||
// Play all / Play random / Play selected over THIS bucket, in
|
||||
// its sort order (`section.cards` is already sorted — Rust sorts
|
||||
// the bucket and the overlay merge re-sorts).
|
||||
QbzSplitButton {
|
||||
id: playBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
label: root.albumSectionLabel(relSection.section.releaseType || "")
|
||||
menuItems: root.albumSectionMenu(relSection.section.releaseType || "")
|
||||
onClicked: root.albumSectionPrimary(relSection.section.releaseType || "",
|
||||
relSection.section.cards || [])
|
||||
onPicked: function (a) {
|
||||
root.albumSectionAction(relSection.section.releaseType || "",
|
||||
relSection.section.cards || [], a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
@@ -2100,6 +2204,11 @@ Rectangle {
|
||||
// artist_qt::map_release stamps this the same way it
|
||||
// stamps the pin; false inverted the first click.
|
||||
isFavorite: modelData.isFavorite === true
|
||||
// "Play selected" over this bucket (the split button).
|
||||
selectMode: root.albumSelectKey === (relSection.section.releaseType || "")
|
||||
selected: root.albumSelected[modelData.id] === true
|
||||
selectMarkBottom: true
|
||||
onSelectToggled: root.albumSelectToggle(modelData.id)
|
||||
|
||||
// ---- smooth append (owner, 2026-08-02) ---------------
|
||||
// "que la aparicion de lo que se cargue, sea smooth" —
|
||||
@@ -3053,13 +3162,51 @@ Rectangle {
|
||||
|
||||
// --- Albums ---------------------------------------------
|
||||
Item { visible: root.libAlbums.length > 0 && root.libTracks.length > 0; width: 1; height: 24 }
|
||||
Text {
|
||||
// Header: title, the release buckets' sort select, and the
|
||||
// split play button — the catalog section header's cluster
|
||||
// minus "See discography" (there is nothing to page here).
|
||||
Row {
|
||||
property string anchorId: "albums"
|
||||
visible: root.libAlbums.length > 0
|
||||
text: QbzSession.tr("Albums", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontHeading
|
||||
font.weight: theme.weightSemibold
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
Text {
|
||||
width: parent.width - libSortBtn.width - libPlayBtn.width - 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: QbzSession.tr("Albums", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontHeading
|
||||
font.weight: theme.weightSemibold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QbzSelect {
|
||||
id: libSortBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sm: true
|
||||
menuWidth: 150
|
||||
options: [QbzSession.tr("Default", QbzSession.trRev),
|
||||
QbzSession.tr("Newest", QbzSession.trRev),
|
||||
QbzSession.tr("Oldest", QbzSession.trRev),
|
||||
QbzSession.tr("A–Z", QbzSession.trRev),
|
||||
QbzSession.tr("Z–A", QbzSession.trRev)]
|
||||
currentIndex: {
|
||||
var s = root.libSort || "default"
|
||||
return s === "newest" ? 1 : s === "oldest" ? 2
|
||||
: s === "title-asc" ? 3 : s === "title-desc" ? 4 : 0
|
||||
}
|
||||
onSelected: function (i) {
|
||||
root.setLibSort(i === 1 ? "newest" : i === 2 ? "oldest"
|
||||
: i === 3 ? "title-asc" : i === 4 ? "title-desc" : "default")
|
||||
}
|
||||
}
|
||||
QbzSplitButton {
|
||||
id: libPlayBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
label: root.albumSectionLabel("library")
|
||||
menuItems: root.albumSectionMenu("library")
|
||||
onClicked: root.albumSectionPrimary("library", root.libAlbumsSorted)
|
||||
onPicked: function (a) { root.albumSectionAction("library", root.libAlbumsSorted, a) }
|
||||
}
|
||||
}
|
||||
Item { visible: root.libAlbums.length > 0; width: 1; height: 10 }
|
||||
Grid {
|
||||
@@ -3069,8 +3216,14 @@ Rectangle {
|
||||
columnSpacing: 24
|
||||
rowSpacing: 24
|
||||
Repeater {
|
||||
model: root.libAlbums
|
||||
model: root.libAlbumsSorted
|
||||
delegate: AlbumCard {
|
||||
// "Play selected" over this section (the split
|
||||
// button), check bottom-right inside the art.
|
||||
selectMode: root.albumSelectKey === "library"
|
||||
selected: root.albumSelected[modelData.id] === true
|
||||
selectMarkBottom: true
|
||||
onSelectToggled: root.albumSelectToggle(modelData.id)
|
||||
albumId: modelData.id
|
||||
title: modelData.title
|
||||
artist: modelData.artist
|
||||
|
||||
@@ -368,6 +368,13 @@ pub struct ArtistViewData {
|
||||
/// repaint the tab through `publish_patch`, the page's one transport.
|
||||
#[serde(rename = "libraryItems")]
|
||||
pub library_items: Vec<FeedItem>,
|
||||
/// Sort of the "In library" Albums section — the same five keys the
|
||||
/// release buckets use, persisted by `artist_prefs` under the pseudo
|
||||
/// release type `library` (`QbzArtist.setSectionSort("library", …)`).
|
||||
/// The rows are sorted in QML (they live there already); only the seed
|
||||
/// travels here.
|
||||
#[serde(rename = "librarySort")]
|
||||
pub library_sort: String,
|
||||
#[serde(rename = "topTracks")]
|
||||
pub top_tracks: Vec<TrackRow>,
|
||||
#[serde(rename = "appearsOn")]
|
||||
@@ -880,6 +887,7 @@ fn map_artist(page: PageArtistResponse) -> ArtistViewData {
|
||||
is_blacklisted: crate::artist_blacklist::is_blacklisted(page.id),
|
||||
library_count,
|
||||
library_items,
|
||||
library_sort: crate::artist_prefs::get_sort("library"),
|
||||
top_tracks,
|
||||
appears_on,
|
||||
last_release,
|
||||
|
||||
@@ -2237,6 +2237,43 @@ pub async fn play_album(
|
||||
play_album_from(runtime, album_id, 0).await
|
||||
}
|
||||
|
||||
/// An album SECTION's "Play all" / "Play selected" (artist page split
|
||||
/// button): every album's tracks, in the order given, as ONE queue that
|
||||
/// replaces the current one — track 1 of the first album starts. Each album
|
||||
/// resolves through `fetch_album_queue` like a single-album play, so a
|
||||
/// purchase preference or a withdrawn row is treated identically; an album
|
||||
/// that fails to resolve is logged and skipped rather than sinking the whole
|
||||
/// section. Local / Plex group keys are not a catalog section and are
|
||||
/// skipped.
|
||||
pub async fn play_albums(
|
||||
runtime: &Arc<AppRuntime<LoggingAdapter>>,
|
||||
album_ids: &[String],
|
||||
) -> Result<(), String> {
|
||||
let mut queue: Vec<QueueTrack> = Vec::new();
|
||||
for album_id in album_ids {
|
||||
if is_local_album(album_id) {
|
||||
log::info!("[qbz-qt] play_albums: skipping local group key {album_id}");
|
||||
continue;
|
||||
}
|
||||
match fetch_album_queue(runtime, album_id).await {
|
||||
Ok(mut tracks) => queue.append(&mut tracks),
|
||||
Err(e) => log::warn!("[qbz-qt] play_albums: {album_id} skipped: {e}"),
|
||||
}
|
||||
}
|
||||
if queue.is_empty() {
|
||||
return Err(format!(
|
||||
"none of the {} album(s) resolved to playable tracks",
|
||||
album_ids.len()
|
||||
));
|
||||
}
|
||||
log::info!(
|
||||
"[qbz-qt] play_albums: queueing {} track(s) from {} album(s)",
|
||||
queue.len(),
|
||||
album_ids.len()
|
||||
);
|
||||
play_track_list(runtime, queue, 0, false).await
|
||||
}
|
||||
|
||||
/// Play an album starting at track `start_index` (AlbumView row play —
|
||||
/// Slint's play_album_from semantics: the queue keeps the whole album,
|
||||
/// the cursor starts at the clicked track).
|
||||
|
||||
@@ -200,6 +200,11 @@ pub mod qbz_player {
|
||||
/// AlbumView header Shuffle.
|
||||
#[qinvokable]
|
||||
fn play_album_shuffled(self: Pin<&mut QbzPlayer>, album_id: QString);
|
||||
/// Artist page album-section split button — a JSON array of album
|
||||
/// ids in the section's sort order, queued as one list.
|
||||
/// `#[auto_cxx_name]` makes the QML name `QbzPlayer.playAlbums`.
|
||||
#[qinvokable]
|
||||
fn play_albums(self: Pin<&mut QbzPlayer>, ids_json: QString);
|
||||
/// AlbumView row play: the album starting AT this track.
|
||||
#[qinvokable]
|
||||
fn play_album_from(self: Pin<&mut QbzPlayer>, album_id: QString, track_id: QString);
|
||||
@@ -477,6 +482,10 @@ impl qbz_player::QbzPlayer {
|
||||
crate::play_album(album_id.to_string());
|
||||
}
|
||||
|
||||
pub fn play_albums(self: Pin<&mut Self>, ids_json: QString) {
|
||||
crate::play_albums(ids_json.to_string());
|
||||
}
|
||||
|
||||
pub fn play_album_shuffled(self: Pin<&mut Self>, album_id: QString) {
|
||||
crate::play_album_shuffled(album_id.to_string());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user