diff --git a/core/include/JellyfinQt/jellyfin.h b/core/include/JellyfinQt/jellyfin.h index 257680e..6729060 100644 --- a/core/include/JellyfinQt/jellyfin.h +++ b/core/include/JellyfinQt/jellyfin.h @@ -36,6 +36,7 @@ #include "viewmodel/loader.h" #include "viewmodel/modelstatus.h" #include "viewmodel/playbackmanager.h" +#include "viewmodel/playlist.h" #include "viewmodel/userdata.h" #include "viewmodel/usermodel.h" #include "viewmodel/user.h" diff --git a/core/include/JellyfinQt/model/playlist.h b/core/include/JellyfinQt/model/playlist.h index c829ac0..a33030e 100644 --- a/core/include/JellyfinQt/model/playlist.h +++ b/core/include/JellyfinQt/model/playlist.h @@ -52,6 +52,11 @@ public: /// Returns the current item in the queue QSharedPointer currentItem(); QSharedPointer nextItem(); + /** + * @return the current index of the playing item if it is in the list. If playing from the queue, + * returns -1. + */ + int currentItemIndexInList() const; /** * @brief Determine the previous item to be played. @@ -63,13 +68,26 @@ public: */ void next(); - // int queueSize() { return m_queue.size(); }; + int queueSize() { return m_queue.size(); }; int listSize() const { return m_list.size(); }; int totalSize() const { return m_queue.size() + m_list.size(); } - QSharedPointer listAt(int index) const; /** - * @brief Removes all the items from the playlist + * @brief Returns the item at the given index of the currently selected playlist, excluding the queue. + * @param index + * @return The given item. + */ + QSharedPointer listAt(int index) const; + + /** + * @brief Returns the item at the given index of the currently queue, excluding the playlist. + * @param index + * @return The given item. + */ + QSharedPointer queueAt(int index) const; + + /** + * @brief Removes all the items from the playlist, but not from the queue. */ void clearList(); @@ -83,11 +101,26 @@ public: * @param index The index to start from. */ void play(int index = 0); + + /** + * @brief playingFromQueue + * @return True if the currently played item comes from the queue. + */ + bool playingFromQueue() const; + signals: + void beforeListCleared(); void listCleared(); - void itemsAddedToQueue(int index, int count); - void itemsAddedToList(int index, int count); + void beforeItemsAddedToQueue(int index, int count); + void beforeItemsAddedToList(int index, int count); + void itemsAddedToQueue(); + void itemsAddedToList(); + void beforeItemsRemovedFromQueue(int index, int count); + void beforeItemsRemovedFromList(int index, int count); + void itemsRemovedFromQueue(); + void itemsRemovedFromList(); void listReshuffled(); + void currentItemChanged(); private: void reshuffle(); diff --git a/core/include/JellyfinQt/model/shuffle.h b/core/include/JellyfinQt/model/shuffle.h index 55c4731..8c5b66d 100644 --- a/core/include/JellyfinQt/model/shuffle.h +++ b/core/include/JellyfinQt/model/shuffle.h @@ -103,6 +103,7 @@ public: virtual int currentItem() const override; virtual int nextItem() const override; + virtual int itemAt(int index) const override; virtual void previous() override; virtual void next() override; @@ -122,6 +123,7 @@ public: ListShuffleBase(const Playlist *parent); virtual int currentItem() const override; virtual int nextItem() const override; + virtual int itemAt(int index) const override; protected: QVector m_map; }; diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h index 0081788..24d6bc4 100644 --- a/core/include/JellyfinQt/viewmodel/playbackmanager.h +++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h @@ -41,6 +41,7 @@ #include "../model/playlist.h" #include "../support/jsonconv.h" #include "../viewmodel/item.h" +#include "../viewmodel/playlist.h" #include "../apiclient.h" #include "itemmodel.h" @@ -83,8 +84,8 @@ public: // Current Item and queue informatoion Q_PROPERTY(QObject *item READ item NOTIFY itemChanged) - // Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged) Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged) + Q_PROPERTY(Jellyfin::ViewModel::Playlist *queue READ queue NOTIFY queueChanged) // Current media player related property getters Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged) @@ -105,7 +106,7 @@ public: QObject *mediaObject() const { return m_mediaPlayer; } qint64 position() const { return m_mediaPlayer->position(); } qint64 duration() const { return m_mediaPlayer->duration(); } - //ItemModel *queue() const { return m_queue; } + ViewModel::Playlist *queue() const { return m_displayQueue; } int queueIndex() const { return m_queueIndex; } // Current media player related property getters @@ -129,7 +130,7 @@ signals: void mediaObjectChanged(QObject *newMediaObject); void positionChanged(qint64 newPosition); void durationChanged(qint64 newDuration); - //void queueChanged(ItemModel *newQue); + void queueChanged(QAbstractItemModel *newQueue); void queueIndexChanged(int newIndex); void playbackStateChanged(QMediaPlayer::State newState); void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus); @@ -198,6 +199,8 @@ private: QSharedPointer m_nextItem; /// The currently played item that will be shown in the GUI ViewModel::Item *m_displayItem = new ViewModel::Item(this); + /// The currently played queue that will be shown in the GUI + ViewModel::Playlist *m_displayQueue = nullptr; // Properties for making the streaming request. QString m_streamUrl; diff --git a/core/include/JellyfinQt/viewmodel/playlist.h b/core/include/JellyfinQt/viewmodel/playlist.h index 7e43d92..4e72916 100644 --- a/core/include/JellyfinQt/viewmodel/playlist.h +++ b/core/include/JellyfinQt/viewmodel/playlist.h @@ -21,36 +21,81 @@ #include -#include -#include -#include +#include +#include +#include #include -#include -#include -#include +#include +#include #include "../apiclient.h" +#include "../model/playlist.h" #include "itemmodel.h" namespace Jellyfin { namespace ViewModel { /** - * @brief Playlist/queue that can be exposed to the UI. It also containts the playlist-related logic, - * which is mostly relevant + * @brief Indicator in which part of the playing queue a given item is positioned. */ -/*class Playlist : public ItemModel { +class NowPlayingSection { + Q_GADGET +public: + enum Value { + Queue, + NowPlaying, + }; + Q_ENUM(Value); +}; + +/** + * @brief Playlist/queue that can be exposed to QML. + */ +class Playlist : public QAbstractListModel { Q_OBJECT friend class ItemUrlFetcherThread; public: - explicit Playlist(ApiClient *apiClient, QObject *parent = nullptr); + enum RoleNames { + // Item properties + name = Qt::UserRole + 1, + artists, + runTimeTicks, + + // Non-item properties + playing, + section, + }; + explicit Playlist(Model::Playlist *data, QObject *parent = nullptr); + + QVariant data(const QModelIndex &parent, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent) const override; + QHash roleNames() const override; + private slots: - void onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex); - void onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int destinationRow); - void onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex); - void onItemsReset(); -};*/ + void onBeforePlaylistCleared(); + void onPlaylistCleared(); + void onBeforeItemsAddedToList(int startIndex, int amount); + void onBeforeItemsAddedToQueue(int startIndex, int amount); + void onItemsAddedToList(); + void onItemsAddedToQueue(); + void onBeforeItemsRemovedFromList(int startIndex, int amount); + void onBeforeItemsRemovedFromQueue(int startIndex, int amount); + void onItemsRemovedFromList(); + void onItemsRemovedFromQueue(); + void onReshuffled(); + void onPlayingItemChanged(); +private: + Model::Playlist *m_data; + // The index of the last played item. + int m_lastPlayedRow = -1; + + /** + * @param index The index, from 0..rowCount(); + * @return True if the item at the current index is being played, false otherwise. + */ + bool isPlaying(int index) const; +}; diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index 4d05fb2..89cda45 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -34,6 +34,7 @@ void registerTypes(const char *uri) { qmlRegisterUncreatableType(uri, 1, 0, "BaseModelLoader", "Please use one of its subclasses"); qmlRegisterType(uri, 1, 0, "ItemModel"); qmlRegisterType(uri, 1, 0, "UserModel"); + qmlRegisterUncreatableType(uri, 1, 0, "Playlist", "Available via PlaybackManager"); // Loaders qmlRegisterUncreatableType(uri, 1, 0, "LoaderBase", "Use one of its subclasses"); @@ -52,6 +53,7 @@ void registerTypes(const char *uri) { qmlRegisterUncreatableType(uri, 1, 0, "ModelStatus", "Is an enum"); qmlRegisterUncreatableType(uri, 1, 0, "PlayMethod", "Is an enum"); qmlRegisterUncreatableType(uri, 1, 0, "ItemFields", "Is an enum"); + qmlRegisterUncreatableType(uri, 1, 0, "NowPlayingSection", "Is an enum"); qRegisterMetaType(); } diff --git a/core/src/model/playlist.cpp b/core/src/model/playlist.cpp index e8d8c37..da4f755 100644 --- a/core/src/model/playlist.cpp +++ b/core/src/model/playlist.cpp @@ -28,6 +28,7 @@ Playlist::Playlist(QObject *parent) m_shuffler(new NoShuffle(this)){} void Playlist::clearList() { + emit beforeListCleared(); m_currentItem.clear(); m_nextItem.clear(); m_list.clear(); @@ -50,13 +51,16 @@ void Playlist::previous() { } m_nextItemFromQueue = !m_queue.isEmpty(); m_currentItemFromQueue = false; + emit currentItemChanged(); } void Playlist::next() { // Determine the new current item if (!m_queue.isEmpty()) { m_currentItem = m_queue.first(); + emit beforeItemsRemovedFromQueue(0, 1); m_queue.removeFirst(); + emit itemsRemovedFromQueue(); m_currentItemFromQueue = true; } else if (!m_list.isEmpty()) { // The queue is empty @@ -89,16 +93,33 @@ void Playlist::next() { } else { m_nextItem.clear(); } + emit currentItemChanged(); } QSharedPointer Playlist::listAt(int index) const { - return m_list.at(index); + if (m_shuffler->canShuffleInAdvance()) { + return m_list.at(m_shuffler->itemAt(index)); + } else { + return m_list.at(index); + } +} + +QSharedPointer Playlist::queueAt(int index) const { + return m_queue.at(index); } QSharedPointer Playlist::currentItem() { return m_currentItem; } +int Playlist::currentItemIndexInList() const { + if (m_currentItemFromQueue) { + return -1; + } else { + return m_shuffler->currentItem(); + } +} + QSharedPointer Playlist::nextItem() { return m_nextItem; } @@ -107,10 +128,11 @@ void Playlist::appendToList(ViewModel::ItemModel &model) { int start = m_list.size(); int count = model.size(); m_list.reserve(count); + emit beforeItemsAddedToList(start, count); for (int i = 0; i < count; i++) { m_list.append(QSharedPointer(model.at(i))); } - emit itemsAddedToList(start, count); + emit itemsAddedToList(); reshuffle(); } @@ -130,6 +152,7 @@ void Playlist::reshuffle() { } } emit listReshuffled(); + emit currentItemChanged(); } void Playlist::play(int index) { @@ -142,6 +165,11 @@ void Playlist::play(int index) { m_nextItem.clear(); } } + emit currentItemChanged(); +} + +bool Playlist::playingFromQueue() const { + return m_currentItemFromQueue; } } // NS Model diff --git a/core/src/model/shuffle.cpp b/core/src/model/shuffle.cpp index 25d816c..b6ee65b 100644 --- a/core/src/model/shuffle.cpp +++ b/core/src/model/shuffle.cpp @@ -63,6 +63,10 @@ int NoShuffle::previousIndex() const { } } +int NoShuffle::itemAt(int index) const { + return index; +} + int NoShuffle::nextIndex() const { if (m_repeatAll) { return (m_index + 1) % m_playlist->listSize(); @@ -91,6 +95,10 @@ int ListShuffleBase::nextItem() const { return m_map[nextIndex()]; } +int ListShuffleBase::itemAt(int index) const { + return m_map[index]; +} + // SimpleListShuffle SimpleListShuffle::SimpleListShuffle(const Playlist *parent) : ListShuffleBase(parent) {} diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp index 3a93fe0..0b84311 100644 --- a/core/src/viewmodel/playbackmanager.cpp +++ b/core/src/viewmodel/playbackmanager.cpp @@ -40,6 +40,8 @@ PlaybackManager::PlaybackManager(QObject *parent) m_mediaPlayer(new QMediaPlayer(this)), m_urlFetcherThread(new ItemUrlFetcherThread(this)), m_queue(new Model::Playlist(this)) { + + m_displayQueue = new ViewModel::Playlist(m_queue, this); // Set up connections. m_updateTimer.setInterval(10000); // 10 seconds m_updateTimer.setSingleShot(false); diff --git a/core/src/viewmodel/playlist.cpp b/core/src/viewmodel/playlist.cpp index 8ee2ae4..e48a289 100644 --- a/core/src/viewmodel/playlist.cpp +++ b/core/src/viewmodel/playlist.cpp @@ -21,15 +21,141 @@ namespace Jellyfin { namespace ViewModel { -/*Playlist::Playlist(ApiClient *apiClient, QObject *parent) - : ItemModel(parent) { +Playlist::Playlist(Model::Playlist *data, QObject *parent) + : QAbstractListModel(parent), + m_data(data) { - connect(this, &QAbstractListModel::rowsInserted, this, &Playlist::onItemsAdded); - connect(this, &QAbstractListModel::rowsRemoved, this, &Playlist::onItemsRemoved); - connect(this, &QAbstractListModel::rowsMoved, this, &Playlist::onItemsMoved); - connect(this, &QAbstractListModel::modelReset, this, &Playlist::onItemsReset); + connect(data, &Model::Playlist::beforeListCleared, this, &Playlist::onBeforePlaylistCleared); + connect(data, &Model::Playlist::listCleared, this, &Playlist::onPlaylistCleared); + connect(data, &Model::Playlist::beforeItemsAddedToList, this, &Playlist::onBeforeItemsAddedToList); + connect(data, &Model::Playlist::beforeItemsAddedToQueue, this, &Playlist::onBeforeItemsAddedToQueue); + connect(data, &Model::Playlist::itemsAddedToList, this, &Playlist::onItemsAddedToList); + connect(data, &Model::Playlist::itemsAddedToQueue, this, &Playlist::onItemsAddedToQueue); + connect(data, &Model::Playlist::beforeItemsRemovedFromList, this, &Playlist::onBeforeItemsRemovedFromList); + connect(data, &Model::Playlist::beforeItemsRemovedFromQueue, this, &Playlist::onBeforeItemsRemovedFromQueue); + connect(data, &Model::Playlist::itemsRemovedFromList, this, &Playlist::onItemsRemovedFromList); + connect(data, &Model::Playlist::itemsRemovedFromQueue, this, &Playlist::onItemsRemovedFromQueue); + connect(data, &Model::Playlist::listReshuffled, this, &Playlist::onReshuffled); + connect(data, &Model::Playlist::currentItemChanged, this, &Playlist::onPlayingItemChanged); +} + +int Playlist::rowCount(const QModelIndex &parent) const { + if (!parent.isValid()) return m_data->totalSize(); + return 0; +} +QHash Playlist::roleNames() const { + return { + {RoleNames::name, "name"}, + {RoleNames::artists, "artists"}, + {RoleNames::runTimeTicks, "runTimeTicks"}, + {RoleNames::section, "section"}, + {RoleNames::playing, "playing"}, + }; +}; + +QVariant Playlist::data(const QModelIndex &index, int role) const { + if (!index.isValid()) return QVariant(); + if (index.row() >= m_data->totalSize()) return QVariant(); + + bool inQueue = index.row() < m_data->queueSize(); + // Handle the special "section" role + if (role == RoleNames::section) { + if (inQueue) { + return QVariant(NowPlayingSection::Queue); + } else { + return QVariant(NowPlayingSection::NowPlaying); + } + } else if (role == RoleNames::playing) { + return isPlaying(index.row()); + } + + // Handle the other roles + QSharedPointer rowData; + + if (inQueue) { + rowData = m_data->queueAt(index.row()); + } else { + rowData = m_data->listAt((index.row() - m_data->queueSize())); + } + + switch(role) { + case RoleNames::name: + return QVariant(rowData->name()); + case RoleNames::artists: + return QVariant(rowData->artists()); + case RoleNames::runTimeTicks: + return QVariant(rowData->runTimeTicks().value_or(-1)); + default: + return QVariant(); + + } + + return QVariant(); +} + +void Playlist::onBeforePlaylistCleared() { + onBeforeItemsRemovedFromList(0, m_data->listSize()); +} +void Playlist::onPlaylistCleared() { + onItemsRemovedFromList(); +} + +void Playlist::onBeforeItemsAddedToList(int startIndex, int count) { + int start = startIndex + m_data->queueSize(); + this->beginInsertRows(QModelIndex(), start, start + count - 1); +} + +void Playlist::onItemsAddedToList() { + this->endInsertRows(); +} + +void Playlist::onBeforeItemsAddedToQueue(int startIndex, int count) { + this->beginInsertRows(QModelIndex(), startIndex, startIndex + count - 1); +} + +void Playlist::onItemsAddedToQueue() { + this->endInsertRows(); +} + +void Playlist::onBeforeItemsRemovedFromList(int startIndex, int count) { + int start = startIndex + m_data->queueSize(); + this->beginRemoveRows(QModelIndex(), start, start + count - 1); +} + +void Playlist::onItemsRemovedFromList() { + this->endInsertRows(); +} + +void Playlist::onBeforeItemsRemovedFromQueue(int startIndex, int count) { + this->beginRemoveRows(QModelIndex(), startIndex, startIndex + count - 1); +} + +void Playlist::onItemsRemovedFromQueue() { + this->endRemoveRows(); +} + +void Playlist::onReshuffled() { + this->beginResetModel(); + this->endResetModel(); +} + +void Playlist::onPlayingItemChanged() { + if (m_lastPlayedRow >= 0 ) { + this->dataChanged(index(m_lastPlayedRow), index(m_lastPlayedRow), {RoleNames::playing}); + } + int newIndex = 0; + if (!m_data->playingFromQueue()) { + newIndex = m_data->queueSize() + m_data->currentItemIndexInList(); + } + m_lastPlayedRow = newIndex; + emit this->dataChanged(index(newIndex), index(newIndex), {RoleNames::playing}); +} + +bool Playlist::isPlaying(int index) const { + return (m_data->playingFromQueue() && index == 0) + || (m_data->currentItemIndexInList() == index - m_data->queueSize()); +} -}*/ } // NS ViewModel } // NS Jellyfin diff --git a/sailfish/qml/components/PlayQueue.qml b/sailfish/qml/components/PlayQueue.qml index 73ddffe..b394f5f 100644 --- a/sailfish/qml/components/PlayQueue.qml +++ b/sailfish/qml/components/PlayQueue.qml @@ -1,16 +1,34 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "music" SilicaListView { - header: SectionHeader { text: qsTr("Play queue") } + //header: PageHeader { title: qsTr("Play queue") } + section.property: "section" + section.delegate: SectionHeader { + text: { + switch(section) { + case J.NowPlayingSection.Queue: + //: Now playing page queue section header + return qsTr("Queue") + case J.NowPlayingSection.NowPlaying: + //: Now playing page playlist section header + return qsTr("Playlist") + default: + return qsTr("Unknown section") + } + } + } delegate: SongDelegate { artists: model.artists name: model.name width: parent.width - indexNumber: ListView.index + indexNumber: index + 1 + duration: model.runTimeTicks + playing: model.playing } + clip: true } diff --git a/sailfish/qml/components/music/SongDelegate.qml b/sailfish/qml/components/music/SongDelegate.qml index d0604a5..0088e2c 100644 --- a/sailfish/qml/components/music/SongDelegate.qml +++ b/sailfish/qml/components/music/SongDelegate.qml @@ -27,6 +27,7 @@ ListItem { property real duration property string name property int indexNumber + property bool playing contentHeight: songName.height + songArtists.height + 2 * Theme.paddingMedium width: parent.width @@ -49,6 +50,7 @@ ListItem { horizontalAlignment: Text.AlignRight font.pixelSize: Theme.fontSizeExtraLarge width: indexMetrics.width + highlighted: playing } Label { @@ -64,6 +66,7 @@ ListItem { text: name font.pixelSize: Theme.fontSizeMedium truncationMode: TruncationMode.Fade + highlighted: down || playing } Label { id: songArtists @@ -78,6 +81,7 @@ ListItem { font.pixelSize: Theme.fontSizeSmall truncationMode: TruncationMode.Fade color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + highlighted: down || playing } Label { @@ -91,5 +95,6 @@ ListItem { text: Utils.ticksToText(songDelegateRoot.duration) font.pixelSize: Theme.fontSizeSmall color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + highlighted: down || playing } } diff --git a/sailfish/qml/harbour-sailfin.qml b/sailfish/qml/harbour-sailfin.qml index f464046..735732c 100644 --- a/sailfish/qml/harbour-sailfin.qml +++ b/sailfish/qml/harbour-sailfin.qml @@ -50,7 +50,7 @@ ApplicationWindow { ApiClient { id: _apiClient objectName: "Test" - supportedCommands: [J.GeneralCommandType.Play, J.GeneralCommandType.DisplayContent, J.GeneralCommandType.DisplayMessage] + supportedCommands: [GeneralCommandType.Play, GeneralCommandType.DisplayContent, GeneralCommandType.DisplayMessage] } initialPage: Component {