Compare commits

..
15 changed files with 1185 additions and 1137 deletions

No files matched your search

-1
View File
@@ -43,7 +43,6 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n CoreAddon
find_package(pybind11 REQUIRED)
find_package(Ytdlp REQUIRED RUNTIME)
find_package(YTMusicAPI 0.24.0 REQUIRED RUNTIME)
find_package(Desert REQUIRED)
ki18n_install(po)
-2
View File
@@ -33,7 +33,6 @@ add_executable(audiotube
errorhandler.cpp
playerutils.cpp
abstractytmusicmodel.cpp
autolistmodel.h
multiiterableview.h
library.cpp
resources.qrc
@@ -52,7 +51,6 @@ target_link_libraries(audiotube
KF5::CoreAddons
KF5::Crash
KF5::WindowSystem
Desert::desert
ytm
asyncdatabase
)
-91
View File
@@ -1,91 +0,0 @@
// SPDX-FileCopyrightText: 2023 Jonah Brüchert <[email protected]>
//
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QDebug>
#include <Desert/Reflection>
#include "updateablemodel.h"
template <typename T>
using AutoListModelBase = UpdateableModel<QAbstractListModel, T>;
template <typename T>
concept IsComparable = requires(T v, T o)
{
{ v == o } -> std::same_as<bool>;
{ v < o } -> std::same_as<bool>;
};
template <typename T>
requires desert::IsDesertObject<T> && IsComparable<T>
class AutoListModel : public AutoListModelBase<T>
{
public:
AutoListModel(QObject *parent = nullptr) : AutoListModelBase<T>(parent) {
}
QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles;
int i = Qt::UserRole + 1;
T mock_obj;
desert::iterate_struct(mock_obj, [&](auto attribute) {
roles.insert(i, attribute.attributeName().toUtf8());
i++;
});
return roles;
}
int rowCount(const QModelIndex &parent) const override {
return parent.isValid() ? 0 : m_data.size();
}
QVariant data(const QModelIndex &index, int role) const override {
int structIndex = role - Qt::UserRole - 1;
if (structIndex >= pfr::tuple_size_v<T>) {
qDebug() << "Tried to fetch a role that is not defined by this model. Role id:" << role;
return {};
}
return structAttributeAt<0, T>(m_data.at(index.row()), structIndex);
}
void updateData(std::vector<T> &&updatedData) {
// allow comparing items. Could also have used an operator< on Data
auto lessThan = [](const T &lhs, const T &rhs) { return lhs < rhs; };
// check if the item has changed data, but is otherwise the same object. Needs to return the actual changes made.
auto itemHasChanged = [](const T &lhs, const T &rhs) {
typename AutoListModelBase<T>::DataChanges changes;
if (lhs != rhs)
changes.changedColumns.append(0);
changes.changedRoles = QVector<int>{}; // TODO more specific updates
return changes;
};
// call the updateData method to trigger merging in the changes to the model
auto changes = AutoListModelBase<T>::updateData(updatedData.cbegin(), updatedData.cend(), m_data, lessThan, itemHasChanged);
// the method above returns some stats, so lets print them.
qDebug() << "changes in model: inserts:" << changes.inserts << " deletes:" << changes.removals
<< " updates:" << changes.updates;
}
private:
std::vector<T> m_data;
};
template <size_t i, typename T>
requires desert::IsDesertObject<T>
QVariant structAttributeAt(const T &object, size_t index) {
if constexpr (i >= pfr::tuple_size_v<T>) {
Q_UNREACHABLE();
} else if (i == index) {
return QVariant::fromValue(pfr::get<i>(object).value);
} else {
return structAttributeAt<i + 1>(object, index);
}
}
+10 -8
View File
@@ -59,7 +59,8 @@ Kirigami.ScrollablePage {
delegate: ColumnLayout {
id: delegateItem
required property string title
required property string artists
required property var artists
required property string artistsDisplayString
required property string videoId
required property int index
@@ -182,7 +183,7 @@ Kirigami.ScrollablePage {
Layout.maximumWidth: 200
leftPadding: 5
color: Kirigami.Theme.disabledTextColor
text: delegateItem.artists
text: delegateItem.artistsDisplayString
elide: Text.ElideRight
}
}
@@ -191,8 +192,8 @@ Kirigami.ScrollablePage {
icon.name: "view-more-horizontal-symbolic"
onPressed: menu.openForSong(delegateItem.videoId,
delegateItem.title,
delegateItem.artists, // TODO
delegateItem.artists)
delegateItem.artists,
delegateItem.artistsDisplayString)
}
}
@@ -243,7 +244,8 @@ Kirigami.ScrollablePage {
delegate: ColumnLayout {
id: mpdelegateItem
required property string title
required property string artists
required property var artists
required property string artistsDisplayString
required property string videoId
Layout.fillWidth: false
@@ -363,7 +365,7 @@ Kirigami.ScrollablePage {
Layout.maximumWidth: 200
leftPadding: 5
color: Kirigami.Theme.disabledTextColor
text: mpdelegateItem.artists
text: mpdelegateItem.artistsDisplayString
elide: Text.ElideRight
}
}
@@ -372,8 +374,8 @@ Kirigami.ScrollablePage {
icon.name: "view-more-horizontal-symbolic"
onPressed: menu.openForSong(mpdelegateItem.videoId,
mpdelegateItem.title,
mpdelegateItem.artists, // TODO
mpdelegateItem.artists)
mpdelegateItem.artists,
mpdelegateItem.artistsDisplayString)
}
}
File diff suppressed because it is too large. Load diff
+156 -76
View File
@@ -22,14 +22,12 @@ Item {
required property var info // VideoInfoExtractor object
required property var audio // Audio object
readonly property bool isWidescreen: width >= Kirigami.Units.gridUnit * 40
readonly property bool isWidescreen: width >= Kirigami.Units.gridUnit * 50
signal requestOpen()
Rectangle {
id: miniProgressBar
z: 1
visible: !root.isWidescreen
anchors.top: parent.top
anchors.left: parent.left
height: root.progressBarHeight
@@ -38,14 +36,14 @@ Item {
}
RowLayout {
width: !isWidescreen? root.width- controlButtonBox.width: root.width
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 20
Layout.preferredWidth: Kirigami.Units.gridUnit * 20
color: Qt.rgba(0, 0, 0, trackClick.containsMouse ? 0.1 : trackClick.pressed ? 0.3 : 0)
RowLayout {
@@ -78,10 +76,12 @@ Item {
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
}
}
// track information
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
@@ -92,6 +92,7 @@ Item {
wrapMode: Text.Wrap
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 15
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
maximumLineCount: 1
@@ -112,14 +113,16 @@ Item {
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
maximumLineCount: 1
Layout.maximumWidth: Kirigami.Units.gridUnit * 15
// Hardcoded because the footerbar blur always makes a dark-ish
// background, so we don't want to use a color scheme color that
// might also be dark
color: "white"
}
}
Item{Layout.fillWidth: true}
}
MouseArea {
id: trackClick
anchors.fill: parent
@@ -127,85 +130,162 @@ Item {
onClicked: root.requestOpen()
}
}
RowLayout {
visible: root.isWidescreen
Layout.preferredWidth: 0
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.smallSpacing
Label {
Layout.alignment: Qt.AlignVCenter
color: "white"
visible: info.title
text: PlayerUtils.formatTimestamp(audio.position)
}
}
Slider {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
from: 0
to: audio.duration
value: audio.position
enabled: audio.seekable
onMoved: {
console.log("Value:", value);
audio.seek(Math.floor(value));
}
RowLayout {
id: controlButtonBox
anchors.centerIn: isWidescreen? root:null
anchors.right: !isWidescreen? root.right:null
Layout.fillHeight: true
spacing: 2
Behavior on value {
NumberAnimation {
duration: 1000
}
}
}
Label {
Layout.alignment: Qt.AlignVCenter
color: "white"
visible: info.title
text: PlayerUtils.formatTimestamp(audio.duration)
}
Label {
color: "white"
visible: info.title && root.isWidescreen
text: PlayerUtils.formatTimestamp(audio.position)
Layout.rightMargin: 20
}
ToolButton {
id: playPauseButton
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.maximumWidth: height
Layout.preferredWidth: height
enabled: info.audioUrl != ""
onClicked: audio.playbackState === Audio.PlayingState ? audio.pause() : audio.play()
icon.name: audio.playbackState === Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
icon.width: Kirigami.Units.gridUnit
icon.height: Kirigami.Units.gridUnit
icon.color: "white"
Button {
id: skipBackwardButton
height: 40
width: 40
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
enabled: UserPlaylistModel.canSkipBack
onClicked: UserPlaylistModel.previous()
contentItem: Item{
Kirigami.Icon {
anchors.centerIn:parent
source:"media-skip-backward"
color: "white"
width: Kirigami.Units.gridUnit
height: Kirigami.Units.gridUnit
}
}
background: Kirigami.ShadowedRectangle{
corners.topLeftRadius: 7
corners.bottomLeftRadius: 7
color: if (isWidescreen){
if (parent.down){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.3)
}else if(parent.hovered){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.7)
}else{
Qt.rgba(1, 1, 1, 0.2)
}
} else {
if (parent.down){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.3)
}else if(parent.hovered){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.7)
}else{
"transparent"
}
}
}
}
ToolButton {
Button {
id: playPauseButton
height: 40
width: 60
onClicked: audio.playbackState === Audio.PlayingState ? audio.pause() : audio.play()
contentItem: Item{
Kirigami.Icon {
anchors.centerIn:parent
source: audio.playbackState === Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
color: "white"
width: Kirigami.Units.gridUnit
height: Kirigami.Units.gridUnit
}
}
background: Kirigami.ShadowedRectangle{
color: if (isWidescreen){
if (parent.down){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.3)
}else if(parent.hovered){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.7)
}else{
Qt.rgba(1, 1, 1, 0.2)
}
} else {
if (parent.down){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.3)
}else if(parent.hovered){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.7)
}else{
"transparent"
}
}
}
}
Button {
id: skipForwardButton
Layout.rightMargin: Math.floor(Kirigami.Units.smallSpacing / 2)
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.maximumWidth: height
Layout.preferredWidth: height
height: 40
width: 40
Layout.rightMargin:isWidescreen?0:10
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
enabled: UserPlaylistModel.canSkip
onClicked: UserPlaylistModel.next()
icon.name: "media-skip-forward"
icon.width: Kirigami.Units.gridUnit
icon.height: Kirigami.Units.gridUnit
icon.color: "white"
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
contentItem: Item{
Kirigami.Icon {
anchors.centerIn:parent
source:"media-skip-forward"
color: "white"
width: Kirigami.Units.gridUnit
height: Kirigami.Units.gridUnit
}
}
background: Kirigami.ShadowedRectangle{
corners.topRightRadius: 7
corners.bottomRightRadius: 7
color: if (isWidescreen){
if (parent.down){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.3)
}else if(parent.hovered){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.7)
}else{
Qt.rgba(1, 1, 1, 0.2)
}
} else {
if (parent.down){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.3)
}else if(parent.hovered){
Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.hoverColor, "transparent", 0.7)
}else{
"transparent"
}
}
}
}
Label {
color: "white"
visible: info.title && root.isWidescreen
text: PlayerUtils.formatTimestamp(audio.duration)
Layout.leftMargin: 20
}
}
}
+2 -1
View File
@@ -36,7 +36,8 @@ Kirigami.ScrollablePage {
required property string title
required property string videoId
required property string artists
required property var artists
required property string artistsDisplayString
required property int index
onClicked: {
+3 -3
View File
@@ -20,7 +20,7 @@ Flickable {
property int footerSpacing:0
property bool maximized: false
readonly property int progressBarHeight: Kirigami.Units.gridUnit / 6
readonly property int progressBarHeight: Kirigami.Units.gridUnit / 4
readonly property int minimizedPlayerContentHeight: Math.round(Kirigami.Units.gridUnit * 3.5)
readonly property int minimizedPlayerHeight: minimizedPlayerContentHeight + progressBarHeight
@@ -158,8 +158,8 @@ Flickable {
layer.effect: HueSaturation {
cached: true
lightness: -0.5
saturation: 1.9
lightness: -0.7
saturation: 3
layer.enabled: true
layer.effect: FastBlur {
+6 -6
View File
@@ -49,7 +49,7 @@ Item {
pageStack.push("qrc:/SearchPage.qml", {
"searchQuery": text,
objectName: "searchPage"
})
})
} else {
wideScreen
? pageStack.push("qrc:/LibraryPage.qml")
@@ -224,6 +224,8 @@ Item {
Controls.ScrollView {
id: mainScrollView
clip: true
Layout.fillHeight: true
Layout.fillWidth: true
@@ -299,8 +301,6 @@ Item {
filterRegularExpression: searchField.filterExpression
}
delegate: Kirigami.AbstractListItem {
required property string query
id: completionDelegate
highlighted: focus
Kirigami.Theme.colorSet: Kirigami.Theme.Window
@@ -316,7 +316,7 @@ Item {
color: Kirigami.Theme.disabledTextColor
}
Controls.Label{
text: query
text: model.display
Layout.fillWidth: true
}
Controls.ToolButton {
@@ -324,13 +324,13 @@ Item {
text: i18n("remove from search history")
display: Controls.AbstractButton.IconOnly
onClicked: {
Library.removeSearch(query)
Library.removeSearch(model.display)
}
}
}
onClicked: {
searchField.text = query
searchField.text = model.display
searchField.accepted()
}
}
+109 -36
View File
@@ -40,7 +40,8 @@ Library &Library::instance()
FavouritesModel *Library::favourites()
{
return new FavouritesModel(this);
auto future = m_database->getResults<Song>("select * from favourites natural join songs order by favourites.rowid desc");
return new FavouritesModel(std::move(future), this);
}
void Library::addFavourite(const QString &videoId, const QString &title, const QString &artist, const QString &album)
@@ -65,7 +66,7 @@ FavouriteWatcher *Library::favouriteWatcher(const QString &videoId)
SearchHistoryModel *Library::searches()
{
return new SearchHistoryModel(this);
return new SearchHistoryModel(m_database->getResults<SingleValue<QString>>("select distinct (search_query) from searches order by search_id desc"), this);
}
void Library::addSearch(const QString &text)
@@ -80,7 +81,8 @@ void Library::removeSearch(const QString &text)
PlaybackHistoryModel *Library::playbackHistory()
{
return new PlaybackHistoryModel(this, PlaybackHistoryModel::History);
auto future = m_database->getResults<PlayedSong>("select * from played_songs natural join songs");
return new PlaybackHistoryModel(std::move(future), this);
}
void Library::addPlaybackHistoryItem(const QString &videoId, const QString &title, const QString &artist, const QString &album)
@@ -108,7 +110,8 @@ WasPlayedWatcher *Library::wasPlayedWatcher(const QString& videoId)
PlaybackHistoryModel *Library::mostPlayed()
{
return new PlaybackHistoryModel(this, PlaybackHistoryModel::MostPlayed);
auto future = m_database->getResults<PlayedSong>("select * from played_songs natural join songs order by plays desc limit 10");
return new PlaybackHistoryModel(std::move(future), this);
}
QNetworkAccessManager &Library::nam()
@@ -168,35 +171,93 @@ void ThumbnailSource::setVideoId(const QString &id) {
});
}
PlaybackHistoryModel::PlaybackHistoryModel(Library *library, Type type)
: AutoListModel<PlayedSong>(library)
PlaybackHistoryModel::PlaybackHistoryModel(QFuture<std::vector<PlayedSong> > &&songs, QObject *parent)
: QAbstractListModel(parent)
{
auto update = [=, this]() {
auto future = [=, this]() {
if (type == MostPlayed) {
return library->database().getResults<PlayedSong>("select * from played_songs natural join songs order by plays desc limit 10");
} else {
return library->database().getResults<PlayedSong>("select * from played_songs natural join songs");
}
}();
connectFuture(future, this, &PlaybackHistoryModel::updateData);
};
update();
connect(library, &Library::playbackHistoryChanged, this, update);
connectFuture(songs, this, [this](const auto songs) {
beginResetModel();
m_playedSongs = songs;
endResetModel();
});
}
FavouritesModel::FavouritesModel(Library *library)
: AutoListModel<Song>(library)
{
auto update = [=, this]() {
auto future = library->database().getResults<Song>("select * from favourites natural join songs order by favourites.rowid desc");
connectFuture(future, this, &FavouritesModel::updateData);
QHash<int, QByteArray> PlaybackHistoryModel::roleNames() const {
return {
{Roles::VideoId, "videoId"},
{Roles::Title, "title"},
{Roles::Artists, "artists"},
{Roles::ArtistsDisplayString, "artistsDisplayString"},
{Roles::Plays, "plays"}
};
}
update();
connect(library, &Library::favouritesChanged, this, update);
int PlaybackHistoryModel::rowCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : m_playedSongs.size();
}
QVariant PlaybackHistoryModel::data(const QModelIndex &index, int role) const {
switch (role) {
case Roles::VideoId:
return m_playedSongs.at(index.row()).videoId;
case Roles::Title:
return m_playedSongs.at(index.row()).title;
case Roles::Artists:
return QVariant::fromValue(std::vector<meta::Artist> {
{
m_playedSongs.at(index.row()).artist.toStdString(),
{}
}
});
case Roles::ArtistsDisplayString:
return m_playedSongs.at(index.row()).artist;
case Roles::Plays:
return m_playedSongs.at(index.row()).plays;
}
Q_UNREACHABLE();
}
FavouritesModel::FavouritesModel(QFuture<std::vector<Song>> &&songs, QObject *parent)
: QAbstractListModel(parent)
{
connectFuture(songs, this, [this](const auto songs) {
beginResetModel();
m_favouriteSongs = songs;
endResetModel();
});
}
QHash<int, QByteArray> FavouritesModel::roleNames() const {
return {
{Roles::VideoId, "videoId"},
{Roles::Title, "title"},
{Roles::Artists, "artists"},
{Roles::ArtistsDisplayString, "artistsDisplayString"}
};
}
int FavouritesModel::rowCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : m_favouriteSongs.size();
}
QVariant FavouritesModel::data(const QModelIndex &index, int role) const {
switch (role) {
case Roles::VideoId:
return m_favouriteSongs.at(index.row()).videoId;
case Roles::Title:
return m_favouriteSongs.at(index.row()).title;
case Roles::ArtistsDisplayString:
return m_favouriteSongs.at(index.row()).artist;
case Roles::Artists:
return QVariant::fromValue(std::vector<meta::Artist> {
{
m_favouriteSongs.at(index.row()).artist.toStdString(),
{}
}
});
}
Q_UNREACHABLE();
}
FavouriteWatcher::FavouriteWatcher(Library *library, const QString &videoId)
@@ -218,15 +279,27 @@ bool FavouriteWatcher::isFavourite() const {
return m_isFavourite;
}
SearchHistoryModel::SearchHistoryModel(Library *library)
: AutoListModel<SearchQuery>(library)
SearchHistoryModel::SearchHistoryModel(QFuture<std::vector<SingleValue<QString>>> &&historyFuture, QObject *parent)
: QAbstractListModel(parent)
{
auto update = [=, this]() {
auto future = library->database().getResults<SearchQuery>("select distinct (search_query) from searches order by search_id desc");
connectFuture(future, this, &SearchHistoryModel::updateData);
};
update();
connect(library, &Library::searchesChanged, update);
connectFuture(historyFuture, this, [this](const auto history) {
beginResetModel();
m_history = history;
endResetModel();
});
}
int SearchHistoryModel::rowCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : m_history.size();
}
QVariant SearchHistoryModel::data(const QModelIndex &index, int role) const {
switch (role) {
case Qt::DisplayRole:
return m_history[index.row()].value;
}
Q_UNREACHABLE();
}
WasPlayedWatcher::WasPlayedWatcher(Library* library, const QString& videoId)
+54 -59
View File
@@ -15,22 +15,10 @@
#include "asyncytmusic.h"
#include "asyncdatabase.h"
#include "autolistmodel.h"
using namespace desert;
class FavouriteWatcher;
class WasPlayedWatcher;
inline std::vector<meta::Artist> metaArtist(const QString &artist) {
return { meta::Artist { artist.toStdString(), {} } };
}
struct Song {
DESERT_OBJECT
int operator<=>(const Song &other) const = default;
using ColumnTypes = std::tuple<QString, QString, QString, QString>;
static Song fromSql(ColumnTypes tuple) {
@@ -38,26 +26,34 @@ struct Song {
return Song {videoId, title, artist, album};
}
Attribute<u"videoId", QString> videoId;
Attribute<u"title", QString> title;
Attribute<u"artists", QString> artists;
Attribute<u"album", QString> album;
QString videoId;
QString title;
QString artist;
QString album;
};
class Library;
class FavouritesModel : public AutoListModel<Song> {
class FavouritesModel : public QAbstractListModel {
Q_OBJECT
enum Roles {
VideoId = Qt::UserRole + 1,
Title,
Artists,
ArtistsDisplayString
};
public:
FavouritesModel(Library *library);
FavouritesModel(QFuture<std::vector<Song>> &&songs, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
private:
std::vector<Song> m_favouriteSongs;
};
struct PlayedSong {
DESERT_OBJECT
int operator<=>(const PlayedSong &other) const = default;
using ColumnTypes = std::tuple<QString, int, QString, QString, QString>;
static PlayedSong fromSql(ColumnTypes tuple) {
@@ -65,56 +61,55 @@ struct PlayedSong {
return PlayedSong {videoId, title, artist, album, plays};
}
Attribute<u"videoId", QString> videoId;
Attribute<u"title", QString> title;
Attribute<u"artists", QString> artists;
Attribute<u"album", QString> album;
Attribute<u"album", int> plays;
QString videoId;
QString title;
QString artist;
QString album;
int plays;
};
class PlaybackHistoryModel : public AutoListModel<PlayedSong> {
class PlaybackHistoryModel : public QAbstractListModel {
Q_OBJECT
public:
enum Type {
MostPlayed,
History
enum Roles {
VideoId = Qt::UserRole + 1,
Title,
Artists,
ArtistsDisplayString,
Plays
};
PlaybackHistoryModel(Library *library, Type type);
};
Q_ENUM(Roles);
class SearchQuery {
DESERT_OBJECT
PlaybackHistoryModel(QFuture<std::vector<PlayedSong>> &&songs, QObject *parent = nullptr);
using ColumnTypes = std::tuple<QString>;
static SearchQuery fromSql(ColumnTypes tuple) {
auto [query] = tuple;
return { query };
}
int operator<=>(const SearchQuery &other) const = default;
Attribute<u"query", QString> query;
};
class SearchHistoryModel : public AutoListModel<SearchQuery> {
Q_OBJECT
public:
SearchHistoryModel(Library *library);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
private:
std::vector<SearchQuery> m_history;
std::vector<PlayedSong> m_playedSongs;
};
class SearchHistoryModel : public QAbstractListModel {
Q_OBJECT
public:
SearchHistoryModel(QFuture<std::vector<SingleValue<QString>>> &&historyFuture, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
private:
std::vector<SingleValue<QString>> m_history;
};
class Library : public QObject
{
Q_OBJECT
Q_PROPERTY(QAbstractListModel *favourites READ favourites CONSTANT)
Q_PROPERTY(QAbstractListModel *searches READ searches CONSTANT)
Q_PROPERTY(QAbstractListModel *playbackHistory READ playbackHistory CONSTANT)
Q_PROPERTY(QAbstractListModel *mostPlayed READ mostPlayed CONSTANT)
Q_PROPERTY(QAbstractListModel *favourites READ favourites NOTIFY favouritesChanged)
Q_PROPERTY(QAbstractListModel *searches READ searches NOTIFY searchesChanged)
Q_PROPERTY(QAbstractListModel *playbackHistory READ playbackHistory NOTIFY playbackHistoryChanged)
Q_PROPERTY(QAbstractListModel *mostPlayed READ mostPlayed NOTIFY playbackHistoryChanged)
public:
explicit Library(QObject *parent = nullptr);
+3 -3
View File
@@ -10,20 +10,20 @@
#include <QtGlobal>
template <typename Tuple, typename Func, std::size_t i>
inline constexpr void iterate_tuple_impl(Tuple &tup, Func fun)
inline constexpr void iterate_impl(Tuple &tup, Func fun)
{
if constexpr(i >= std::tuple_size_v<std::decay_t<decltype(tup)>>) {
return;
} else {
fun(std::get<i>(tup));
return iterate_tuple_impl<Tuple, Func, i + 1>(tup, fun);
return iterate_impl<Tuple, Func, i + 1>(tup, fun);
}
}
template <typename Tuple, typename Func>
inline constexpr void iterate_tuple(Tuple &tup, Func fun)
{
iterate_tuple_impl<Tuple, Func, 0>(tup, fun);
iterate_impl<Tuple, Func, 0>(tup, fun);
}
/// MultiIterableView is a view over multiple contigous memory arrays, like std::vector or std::array.
-475
View File
@@ -1,475 +0,0 @@
/****************************************************************************
** MIT License
**
** Copyright (C) 2018-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
** Author: André Somers <[email protected]>
**
** This file is part of KDToolBox (https://github.com/KDAB/KDToolBox).
**
** Permission is hereby granted, free of charge, to any person obtaining a copy
** of this software and associated documentation files (the "Software"), to deal
** in the Software without restriction, including without limitation the rights
** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
** copies of the Software, ** and to permit persons to whom the Software is
** furnished to do so, subject to the following conditions:
**
** The above copyright notice and this permission notice (including the next paragraph)
** shall be included in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
** LIABILITY, WHETHER IN AN ACTION OF ** CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
****************************************************************************/
#ifndef UPDATEABLEMODEL_H
#define UPDATEABLEMODEL_H
#include <QModelIndex>
#include <QSet>
#include <QVector>
#include <functional>
// concepts
#define ForwardIt typename
#define RandomIt typename
#define Container typename
#define BinaryPredicate typename
#define EventHandler typename
#define QAIM typename // QAbstractItemModel-derived class
// basic algorithm
// precondition: src and target are both ordered with respect to lessThan
template<ForwardIt FwdIt, Container TargetCollection, BinaryPredicate LessThan, BinaryPredicate HasChanged,
EventHandler OnChanged, EventHandler OnInsert, EventHandler OnRemove, EventHandler OnEqual>
void updateCollection(const FwdIt srcBegin, const FwdIt srcEnd, TargetCollection &target, LessThan lessThan,
HasChanged itemHasChanged, OnChanged onChanged, OnInsert onInsert, OnRemove onRemove,
OnEqual onEqual)
{
auto srcIt = srcBegin;
target.reserve(std::distance(srcBegin, srcEnd));
auto targetIt = std::begin(target);
while (srcIt != srcEnd)
{
if (targetIt == std::end(target))
{
// insert: src has still items left while target has no more items
// so, insert all remaining items from src into target
targetIt = onInsert(srcIt, srcEnd, targetIt);
// targetIt now points to the item after the item(s) just inserted
srcIt = srcEnd;
}
else if (lessThan(*srcIt, *targetIt))
{
// insert: src has one or more items that need to be inserted into target
auto srcInsertEnd = std::next(srcIt);
while (srcInsertEnd != srcEnd && lessThan(*srcInsertEnd, *targetIt))
{
srcInsertEnd++;
}
targetIt = onInsert(srcIt, srcInsertEnd, targetIt);
// targetIt now points to the item after the item(s) just inserted
srcIt = srcInsertEnd;
}
else if (lessThan(*targetIt, *srcIt))
{
// removal: target has items that are not in src (any more), so remove them
auto targetRemoveEnd = std::next(targetIt);
auto targetEnd = std::end(target);
while (targetRemoveEnd != targetEnd && lessThan(*targetRemoveEnd, *srcIt))
{
targetRemoveEnd++;
}
targetIt = onRemove(targetIt, targetRemoveEnd);
// targetIt now points to the item after the items just removed,
// the same item targetEnd was pointing to before the call
}
else
{
// same item, check for changes
auto changes = itemHasChanged(*srcIt, *targetIt);
if (changes)
{
onChanged(srcIt, targetIt, changes);
}
else
{
onEqual(srcIt, targetIt);
}
srcIt++;
targetIt++;
}
}
if (targetIt != std::end(target))
{
// target has some items left, but we're at the end of src, so remove the rest from target
onRemove(targetIt, std::end(target));
}
}
// helper functions
namespace
{
// helper function to create a QSet<T> out of any container<T>.
template<typename T>
auto setFromContainer(T container) -> QSet<typename T::value_type>
{
QSet<typename T::value_type> set;
set.reserve(container.size());
for (const auto value : container)
{
set.insert(value);
}
return set;
}
// helper for manual overload ranking
// highest rank is selected first
template<size_t Rank>
struct OverloadRanker;
template<>
struct OverloadRanker<0>
{
};
template<size_t Rank>
struct OverloadRanker : OverloadRanker<Rank - 1>
{
};
// insert range into container, 2 overloads (using SFINAE) with manual ranking in case of multiple matches
constexpr size_t BestInsertRangeOverloadRank = 1;
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
auto insertRange_imp(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd,
OverloadRanker<1>) // Best match
-> decltype(container.insert(targetInsertBefore, sourceBegin, sourceEnd), InsertIt())
{
// primary option: use insert with a whole range directly
InsertIt it =
std::next(container.insert(targetInsertBefore, sourceBegin, sourceEnd), std::distance(sourceBegin, sourceEnd));
return it;
}
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
auto insertRange_imp(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd,
OverloadRanker<0>) // Fallback
-> InsertIt
{
#if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0))
// Check if, when using QVector in Qt , the value type is default constructable
static_assert(!std::is_same<QVector<typename C::value_type>, C>::value ||
std::is_default_constructible<typename C::value_type>::value,
"When using QVector as your container type, the value type must be default-contructible. Consider "
"using std::vector instead or use a more recent Qt version.");
#endif
// store the location and the length of the inserted items
const auto location = std::distance(std::begin(container), targetInsertBefore);
const auto originalCount = container.size();
// append the new items to the container
std::copy(sourceBegin, sourceEnd, std::back_inserter(container));
// move the items into the right position in the container
const auto b = std::begin(container);
targetInsertBefore = std::next(b, location);
const InsertIt originalEnd = std::next(b, originalCount);
return std::rotate(targetInsertBefore, originalEnd, std::end(container));
}
/**
* Inserts a range into the target container
* @returns an iterator pointing one item past the last item inserted
* @param container is the target container
* @param targetInsertBefore iterator to the item the new items should be inserted before
* @param sourceBegin iterator to the first item to insert into @arg container
* @param sourceEnd iterator to the item past the last item to insert into @arg container
*
* Implementation is done using SFINEA to select an efficient method for the type of
* container used.
*/
// "public API"
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
InsertIt insertRange(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd)
{
return insertRange_imp(container, targetInsertBefore, sourceBegin, sourceEnd,
OverloadRanker<BestInsertRangeOverloadRank>{});
}
// less than on data type
constexpr size_t BestDataLessThanOverloadRank = 1;
template<typename T>
auto dataLessThan_imp(const T &lhs, const T &rhs, OverloadRanker<1>) -> decltype(operator<(lhs, rhs), bool())
{
return operator<(lhs, rhs);
}
template<typename T>
auto dataLessThan_imp(const T &lhs, const T &rhs, OverloadRanker<0>) -> decltype(bool())
{
// fake implementation to stop the compiler from whining. User is probably providing his own via function pointer
Q_UNUSED(lhs);
Q_UNUSED(rhs);
return false;
}
template<typename T>
bool dataLessThan(const T &lhs, const T &rhs)
{
return dataLessThan_imp(lhs, rhs, OverloadRanker<BestDataLessThanOverloadRank>{});
}
}
// actual class to inherit from
template<QAIM BaseModel, typename DataType>
class UpdateableModel : public BaseModel
{
// can't use Q_OBJECT on templates
public:
explicit UpdateableModel(QObject *parent = nullptr)
: BaseModel(parent)
{
}
protected: // types
struct Operations
{
uint inserts;
uint removals;
uint updates;
};
struct DataChanges
{
operator bool() const { return !changedColumns.isEmpty(); }
QVector<int> changedColumns; // important: keep ordered!
QVector<int> changedRoles;
};
enum ChangeMergePolicy
{
AlwaysMergeNeighbouringRows,
MergeWhenColumnsMatch,
MergeWhenRolesMatch,
MergeOnPerfectMatch
};
using HasChangesFunction = std::function<DataChanges(const DataType & /*lhs*/, const DataType & /*rhs*/)>;
protected: // methods
void setChangeMergePolicy(ChangeMergePolicy policy) { m_changeMergePolicy = policy; }
ChangeMergePolicy changeMergePolicy() const { return m_changeMergePolicy; }
virtual bool lessThan(const DataType &lhs, const DataType &rhs) const { return dataLessThan(lhs, rhs); }
virtual DataChanges itemHasChanged(const DataType &lhs, const DataType &rhs) const
{
Q_UNUSED(lhs);
Q_UNUSED(rhs);
return {};
}
template<ForwardIt Iterator, Container DataContainer, BinaryPredicate LessThan>
Operations updateData(Iterator srcBegin, Iterator srcEnd, DataContainer &targetContainer, LessThan lessThan,
HasChangesFunction itemHasChanged)
{
Operations ops{0, 0, 0};
// events
auto onChanged = [this, &targetContainer, &ops](Iterator lhs, typename DataContainer::iterator rhs,
const DataChanges &changes) {
*rhs = *lhs;
int row = std::distance(targetContainer.begin(), rhs);
addChange(row, changes.changedColumns, changes.changedRoles);
ops.updates++;
};
auto onInsert = [this, &targetContainer, &ops](Iterator lhsBegin, Iterator lhsEnd,
typename DataContainer::iterator rhsInsertAt) {
flushCachedChanges();
int firstNewRow = std::distance(targetContainer.begin(), rhsInsertAt);
int lastNewRow = firstNewRow + std::distance(lhsBegin, lhsEnd) - 1;
beginInsertRows(firstNewRow, lastNewRow);
auto rangeEnd = insertRange(targetContainer, rhsInsertAt, lhsBegin, lhsEnd);
endInsertRows();
ops.inserts += lhsEnd - lhsBegin;
return rangeEnd;
};
auto onRemove = [this, &targetContainer, &ops](typename DataContainer::iterator rhsBegin,
typename DataContainer::iterator rhsEnd) {
flushCachedChanges();
const auto targetContainerBegin = std::begin(targetContainer);
int firstRemovedRow = std::distance(targetContainerBegin, rhsBegin);
int lastRemovedRow = std::distance(targetContainerBegin, rhsEnd) - 1;
beginRemoveRows(firstRemovedRow, lastRemovedRow);
auto newRhsEnd = targetContainer.erase(rhsBegin, rhsEnd);
endRemoveRows();
ops.removals += std::distance(rhsBegin, rhsEnd);
return newRhsEnd;
};
auto onEqual = [this](Iterator /*lhs*/, typename DataContainer::iterator /*rhs*/) { flushCachedChanges(); };
updateCollection(srcBegin, srcEnd, targetContainer, lessThan, itemHasChanged, onChanged, onInsert, onRemove,
onEqual);
flushCachedChanges();
return ops;
}
template<ForwardIt Iterator, Container Data>
Operations updateData(Iterator srcBegin, Iterator srcEnd, Data &targetContainer, HasChangesFunction hasChanged = {})
{
auto lessThanFunction = [this](const DataType &lhs, const DataType &rhs) { return this->lessThan(lhs, rhs); };
if (!hasChanged)
{
hasChanged = [this](const DataType &lhs, const DataType &rhs) -> DataChanges {
return this->itemHasChanged(lhs, rhs);
};
}
return updateData(srcBegin, srcEnd, targetContainer, lessThanFunction, hasChanged);
}
protected:
// Shadowing the methods in the base model, because MSVC thinks the onRemove and onInsert
// lambdas in updateData are not allowed to access the protected member functions in BaseClass.
// GCC is ok with it...
void beginInsertRows(int firstRow, int lastRow) { BaseModel::beginInsertRows(QModelIndex(), firstRow, lastRow); }
void endInsertRows() { BaseModel::endInsertRows(); }
void beginRemoveRows(int firstRow, int lastRow) { BaseModel::beginRemoveRows(QModelIndex(), firstRow, lastRow); }
void endRemoveRows() { BaseModel::endRemoveRows(); }
private:
void flushCachedChanges()
{
if (m_firstChangedRow == -1)
return;
// reconstruct vector of changed roles
QVector<int> roles;
roles.reserve(m_changedRoles.count());
for (int role : qAsConst(m_changedRoles))
{
roles.append(role);
}
// convert the vector of ints indicating changed columns into a vector of pairs of ints indicating ranges.
// for example, the vector {1, 2, 3, 5, 6, 9} would be converted to {{1, 3}, {5, 6}, {9, 9}}
typedef QVector<QPair<int, int>> ColumnRanges;
auto accumulator = [](ColumnRanges ranges, int column) {
if (ranges.isEmpty() || (ranges.last().second < column - 1))
{
ranges.append(qMakePair(column, column));
}
else
{
ranges[ranges.count() - 1].second = column;
}
return ranges;
};
const auto ranges =
std::accumulate(m_changedColumns.cbegin(), m_changedColumns.cend(), ColumnRanges(), accumulator);
for (const auto &columnRange : ranges)
{
QModelIndex topLeft = BaseModel::index(m_firstChangedRow, columnRange.first);
QModelIndex bottomRight = BaseModel::index(m_lastChangedRow, columnRange.second);
Q_EMIT BaseModel::dataChanged(topLeft, bottomRight, roles);
}
m_firstChangedRow = -1;
}
void addChange(int row, const QVector<int> &columns, const QVector<int> &roles)
{
auto mergeColumns = [this](const QVector<int> &columns) {
QVector<int> columnUnion;
// we have to cast this to QAbstractItemModel, to work around QAbstractListModel making columnCount private
columnUnion.reserve(static_cast<QAbstractItemModel *>(this)->columnCount());
auto inserter = std::back_inserter(columnUnion);
std::set_union(m_changedColumns.cbegin(), m_changedColumns.cend(), columns.cbegin(), columns.cend(),
inserter);
m_changedColumns = std::move(columnUnion);
};
if (m_firstChangedRow > -1)
{
// from here rows are guaranteed to be sequential, so no need to check for that
m_lastChangedRow = row; // optimistic, but lets see...
switch (m_changeMergePolicy)
{
case AlwaysMergeNeighbouringRows:
m_changedRoles = m_changedRoles.intersect(setFromContainer(roles));
mergeColumns(columns);
return;
case MergeWhenColumnsMatch:
if (m_changedColumns == columns)
{
m_changedRoles = m_changedRoles.intersect(setFromContainer(roles));
return;
}
break;
case MergeWhenRolesMatch:
if (m_changedRoles == setFromContainer(roles))
{
mergeColumns(columns);
return;
}
break;
case MergeOnPerfectMatch:
if (m_changedColumns == columns && m_changedRoles == setFromContainer(roles))
{
return;
}
break;
}
m_lastChangedRow = row - 1; // we were too optimistic
flushCachedChanges();
}
// just copy the details of the first change
m_firstChangedRow = row;
m_lastChangedRow = row;
m_changedColumns = columns;
m_changedRoles = setFromContainer(roles);
}
ChangeMergePolicy m_changeMergePolicy = MergeOnPerfectMatch;
int m_firstChangedRow = -1;
int m_lastChangedRow = -1;
QVector<int> m_changedColumns;
QSet<int> m_changedRoles;
};
#undef ForwardIt
#undef RandomIt
#undef Container
#undef BinaryPredicate
#undef EventHandler
#undef QAIM
#endif // UPDATEABLEMODEL_H
+48
View File
@@ -129,6 +129,20 @@ QString UserPlaylistModel::nextVideoId() const
return QString::fromStdString((currentTrack + 1)->video_id);
}
QString UserPlaylistModel::previousVideoId() const
{
auto currentTrack = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
[this](const watch::Playlist::Track &track) {
return track.video_id == m_currentVideoId.toStdString();
});
if (currentTrack == m_playlist.tracks.end() || currentTrack - 1 == m_playlist.tracks.end()) {
return {};
}
return QString::fromStdString((currentTrack - 1)->video_id);
}
QString UserPlaylistModel::currentVideoId() const
{
return m_currentVideoId;
@@ -141,6 +155,8 @@ void UserPlaylistModel::setCurrentVideoId(const QString &videoId)
emitCurrentVideoChanged(old);
Q_EMIT currentVideoIdChanged();
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
}
int UserPlaylistModel::currentIndex() const
@@ -164,12 +180,32 @@ bool UserPlaylistModel::canSkip() const
&& currentTrackIt != m_playlist.tracks.end()
&& !m_playlist.tracks.empty();
}
bool UserPlaylistModel::canSkipBack() const
{
const auto currentTrackIt = std::ranges::find_if(m_playlist.tracks, [this](const watch::Playlist::Track &track) {
return track.video_id == m_currentVideoId.toStdString();
});
return currentTrackIt != m_playlist.tracks.begin()
&& !m_playlist.tracks.empty();
}
void UserPlaylistModel::next()
{
setCurrentVideoId(nextVideoId());
Q_EMIT currentVideoIdChanged();
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
}
void UserPlaylistModel::previous()
{
setCurrentVideoId(previousVideoId());
Q_EMIT currentVideoIdChanged();
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
}
void UserPlaylistModel::skipTo(const QString &videoId)
@@ -179,6 +215,8 @@ void UserPlaylistModel::skipTo(const QString &videoId)
emitCurrentVideoChanged(old);
Q_EMIT currentVideoIdChanged();
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
}
void UserPlaylistModel::playNext(const QString &videoId, const QString &title, const std::vector<meta::Artist> &artists)
@@ -199,6 +237,8 @@ void UserPlaylistModel::playNext(const QString &videoId, const QString &title, c
endInsertRows();
setCurrentVideoId(videoId);
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
return;
}
@@ -208,6 +248,8 @@ void UserPlaylistModel::playNext(const QString &videoId, const QString &title, c
m_playlist.tracks.push_back(std::move(track));
endInsertRows();
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
return;
}
@@ -216,6 +258,8 @@ void UserPlaylistModel::playNext(const QString &videoId, const QString &title, c
m_playlist.tracks.insert(currentIt + 1, std::move(track));
endInsertRows();
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
}
void UserPlaylistModel::append(const QString &videoId, const QString &title, const std::vector<meta::Artist> &artists)
@@ -242,6 +286,8 @@ void UserPlaylistModel::clear()
setCurrentVideoId({});
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
}
void UserPlaylistModel::remove(const QString &videoId)
@@ -259,6 +305,8 @@ void UserPlaylistModel::remove(const QString &videoId)
endRemoveRows();
Q_EMIT canSkipChanged();
Q_EMIT canSkipBackChanged();
}
void UserPlaylistModel::shufflePlaylist()
+8
View File
@@ -23,6 +23,7 @@ class UserPlaylistModel : public AbstractYTMusicModel
Q_PROPERTY(QString currentVideoId READ currentVideoId NOTIFY currentVideoIdChanged)
Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentVideoIdChanged)
Q_PROPERTY(bool canSkip READ canSkip NOTIFY canSkipChanged)
Q_PROPERTY(bool canSkipBack READ canSkipBack NOTIFY canSkipBackChanged)
Q_PROPERTY(QString lyrics READ lyrics NOTIFY lyricsChanged)
public:
@@ -46,6 +47,7 @@ public:
Q_SIGNAL void initialVideoIdChanged();
QString nextVideoId() const;
QString previousVideoId() const;
QString currentVideoId() const;
void setCurrentVideoId(const QString &videoId);
@@ -54,7 +56,11 @@ public:
int currentIndex() const;
bool canSkip() const;
bool canSkipBack() const;
Q_SIGNAL void canSkipChanged();
Q_SIGNAL void canSkipBackChanged();
QString playlistId() const;
void setPlaylistId(const QString &playlistId);
@@ -68,6 +74,8 @@ public:
Q_SIGNAL void lyricsChanged();
Q_INVOKABLE void next();
Q_INVOKABLE void previous();
Q_INVOKABLE void skipTo(const QString &videoId);
Q_INVOKABLE void playNext(const QString &videoId, const QString &title, const std::vector<meta::Artist> &artists);
Q_INVOKABLE void append(const QString &videoId, const QString &title, const std::vector<meta::Artist> &artists);