From 9d4c521819088cea5819a88ac01208acaaeb954d Mon Sep 17 00:00:00 2001 From: janbar Date: Tue, 26 Dec 2017 12:15:10 +0100 Subject: [PATCH 1/9] backend version 3.0 --- backend/modules/NosonApp/albumsmodel.cpp | 67 +++- backend/modules/NosonApp/albumsmodel.h | 12 +- backend/modules/NosonApp/artistsmodel.cpp | 66 +++- backend/modules/NosonApp/artistsmodel.h | 10 +- backend/modules/NosonApp/favoritesmodel.cpp | 72 +++- backend/modules/NosonApp/favoritesmodel.h | 33 +- backend/modules/NosonApp/genresmodel.cpp | 66 +++- backend/modules/NosonApp/genresmodel.h | 10 +- backend/modules/NosonApp/listmodel.cpp | 6 +- backend/modules/NosonApp/listmodel.h | 14 +- backend/modules/NosonApp/mediamodel.cpp | 395 +++++++++++++------- backend/modules/NosonApp/mediamodel.h | 62 +-- backend/modules/NosonApp/player.cpp | 87 +++++ backend/modules/NosonApp/player.h | 7 + backend/modules/NosonApp/playlistsmodel.cpp | 66 +++- backend/modules/NosonApp/playlistsmodel.h | 10 +- backend/modules/NosonApp/queuemodel.cpp | 66 +++- backend/modules/NosonApp/queuemodel.h | 10 +- backend/modules/NosonApp/radiosmodel.cpp | 66 +++- backend/modules/NosonApp/radiosmodel.h | 10 +- backend/modules/NosonApp/renderingmodel.cpp | 45 ++- backend/modules/NosonApp/renderingmodel.h | 8 +- backend/modules/NosonApp/roomsmodel.cpp | 126 ++++--- backend/modules/NosonApp/roomsmodel.h | 16 +- backend/modules/NosonApp/servicesmodel.cpp | 62 ++- backend/modules/NosonApp/servicesmodel.h | 8 +- backend/modules/NosonApp/sonos.cpp | 206 +++++++++- backend/modules/NosonApp/sonos.h | 19 + backend/modules/NosonApp/tracksmodel.cpp | 158 ++++++-- backend/modules/NosonApp/tracksmodel.h | 19 +- backend/modules/NosonApp/zonesmodel.cpp | 67 +++- backend/modules/NosonApp/zonesmodel.h | 12 +- 32 files changed, 1435 insertions(+), 446 deletions(-) diff --git a/backend/modules/NosonApp/albumsmodel.cpp b/backend/modules/NosonApp/albumsmodel.cpp index ace1b3a7..676148e6 100644 --- a/backend/modules/NosonApp/albumsmodel.cpp +++ b/backend/modules/NosonApp/albumsmodel.cpp @@ -54,7 +54,9 @@ AlbumsModel::AlbumsModel(QObject* parent) AlbumsModel::~AlbumsModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void AlbumsModel::addItem(AlbumItem* item) @@ -140,29 +142,32 @@ bool AlbumsModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void AlbumsModel::clear() +void AlbumsModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool AlbumsModel::load() +bool AlbumsModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -174,20 +179,52 @@ bool AlbumsModel::load() { AlbumItem* item = new AlbumItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } + if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool AlbumsModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void AlbumsModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (AlbumItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void AlbumsModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/albumsmodel.h b/backend/modules/NosonApp/albumsmodel.h index c681ce0b..1fc46a59 100644 --- a/backend/modules/NosonApp/albumsmodel.h +++ b/backend/modules/NosonApp/albumsmodel.h @@ -85,12 +85,16 @@ class AlbumsModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); - + + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -98,12 +102,14 @@ class AlbumsModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif // ALBUMSMODEL diff --git a/backend/modules/NosonApp/artistsmodel.cpp b/backend/modules/NosonApp/artistsmodel.cpp index 821f077b..a8a9dba4 100644 --- a/backend/modules/NosonApp/artistsmodel.cpp +++ b/backend/modules/NosonApp/artistsmodel.cpp @@ -51,7 +51,9 @@ ArtistsModel::ArtistsModel(QObject* parent) ArtistsModel::~ArtistsModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void ArtistsModel::addItem(ArtistItem* item) @@ -133,29 +135,32 @@ bool ArtistsModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void ArtistsModel::clear() +void ArtistsModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool ArtistsModel::load() +bool ArtistsModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -167,20 +172,51 @@ bool ArtistsModel::load() { ArtistItem* item = new ArtistItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool ArtistsModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void ArtistsModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (ArtistItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void ArtistsModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/artistsmodel.h b/backend/modules/NosonApp/artistsmodel.h index dd066122..f32b20f2 100644 --- a/backend/modules/NosonApp/artistsmodel.h +++ b/backend/modules/NosonApp/artistsmodel.h @@ -81,12 +81,16 @@ class ArtistsModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -94,12 +98,14 @@ class ArtistsModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* ARTISTSMODEL_H */ diff --git a/backend/modules/NosonApp/favoritesmodel.cpp b/backend/modules/NosonApp/favoritesmodel.cpp index e044cb43..1a2448f7 100644 --- a/backend/modules/NosonApp/favoritesmodel.cpp +++ b/backend/modules/NosonApp/favoritesmodel.cpp @@ -98,7 +98,9 @@ FavoritesModel::FavoritesModel(QObject* parent) FavoritesModel::~FavoritesModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void FavoritesModel::addItem(FavoriteItem* item) @@ -213,30 +215,32 @@ bool FavoritesModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void FavoritesModel::clear() +void FavoritesModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - m_objectIDs.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool FavoritesModel::load() +bool FavoritesModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -248,20 +252,54 @@ bool FavoritesModel::load() { FavoriteItem* item = new FavoriteItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool FavoritesModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void FavoritesModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + m_objectIDs.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (FavoriteItem* item, m_data) { + m_items << item; + m_objectIDs.insert(item->objectId(), item->id()); + } + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void FavoritesModel::handleDataUpdate() @@ -275,6 +313,8 @@ void FavoritesModel::handleDataUpdate() QString FavoritesModel::findFavorite(const QVariant& payload) const { + if (!m_provider) + return ""; SONOS::DigitalItemPtr ptr = payload.value(); SONOS::PlayerPtr player = m_provider->getPlayer(); if (ptr && player) diff --git a/backend/modules/NosonApp/favoritesmodel.h b/backend/modules/NosonApp/favoritesmodel.h index cfa5363e..61b8cc2d 100644 --- a/backend/modules/NosonApp/favoritesmodel.h +++ b/backend/modules/NosonApp/favoritesmodel.h @@ -31,16 +31,19 @@ class FavoriteType : public QObject Q_OBJECT Q_ENUMS(itemType) - public: - enum itemType - { - unknown = 0, - album = 1, - person = 2, - genre = 3, - playlist = 4, - audioItem = 5, - }; +public: + enum itemType + { + unknown = 0, + album = 1, + person = 2, + genre = 3, + playlist = 4, + audioItem = 5, + }; + + FavoriteType(QObject* parent = 0) + : QObject(parent) { } }; class FavoriteItem @@ -131,12 +134,16 @@ class FavoritesModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -146,12 +153,14 @@ class FavoritesModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; QMap m_objectIDs; }; diff --git a/backend/modules/NosonApp/genresmodel.cpp b/backend/modules/NosonApp/genresmodel.cpp index 9fe611f1..28831aca 100644 --- a/backend/modules/NosonApp/genresmodel.cpp +++ b/backend/modules/NosonApp/genresmodel.cpp @@ -51,7 +51,9 @@ GenresModel::GenresModel(QObject* parent) GenresModel::~GenresModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void GenresModel::addItem(GenreItem* item) @@ -129,29 +131,32 @@ bool GenresModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void GenresModel::clear() +void GenresModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool GenresModel::load() +bool GenresModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -163,20 +168,51 @@ bool GenresModel::load() { GenreItem* item = new GenreItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool GenresModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void GenresModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (GenreItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void GenresModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/genresmodel.h b/backend/modules/NosonApp/genresmodel.h index 6bb4330e..b7605776 100644 --- a/backend/modules/NosonApp/genresmodel.h +++ b/backend/modules/NosonApp/genresmodel.h @@ -77,12 +77,16 @@ class GenresModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -90,12 +94,14 @@ class GenresModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* GENRESMODEL_H */ diff --git a/backend/modules/NosonApp/listmodel.cpp b/backend/modules/NosonApp/listmodel.cpp index 0bbe0d89..9890d8f3 100644 --- a/backend/modules/NosonApp/listmodel.cpp +++ b/backend/modules/NosonApp/listmodel.cpp @@ -26,7 +26,7 @@ ListModel::ListModel() , m_provider(0) , m_updateID(0) , m_pending(false) -, m_loaded(false) +, m_dataState(ListModel::NoData) , m_updateSignaled(false) { m_lock = SONOS::LockGuard::CreateLock(); @@ -53,8 +53,8 @@ bool ListModel::init(QObject* sonos, const QString& root, bool fill /*= false*/) m_provider = _sonos; m_root = root; // Reset container status to allow async reload - m_loaded = false; + m_dataState = ListModel::NoData; if (fill) - return this->load(); + return this->loadData(); return false; // not filled } diff --git a/backend/modules/NosonApp/listmodel.h b/backend/modules/NosonApp/listmodel.h index d6d68ed4..43e5afc8 100644 --- a/backend/modules/NosonApp/listmodel.h +++ b/backend/modules/NosonApp/listmodel.h @@ -42,25 +42,33 @@ class ListModel ListModel(); virtual ~ListModel(); - virtual void clear() = 0; + virtual void clearData() = 0; - virtual bool load() = 0; + virtual bool loadData() = 0; virtual void handleDataUpdate() = 0; + enum dataState { + NoData = 0, + Loaded = 1, + Synced = 2 + }; + protected: SONOS::LockGuard::Lockable* m_lock; Sonos* m_provider; unsigned m_updateID; QString m_root; bool m_pending; - bool m_loaded; + dataState m_dataState; virtual bool init(QObject* sonos, const QString& root, bool fill = false); bool updateSignaled() { return m_updateSignaled.Load(); } void setUpdateSignaled(bool val) { m_updateSignaled.Store(val); } + virtual bool customizedLoad(int id) { return false; } + private: SONOS::Locked m_updateSignaled; }; diff --git a/backend/modules/NosonApp/mediamodel.cpp b/backend/modules/NosonApp/mediamodel.cpp index 3b2c726b..d7837ab5 100644 --- a/backend/modules/NosonApp/mediamodel.cpp +++ b/backend/modules/NosonApp/mediamodel.cpp @@ -111,7 +111,9 @@ MediaModel::MediaModel(QObject* parent) MediaModel::~MediaModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); SAFE_DELETE(m_smapi); } @@ -248,26 +250,25 @@ bool MediaModel::init(QObject* sonos, const QVariant& service, bool fill) return ListModel::init(sonos, "", fill); } -void MediaModel::clear() +void MediaModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool MediaModel::load() +bool MediaModel::loadData() { setUpdateSignaled(false); SONOS::LockGuard lock(m_lock); if (!m_smapi) + { + emit loaded(false); return false; + } - clear(); + clearData(); + m_dataState = ListModel::NoData; m_searching = false; // enable browse state m_nextIndex = m_totalCount = 0; SONOS::SMAPIMetadata meta; @@ -276,94 +277,32 @@ bool MediaModel::load() emit totalCountChanged(); if (m_smapi->AuthTokenExpired()) emit authStatusChanged(); + emit loaded(false); return false; } m_totalCount = meta.TotalCount(); m_nextIndex = meta.ItemCount(); - emit totalCountChanged(); SONOS::SMAPIItemList list = meta.GetItems(); for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) { MediaItem* item = new MediaItem(*it); if (item->isValid()) - addItem(item); - else - delete item; - } - return m_loaded = true; -} - -bool MediaModel::loadMore() -{ - SONOS::LockGuard lock(m_lock); - if (!m_smapi) - return false; - // At end return false - if (m_nextIndex >= m_totalCount) - return false; - - SONOS::SMAPIMetadata meta; - // browse or search for next items depending of current state - if ((!m_searching && !m_smapi->GetMetadata(pathId().toUtf8().constData(), m_nextIndex, LOAD_BULKSIZE, false, meta)) || - (m_searching && !m_smapi->Search(m_searchCategory, m_searchTerm, m_nextIndex, LOAD_BULKSIZE, meta))) - { - if (m_smapi->AuthTokenExpired()) - emit authStatusChanged(); - return false; - } - if (m_totalCount != meta.TotalCount()) - { - m_totalCount = meta.TotalCount(); - emit totalCountChanged(); - } - m_nextIndex += meta.ItemCount(); - - SONOS::SMAPIItemList list= meta.GetItems(); - for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) - { - MediaItem* item = new MediaItem(*it); - if (item->isValid()) - addItem(item); + m_data << item; else + { delete item; + // Also decrease total count + if (m_totalCount > 0) + --m_totalCount; + } } + emit totalCountChanged(); + m_dataState = ListModel::Loaded; + emit loaded(true); return true; } -bool MediaModel::loadChild(const QString& id, const QString& title, int displayType, int viewIndex /*= 0*/) -{ - if (id.isEmpty()) - return false; - SONOS::LockGuard lock(m_lock); - // save current view index for this path item - if (!m_path.empty()) - m_path.top().viewIndex = viewIndex; - m_path.push(Path(id, title, displayType)); - emit pathChanged(); - return load(); -} - -bool MediaModel::loadParent() -{ - SONOS::LockGuard lock(m_lock); - if (!m_path.empty()) - m_path.pop(); - // reload current search else the parent item - if (pathName() == SEARCH_TAG) - { - m_searching = true; // reset state before signal the change - emit pathChanged(); - return search(); - } - else - { - m_searching = false; // reset state before signal the change - emit pathChanged(); - return load(); - } -} - QString MediaModel::pathName() const { SONOS::LockGuard lock(m_lock); @@ -413,47 +352,6 @@ QList MediaModel::listSearchCategories() const return list; } -bool MediaModel::loadSearch(const QString &category, const QString &term) -{ - SONOS::LockGuard lock(m_lock); - m_searchCategory = category.toUtf8().constData(); - m_searchTerm = term.toUtf8().constData(); - m_searching = true; // enable search state - m_path.clear(); - m_path.push(Path("", SEARCH_TAG, ROOT_DISPLAY_TYPE)); - emit pathChanged(); - return search(); -} - -bool MediaModel::search() -{ - if (!m_smapi) - return false; - - SONOS::SMAPIMetadata meta; - if (!m_smapi->Search(m_searchCategory, m_searchTerm, 0, LOAD_BULKSIZE, meta)) - { - emit totalCountChanged(); - if (m_smapi->AuthTokenExpired()) - emit authStatusChanged(); - return false; - } - clear(); - m_totalCount = meta.TotalCount(); - m_nextIndex = meta.ItemCount(); - emit totalCountChanged(); - SONOS::SMAPIItemList list = meta.GetItems(); - for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) - { - MediaItem* item = new MediaItem(*it); - if (item->isValid()) - addItem(item); - else - delete item; - } - return m_loaded = true; -} - bool MediaModel::isAuthExpired() const { return (m_smapi ? m_smapi->AuthTokenExpired() : false); @@ -525,7 +423,254 @@ MediaAuth* MediaModel::getDeviceAuth() bool MediaModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +bool MediaModel::loadMoreData() +{ + SONOS::LockGuard lock(m_lock); + if (!m_smapi) + { + emit loadedMore(false); + return false; + } + // At end return false + if (m_nextIndex >= m_totalCount) + { + emit loadedMore(false); + return false; + } + + SONOS::SMAPIMetadata meta; + // browse or search for next items depending of current state + if ((!m_searching && !m_smapi->GetMetadata(pathId().toUtf8().constData(), m_nextIndex, LOAD_BULKSIZE, false, meta)) || + (m_searching && !m_smapi->Search(m_searchCategory, m_searchTerm, m_nextIndex, LOAD_BULKSIZE, meta))) + { + if (m_smapi->AuthTokenExpired()) + emit authStatusChanged(); + emit loaded(false); + return false; + } + if (m_totalCount != meta.TotalCount()) + { + m_totalCount = meta.TotalCount(); + emit totalCountChanged(); + } + m_nextIndex += meta.ItemCount(); + + SONOS::SMAPIItemList list= meta.GetItems(); + for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) + { + MediaItem* item = new MediaItem(*it); + if (item->isValid()) + m_data << item; + else + { + delete item; + // Also decrease total count + if (m_totalCount) { + --m_totalCount; + emit totalCountChanged(); + } + } + } + m_dataState = ListModel::Loaded; + emit loadedMore(true); + return true; +} + +bool MediaModel::asyncLoadMore() +{ + if (!m_provider) + return false; + m_provider->runCustomizedModelLoader(this, 1); + return true; +} + +bool MediaModel::loadChild(const QString& id, const QString& title, int displayType, int viewIndex /*= 0*/) +{ + if (id.isEmpty()) + return false; + SONOS::LockGuard lock(m_lock); + // save current view index for this path item + if (!m_path.empty()) + m_path.top().viewIndex = viewIndex; + m_path.push(Path(id, title, displayType)); + emit pathChanged(); + return loadData(); +} + +bool MediaModel::asyncLoadChild(const QString &id, const QString &title, int displayType, int viewIndex /*= 0*/) +{ + if (id.isEmpty()) + return false; + { + SONOS::LockGuard lock(m_lock); + // save current view index for this path item + if (!m_path.empty()) + m_path.top().viewIndex = viewIndex; + m_path.push(Path(id, title, displayType)); + emit pathChanged(); + } + return asyncLoad(); +} + +bool MediaModel::loadParent() +{ + SONOS::LockGuard lock(m_lock); + if (!m_path.empty()) + m_path.pop(); + // reload current search else the parent item + if (pathName() == SEARCH_TAG) + { + m_searching = true; // reset state before signal the change + emit pathChanged(); + return search(); + } + else + { + m_searching = false; // reset state before signal the change + emit pathChanged(); + return loadData(); + } +} + +bool MediaModel::asyncLoadParent() +{ + if (!m_provider) + return false; + m_provider->runCustomizedModelLoader(this, 2); + return true; +} + +bool MediaModel::loadSearch(const QString &category, const QString &term) +{ + SONOS::LockGuard lock(m_lock); + m_searchCategory = category.toUtf8().constData(); + m_searchTerm = term.toUtf8().constData(); + m_searching = true; // enable search state + m_path.clear(); + m_path.push(Path("", SEARCH_TAG, ROOT_DISPLAY_TYPE)); + emit pathChanged(); + return search(); +} + +bool MediaModel::asyncLoadSearch(const QString &category, const QString &term) +{ + { + SONOS::LockGuard lock(m_lock); + m_searchCategory = category.toUtf8().constData(); + m_searchTerm = term.toUtf8().constData(); + m_searching = true; // enable search state + m_path.clear(); + m_path.push(Path("", SEARCH_TAG, ROOT_DISPLAY_TYPE)); + emit pathChanged(); + } + if (!m_provider) + return false; + m_provider->runCustomizedModelLoader(this, 3); + return true; +} + +bool MediaModel::search() +{ + if (!m_smapi) + { + emit loaded(false); + return false; + } + + SONOS::SMAPIMetadata meta; + if (!m_smapi->Search(m_searchCategory, m_searchTerm, 0, LOAD_BULKSIZE, meta)) + { + emit totalCountChanged(); + if (m_smapi->AuthTokenExpired()) + emit authStatusChanged(); + emit loaded(false); + return false; + } + clearData(); + m_dataState = ListModel::NoData; + m_totalCount = meta.TotalCount(); + m_nextIndex = meta.ItemCount(); + SONOS::SMAPIItemList list = meta.GetItems(); + for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) + { + MediaItem* item = new MediaItem(*it); + if (item->isValid()) + m_data << item; + else + { + delete item; + // Also decrease total count + if (m_totalCount > 0) + --m_totalCount; + } + } + emit totalCountChanged(); + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; +} + +void MediaModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (MediaItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); +} + +void MediaModel::appendModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + int cnt = m_items.count(); + beginInsertRows(QModelIndex(), cnt, cnt + m_data.count()-1); + foreach (MediaItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + } + emit countChanged(); +} + +bool MediaModel::customizedLoad(int id) +{ + switch (id) + { + case 0: + return loadData(); + case 1: + return loadMoreData(); + case 2: + return loadParent(); + case 3: + return search(); + default: + return false; + } } void MediaModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/mediamodel.h b/backend/modules/NosonApp/mediamodel.h index 325ea775..44696cb0 100644 --- a/backend/modules/NosonApp/mediamodel.h +++ b/backend/modules/NosonApp/mediamodel.h @@ -33,17 +33,20 @@ class MediaType : public QObject Q_OBJECT Q_ENUMS(itemType) - public: - enum itemType - { - unknown = 0, - album = 1, - person = 2, - genre = 3, - playlist = 4, - audioItem = 5, - folder = 6, - }; +public: + enum itemType + { + unknown = 0, + album = 1, + person = 2, + genre = 3, + playlist = 4, + audioItem = 5, + folder = 6, + }; + + MediaType(QObject* parent) + : QObject(parent) {} }; class MediaItem @@ -172,20 +175,14 @@ class MediaModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QVariant& service, bool fill = false); - Q_INVOKABLE void clear(); + Q_INVOKABLE void clearData(); - Q_INVOKABLE bool load(); + Q_INVOKABLE bool loadData(); int totalCount() const { return m_totalCount; } bool isRoot() const { return (m_path.empty()); } - Q_INVOKABLE bool loadMore(); - - Q_INVOKABLE bool loadChild(const QString& id, const QString& title, int displayType, int viewIndex = 0); - - Q_INVOKABLE bool loadParent(); - Q_INVOKABLE QString pathName() const; Q_INVOKABLE QString pathId() const; @@ -196,8 +193,6 @@ class MediaModel : public QAbstractListModel, public ListModel Q_INVOKABLE QList listSearchCategories() const; - Q_INVOKABLE bool loadSearch(const QString& category, const QString& term); - bool isAuthExpired() const; int policyAuth() const; @@ -218,6 +213,28 @@ class MediaModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool asyncLoad(); + virtual bool loadMoreData(); + + Q_INVOKABLE bool asyncLoadMore(); + + virtual bool loadChild(const QString& id, const QString& title, int displayType, int viewIndex = 0); + + Q_INVOKABLE bool asyncLoadChild(const QString& id, const QString& title, int displayType, int viewIndex = 0); + + virtual bool loadParent(); + + Q_INVOKABLE bool asyncLoadParent(); + + virtual bool loadSearch(const QString& category, const QString& term); + + Q_INVOKABLE bool asyncLoadSearch(const QString& category, const QString& term); + + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel(); + + virtual bool customizedLoad(int id); + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -228,12 +245,15 @@ class MediaModel : public QAbstractListModel, public ListModel void totalCountChanged(); void pathChanged(); void authStatusChanged(); + void loaded(bool succeeded); + void loadedMore(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; SONOS::SMAPI* m_smapi; SONOS::SMOAKeyring::Credentials m_auth; diff --git a/backend/modules/NosonApp/player.cpp b/backend/modules/NosonApp/player.cpp index a9176b44..e8f1c6e7 100644 --- a/backend/modules/NosonApp/player.cpp +++ b/backend/modules/NosonApp/player.cpp @@ -72,6 +72,16 @@ bool Player::init(QObject* sonos) return false; } +void Player::beginJob() +{ + m_sonos->beginJob(); +} + +void Player::endJob() +{ + m_sonos->endJob(); +} + void Player::renewSubscriptions() { if (m_player) @@ -111,6 +121,31 @@ int Player::remainingSleepTimerDuration() return 0; } +class playSourceWorker : public SONOS::OS::CWorker +{ +public: + playSourceWorker(Player& player, const QVariant& payload) + : m_player(player) + , m_payload(payload) + { } + + virtual void Process() + { + m_player.beginJob(); + if (!m_player.setSource(m_payload) || !m_player.play()) + emit m_player.jobFailed(); + m_player.endJob(); + } +private: + Player& m_player; + QVariant m_payload; +}; + +bool Player::startPlaySource(const QVariant& payload) +{ + return m_sonos->startJob(new playSourceWorker(*this, payload)); +} + bool Player::play() { return m_player ? m_player->Play() : false; @@ -217,6 +252,33 @@ bool Player::toggleMute(const QString& uuid) return false; } +class playStreamWorker : public SONOS::OS::CWorker +{ +public: + playStreamWorker(Player& player, const QString& url, const QString& title) + : m_player(player) + , m_url(url) + , m_title(title) + { } + + virtual void Process() + { + m_player.beginJob(); + if (!m_player.playStream(m_url, m_title)) + emit m_player.jobFailed(); + m_player.endJob(); + } +private: + Player& m_player; + const QString m_url; + const QString m_title; +}; + +bool Player::startPlayStream(const QString& url, const QString& title) +{ + return m_sonos->startJob(new playStreamWorker(*this, url, title)); +} + bool Player::playStream(const QString& url, const QString& title) { if (m_player) @@ -341,6 +403,31 @@ bool Player::destroyFavorite(const QString& FVid) return m_player ? m_player->DestroyFavorite(FVid.toUtf8().constData()) : false; } +class playFavoriteWorker : public SONOS::OS::CWorker +{ +public: + playFavoriteWorker(Player& player, const QVariant& payload) + : m_player(player) + , m_payload(payload) + { } + + virtual void Process() + { + m_player.beginJob(); + if (!m_player.playFavorite(m_payload)) + emit m_player.jobFailed(); + m_player.endJob(); + } +private: + Player& m_player; + QVariant m_payload; +}; + +bool Player::startPlayFavorite(const QVariant& payload) +{ + return m_sonos->startJob(new playFavoriteWorker(*this, payload)); +} + bool Player::playFavorite(const QVariant& payload) { SONOS::DigitalItemPtr favorite(payload.value()); diff --git a/backend/modules/NosonApp/player.h b/backend/modules/NosonApp/player.h index 1650549a..069246fa 100644 --- a/backend/modules/NosonApp/player.h +++ b/backend/modules/NosonApp/player.h @@ -21,6 +21,7 @@ #ifndef PLAYER_H #define PLAYER_H +#include "../../lib/noson/noson/src/private/os/threads/threadpool.h" #include "../../lib/noson/noson/src/sonosplayer.h" #include @@ -53,6 +54,8 @@ class Player : public QObject Q_INVOKABLE bool init(QObject* sonos); bool connected() const { return m_connected; } + void beginJob(); + void endJob(); Q_INVOKABLE void renewSubscriptions(); Q_INVOKABLE bool ping(); @@ -60,6 +63,7 @@ class Player : public QObject Q_INVOKABLE bool configureSleepTimer(int seconds); Q_INVOKABLE int remainingSleepTimerDuration(); + Q_INVOKABLE bool startPlaySource(const QVariant& payload); // asynchronous Q_INVOKABLE bool play(); Q_INVOKABLE bool stop(); Q_INVOKABLE bool pause(); @@ -71,6 +75,7 @@ class Player : public QObject Q_INVOKABLE bool toggleMute(); Q_INVOKABLE bool toggleMute(const QString& uuid); + Q_INVOKABLE bool startPlayStream(const QString& url, const QString& title); // asynchonous Q_INVOKABLE bool playStream(const QString& url, const QString& title); Q_INVOKABLE bool playLineIN(); Q_INVOKABLE bool playDigitalIN(); @@ -92,6 +97,7 @@ class Player : public QObject Q_INVOKABLE bool addItemToFavorites(const QVariant& payload, const QString& description, const QString& artURI); Q_INVOKABLE bool destroyFavorite(const QString& FVid); + Q_INVOKABLE bool startPlayFavorite(const QVariant& payload); // asynchronous Q_INVOKABLE bool playFavorite(const QVariant& payload); bool muteMaster() const { return m_RCGroup.mute; } @@ -132,6 +138,7 @@ class Player : public QObject QString playMode() const { return QString::fromUtf8(m_AVTProperty.CurrentPlayMode.c_str()); } signals: + void jobFailed(); void connectedChanged(); void renderingChanged(); void renderingGroupChanged(); diff --git a/backend/modules/NosonApp/playlistsmodel.cpp b/backend/modules/NosonApp/playlistsmodel.cpp index 1b378ece..9a0fe22a 100644 --- a/backend/modules/NosonApp/playlistsmodel.cpp +++ b/backend/modules/NosonApp/playlistsmodel.cpp @@ -56,7 +56,9 @@ PlaylistsModel::PlaylistsModel(QObject* parent) PlaylistsModel::~PlaylistsModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void PlaylistsModel::addItem(PlaylistItem* item) @@ -142,29 +144,32 @@ bool PlaylistsModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void PlaylistsModel::clear() +void PlaylistsModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool PlaylistsModel::load() +bool PlaylistsModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -176,14 +181,19 @@ bool PlaylistsModel::load() { PlaylistItem* item = new PlaylistItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } void PlaylistsModel::handleDataUpdate() @@ -195,8 +205,34 @@ void PlaylistsModel::handleDataUpdate() } } +void PlaylistsModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (PlaylistItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); +} + bool PlaylistsModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; } diff --git a/backend/modules/NosonApp/playlistsmodel.h b/backend/modules/NosonApp/playlistsmodel.h index 416738d2..2fa4b395 100644 --- a/backend/modules/NosonApp/playlistsmodel.h +++ b/backend/modules/NosonApp/playlistsmodel.h @@ -86,12 +86,16 @@ class PlaylistsModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -99,12 +103,14 @@ class PlaylistsModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; diff --git a/backend/modules/NosonApp/queuemodel.cpp b/backend/modules/NosonApp/queuemodel.cpp index 39f67dd6..1499b0a3 100644 --- a/backend/modules/NosonApp/queuemodel.cpp +++ b/backend/modules/NosonApp/queuemodel.cpp @@ -29,7 +29,9 @@ QueueModel::QueueModel(QObject* parent) QueueModel::~QueueModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void QueueModel::addItem(TrackItem* item) @@ -123,29 +125,32 @@ bool QueueModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void QueueModel::clear() +void QueueModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool QueueModel::load() +bool QueueModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -156,18 +161,49 @@ bool QueueModel::load() for (SONOS::ContentList::iterator it = cl.begin(); it != cl.end(); ++it) { TrackItem* item = new TrackItem(*it, url); - addItem(item); + m_data << item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool QueueModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void QueueModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (TrackItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void QueueModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/queuemodel.h b/backend/modules/NosonApp/queuemodel.h index 68b725c2..9385fa23 100644 --- a/backend/modules/NosonApp/queuemodel.h +++ b/backend/modules/NosonApp/queuemodel.h @@ -55,12 +55,16 @@ class QueueModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -68,12 +72,14 @@ class QueueModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* QUEUEMODEL_H */ diff --git a/backend/modules/NosonApp/radiosmodel.cpp b/backend/modules/NosonApp/radiosmodel.cpp index d9f3f76e..be4797ba 100644 --- a/backend/modules/NosonApp/radiosmodel.cpp +++ b/backend/modules/NosonApp/radiosmodel.cpp @@ -68,7 +68,9 @@ RadiosModel::RadiosModel(QObject* parent) RadiosModel::~RadiosModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void RadiosModel::addItem(RadioItem* item) @@ -158,29 +160,32 @@ bool RadiosModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void RadiosModel::clear() +void RadiosModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool RadiosModel::load() +bool RadiosModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -192,20 +197,51 @@ bool RadiosModel::load() { RadioItem* item = new RadioItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool RadiosModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void RadiosModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (RadioItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void RadiosModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/radiosmodel.h b/backend/modules/NosonApp/radiosmodel.h index 9b111cd4..83c2882c 100644 --- a/backend/modules/NosonApp/radiosmodel.h +++ b/backend/modules/NosonApp/radiosmodel.h @@ -89,12 +89,16 @@ class RadiosModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -102,12 +106,14 @@ class RadiosModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* RADIOSMODEL_H */ diff --git a/backend/modules/NosonApp/renderingmodel.cpp b/backend/modules/NosonApp/renderingmodel.cpp index 44c8b0db..f43d9c8d 100644 --- a/backend/modules/NosonApp/renderingmodel.cpp +++ b/backend/modules/NosonApp/renderingmodel.cpp @@ -37,7 +37,9 @@ RenderingModel::RenderingModel(QObject* parent) RenderingModel::~RenderingModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void RenderingModel::addItem(RenderingItem* item) @@ -106,28 +108,49 @@ QHash RenderingModel::roleNames() const return roles; } -void RenderingModel::clear() +void RenderingModel::clearData() { - beginRemoveRows(QModelIndex(), 0, m_items.count()); qDeleteAll(m_items); m_items.clear(); - endRemoveRows(); - emit countChanged(); } -bool RenderingModel::load(QObject* player) +bool RenderingModel::loadData() { - Player* _player = reinterpret_cast (player); - if (!_player) + if (!m_player) return false; - clear(); - const Player::RCTable& tab = _player->renderingTable(); + clearData(); + const Player::RCTable& tab = m_player->renderingTable(); for (Player::RCTable::const_iterator it = tab.begin(); it != tab.end(); ++it) - addItem(new RenderingItem(*it)); + m_data << new RenderingItem(*it); return true; } +bool RenderingModel::load(QObject* player) +{ + m_player = reinterpret_cast (player); + if (!loadData()) + return false; + resetModel(); + return true; +} + +void RenderingModel::resetModel() +{ + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (RenderingItem* item, m_data) + m_items << item; + m_data.clear(); + endInsertRows(); + endResetModel(); + emit countChanged(); +} + void RenderingModel::setVolume(int index, const QVariant& volume) { setData(QAbstractListModel::index(index), volume, VolumeRole); diff --git a/backend/modules/NosonApp/renderingmodel.h b/backend/modules/NosonApp/renderingmodel.h index da679fab..90c4c6f0 100644 --- a/backend/modules/NosonApp/renderingmodel.h +++ b/backend/modules/NosonApp/renderingmodel.h @@ -76,10 +76,14 @@ class RenderingModel : public QAbstractListModel bool setData(const QModelIndex& index, const QVariant& value, int role); - Q_INVOKABLE void clear(); + virtual void clearData(); + + virtual bool loadData(); Q_INVOKABLE bool load(QObject* player); + virtual void resetModel(); + Q_INVOKABLE void setVolume(int index, const QVariant& volume); Q_INVOKABLE void setMute(int index, const QVariant& mute); @@ -92,6 +96,8 @@ class RenderingModel : public QAbstractListModel private: QList m_items; + QList m_data; + Player* m_player; }; #endif /* RENDERINGMODEL_H */ diff --git a/backend/modules/NosonApp/roomsmodel.cpp b/backend/modules/NosonApp/roomsmodel.cpp index bc6061a6..369b8e6c 100644 --- a/backend/modules/NosonApp/roomsmodel.cpp +++ b/backend/modules/NosonApp/roomsmodel.cpp @@ -42,23 +42,15 @@ QVariant RoomItem::payload() const RoomsModel::RoomsModel(QObject* parent) : QAbstractListModel(parent) +, m_zoneId("") { } RoomsModel::~RoomsModel() { - clear(); -} - -void RoomsModel::addItem(RoomItem* item) -{ - { - SONOS::LockGuard lock(m_lock); - beginInsertRows(QModelIndex(), rowCount(), rowCount()); - m_items << item; - endInsertRows(); - } - emit countChanged(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } int RoomsModel::rowCount(const QModelIndex& parent) const @@ -103,7 +95,6 @@ QHash RoomsModel::roleNames() const QVariantMap RoomsModel::get(int row) { - SONOS::LockGuard lock(m_lock); if (row < 0 || row >= m_items.count()) return QVariantMap(); const RoomItem* item = m_items[row]; @@ -117,71 +108,82 @@ QVariantMap RoomsModel::get(int row) return model; } -bool RoomsModel::init(QObject* sonos, bool fill) -{ - return ListModel::init(sonos, "", fill); -} - -void RoomsModel::clear() +void RoomsModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + qDeleteAll(m_data); + m_data.clear(); } -bool RoomsModel::load() +bool RoomsModel::loadData() { - setUpdateSignaled(false); - if (!m_provider) return false; - clear(); - SONOS::ZonePlayerList zonePlayers = m_provider->getSystem().GetZonePlayerList(); - for (SONOS::ZonePlayerList::iterator it = zonePlayers.begin(); it != zonePlayers.end(); ++it) - { - RoomItem* item = new RoomItem(it->second); - if (item->isValid()) - addItem(item); - else - delete item; - } - return m_loaded = true; -} - -bool RoomsModel::load(const QString& zoneId) -{ - setUpdateSignaled(false); + clearData(); - if (!m_provider) - return false; - clear(); - SONOS::ZoneList zones = m_provider->getSystem().GetZoneList(); - SONOS::ZoneList::const_iterator itz = zones.find(zoneId.toUtf8().constData()); - if (itz != zones.end()) + if (m_zoneId.isNull()) { - for (std::vector::iterator it = itz->second->begin(); it != itz->second->end(); ++it) + SONOS::ZonePlayerList zonePlayers = m_provider->getSystem().GetZonePlayerList(); + for (SONOS::ZonePlayerList::iterator it = zonePlayers.begin(); it != zonePlayers.end(); ++it) { - RoomItem* item = new RoomItem(*it); + RoomItem* item = new RoomItem(it->second); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } } - return m_loaded = true; + else + { + SONOS::ZoneList zones = m_provider->getSystem().GetZoneList(); + SONOS::ZoneList::const_iterator itz = zones.find(m_zoneId.toUtf8().constData()); + if (itz != zones.end()) + { + for (std::vector::iterator it = itz->second->begin(); it != itz->second->end(); ++it) + { + RoomItem* item = new RoomItem(*it); + if (item->isValid()) + m_data << item; + else + delete item; + } + } + } + return true; } -void RoomsModel::handleDataUpdate() +bool RoomsModel::load(QObject* sonos) { - if (!updateSignaled()) - { - setUpdateSignaled(true); - dataUpdated(); - } + m_provider = reinterpret_cast (sonos); + m_zoneId = QString::null; + if (!loadData()) + return false; + resetModel(); + return true; +} + +bool RoomsModel::load(QObject* sonos, const QString& zoneId) +{ + m_provider = reinterpret_cast (sonos); + m_zoneId = zoneId; + if (!loadData()) + return false; + resetModel(); + return true; +} + +void RoomsModel::resetModel() +{ + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (RoomItem* item, m_data) + m_items << item; + m_data.clear(); + endInsertRows(); + endResetModel(); + emit countChanged(); } diff --git a/backend/modules/NosonApp/roomsmodel.h b/backend/modules/NosonApp/roomsmodel.h index ab0e872c..275c3cca 100644 --- a/backend/modules/NosonApp/roomsmodel.h +++ b/backend/modules/NosonApp/roomsmodel.h @@ -55,7 +55,7 @@ class RoomItem }; -class RoomsModel : public QAbstractListModel, public ListModel +class RoomsModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int count READ rowCount NOTIFY countChanged) @@ -81,18 +81,17 @@ class RoomsModel : public QAbstractListModel, public ListModel Q_INVOKABLE QVariantMap get(int row); - Q_INVOKABLE bool init(QObject* sonos, bool fill = false); + virtual void clearData(); - Q_INVOKABLE void clear(); + virtual bool loadData(); - Q_INVOKABLE bool load(); + Q_INVOKABLE bool load(QObject* sonos); - Q_INVOKABLE bool load(const QString& zoneId); + Q_INVOKABLE bool load(QObject* sonos, const QString& zoneId); - virtual void handleDataUpdate(); + virtual void resetModel(); signals: - void dataUpdated(); void countChanged(); protected: @@ -100,6 +99,9 @@ class RoomsModel : public QAbstractListModel, public ListModel private: QList m_items; + QList m_data; + Sonos* m_provider; + QString m_zoneId; }; #endif /* ROOMSMODEL_H */ diff --git a/backend/modules/NosonApp/servicesmodel.cpp b/backend/modules/NosonApp/servicesmodel.cpp index fe49d739..09177cbf 100644 --- a/backend/modules/NosonApp/servicesmodel.cpp +++ b/backend/modules/NosonApp/servicesmodel.cpp @@ -51,7 +51,9 @@ ServicesModel::ServicesModel(QObject* parent) ServicesModel::~ServicesModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void ServicesModel::addItem(ServiceItem* item) @@ -132,44 +134,76 @@ bool ServicesModel::init(QObject* sonos, bool fill) return ListModel::init(sonos, "", fill); } -void ServicesModel::clear() +void ServicesModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool ServicesModel::load() +bool ServicesModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; SONOS::SMServiceList list = player->GetAvailableServices(); for (SONOS::SMServiceList::const_iterator it = list.begin(); it != list.end(); ++it) { ServiceItem* item = new ServiceItem(*it); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool ServicesModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void ServicesModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (ServiceItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void ServicesModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/servicesmodel.h b/backend/modules/NosonApp/servicesmodel.h index 722e7ea5..fd7d37c7 100644 --- a/backend/modules/NosonApp/servicesmodel.h +++ b/backend/modules/NosonApp/servicesmodel.h @@ -86,23 +86,27 @@ class ServicesModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + virtual void handleDataUpdate(); signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* SERVICESMODEL_H */ diff --git a/backend/modules/NosonApp/sonos.cpp b/backend/modules/NosonApp/sonos.cpp index 445d738b..10c45c3e 100644 --- a/backend/modules/NosonApp/sonos.cpp +++ b/backend/modules/NosonApp/sonos.cpp @@ -25,6 +25,8 @@ #include +#define JOB_THREADPOOL_SIZE 16 + class ContentLoader : public SONOS::OS::CWorker { public: @@ -36,14 +38,37 @@ class ContentLoader : public SONOS::OS::CWorker virtual void Process() { + m_sonos.beginJob(); if (m_payload) m_sonos.loadModel(m_payload); else m_sonos.loadEmptyModels(); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + ListModel* m_payload; +}; + +class CustomizedContentLoader : public SONOS::OS::CWorker +{ +public: + CustomizedContentLoader(Sonos& sonos, ListModel* payload, int id) + : m_sonos(sonos) + , m_payload(payload) + , m_id(id) { } + + virtual void Process() + { + m_sonos.beginJob(); + if (m_payload) + m_sonos.customizedLoadModel(m_payload, m_id); + m_sonos.endJob(); } private: Sonos& m_sonos; ListModel* m_payload; + int m_id; }; Sonos::Sonos(QObject* parent) @@ -51,7 +76,8 @@ Sonos::Sonos(QObject* parent) , m_library(ManagedContents()) , m_shareUpdateID(0) , m_system(this, topologyEventCB) -, m_threadpool(5) +, m_threadpool(JOB_THREADPOOL_SIZE) +, m_jobCount(SONOS::LockedNumber(0)) , m_locale("en_US") { SONOS::DBGLevel(2); @@ -66,6 +92,27 @@ Sonos::~Sonos() } } +class InitWorker : public SONOS::OS::CWorker +{ +public: + InitWorker(Sonos& sonos, int debug) : m_sonos(sonos), m_debug(debug) { } + + virtual void Process() + { + m_sonos.beginJob(); + emit m_sonos.initDone(m_sonos.init(m_debug)); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + int m_debug; +}; + +bool Sonos::startInit(int debug) +{ + return m_threadpool.Enqueue(new InitWorker(*this, debug)); +} + bool Sonos::init(int debug) { SONOS::DBGLevel(debug > DBG_INFO ? debug : DBG_INFO); @@ -157,6 +204,55 @@ bool Sonos::joinZone(const QVariant& zonePayload, const QVariant& toZonePayload) } +bool Sonos::joinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload) +{ + std::vector zones; + SONOS::ZonePtr toZone = toZonePayload.value(); + for (QVariantList::const_iterator it = zonePayloads.begin(); it != zonePayloads.end(); ++it) + zones.push_back(it->value()); + if (toZone && toZone->GetCoordinator()) + { + for (std::vector::const_iterator it = zones.begin(); it != zones.end(); ++it) + { + if ((*it)->GetZoneName() == toZone->GetZoneName()) + continue; + for (std::vector::iterator itr = (*it)->begin(); itr != (*it)->end(); ++itr) + { + SONOS::Player player(*itr); + player.JoinToGroup(toZone->GetCoordinator()->GetUUID()); + } + } + return true; + } + return false; +} + +class JoinZonesWorker : public SONOS::OS::CWorker +{ +public: + JoinZonesWorker(Sonos& sonos, const QVariantList& zonePayloads, const QVariant& toZonePayload) + : m_sonos(sonos) + , m_zonePayloads(zonePayloads) + , m_toZonePayload(toZonePayload) + { } + + virtual void Process() + { + m_sonos.beginJob(); + m_sonos.joinZones(m_zonePayloads, m_toZonePayload); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + QVariantList m_zonePayloads; + QVariant m_toZonePayload; +}; + +bool Sonos::startJoinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload) +{ + return m_threadpool.Enqueue(new JoinZonesWorker(*this, zonePayloads, toZonePayload)); +} + bool Sonos::unjoinRoom(const QVariant& roomPayload) { SONOS::ZonePlayerPtr room = roomPayload.value(); @@ -168,6 +264,45 @@ bool Sonos::unjoinRoom(const QVariant& roomPayload) return false; } +bool Sonos::unjoinRooms(const QVariantList& roomPayloads) +{ + for (QVariantList::const_iterator it = roomPayloads.begin(); it != roomPayloads.end(); ++it) { + SONOS::ZonePlayerPtr room = it->value(); + if (room && room->IsValid()) + { + SONOS::Player player(room); + return player.BecomeStandalone(); + } + else + return false; + } + return true; +} + +class UnjoinRoomsWorker : public SONOS::OS::CWorker +{ +public: + UnjoinRoomsWorker(Sonos& sonos, const QVariantList& roomPayloads) + : m_sonos(sonos) + , m_roomPayloads(roomPayloads) + { } + + virtual void Process() + { + m_sonos.beginJob(); + m_sonos.unjoinRooms(m_roomPayloads); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + QVariantList m_roomPayloads; +}; + +bool Sonos::startUnjoinRooms(const QVariantList& roomPayloads) +{ + return m_threadpool.Enqueue(new UnjoinRoomsWorker(*this, roomPayloads)); +} + bool Sonos::unjoinZone(const QVariant& zonePayload) { SONOS::ZonePtr zone = zonePayload.value(); @@ -184,7 +319,31 @@ bool Sonos::unjoinZone(const QVariant& zonePayload) } -const SONOS::System& Sonos::getSystem() const +class UnjoinZoneWorker : public SONOS::OS::CWorker +{ +public: + UnjoinZoneWorker(Sonos& sonos, const QVariant& zonePayload) + : m_sonos(sonos) + , m_zonePayload(zonePayload) + { } + + virtual void Process() + { + m_sonos.beginJob(); + m_sonos.unjoinZone(m_zonePayload); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + QVariant m_zonePayload; +}; + +bool Sonos::startUnjoinZone(const QVariant& zonePayload) +{ + return m_threadpool.Enqueue(new UnjoinZoneWorker(*this, zonePayload)); +} + +const SONOS::System &Sonos::getSystem() const { return m_system; } @@ -205,7 +364,7 @@ void Sonos::loadEmptyModels() { SONOS::Locked::pointer mc = m_library.Get(); for (ManagedContents::iterator it = mc->begin(); it != mc->end(); ++it) - if (!it->model->m_loaded) + if (it->model->m_dataState == ListModel::NoData) left.push_back(qMakePair(it->model, SONOS::LockGuard(it->model->m_lock))); } emit loadingStarted(); @@ -214,7 +373,7 @@ void Sonos::loadEmptyModels() while (!left.isEmpty()) { QPair item = left.front(); - item.first->load(); + item.first->loadData(); left.pop_front(); } } @@ -228,6 +387,8 @@ void Sonos::runModelLoader(ListModel* model) model->m_pending = true; // decline next request m_threadpool.Enqueue(new ContentLoader(*this, model)); } + else + SONOS::DBG(DBG_ERROR, "%s: request has been declined (%p)\n", __FUNCTION__, model); } void Sonos::loadModel(ListModel* model) @@ -248,11 +409,29 @@ void Sonos::loadModel(ListModel* model) SONOS::DBG(DBG_INFO, "%s: %p (%s)\n", __FUNCTION__, item.first, item.first->m_root.toUtf8().constData()); emit loadingStarted(); item.first->m_pending = false; // accept add next request in queue - item.first->load(); + item.first->loadData(); emit loadingFinished(); } } +void Sonos::runCustomizedModelLoader(ListModel* model, int id) +{ + if (model && !model->m_pending) + { + model->m_pending = true; // decline next request + m_threadpool.Enqueue(new CustomizedContentLoader(*this, model, id)); + } + else + SONOS::DBG(DBG_ERROR, "%s: request id %d has been declined (%p)\n", __FUNCTION__, id, model); +} + +void Sonos::customizedLoadModel(ListModel *model, int id) +{ + SONOS::LockGuard guard(model->m_lock); + model->m_pending = false; // accept add next request in queue + model->customizedLoad(id); +} + void Sonos::registerModel(ListModel* model, const QString& root) { if (model) @@ -291,6 +470,23 @@ void Sonos::unregisterModel(ListModel* model) } } +bool Sonos::startJob(SONOS::OS::CWorker* worker) +{ + return m_threadpool.Enqueue(worker); +} + +void Sonos::beginJob() +{ + m_jobCount.Add(1); + emit jobCountChanged(); +} + +void Sonos::endJob() +{ + m_jobCount.Add(-1); + emit jobCountChanged(); +} + void Sonos::playerEventCB(void* handle) { Sonos* sonos = static_cast(handle); diff --git a/backend/modules/NosonApp/sonos.h b/backend/modules/NosonApp/sonos.h index aa6c59da..8b7c7138 100644 --- a/backend/modules/NosonApp/sonos.h +++ b/backend/modules/NosonApp/sonos.h @@ -46,11 +46,13 @@ class Sonos : public QObject { Q_OBJECT + Q_PROPERTY(int jobCount READ jobCount NOTIFY jobCountChanged) public: explicit Sonos(QObject *parent = 0); ~Sonos(); + Q_INVOKABLE bool startInit(int debug = 0); // asynchronous Q_INVOKABLE bool init(int debug = 0); Q_INVOKABLE void setLocale(const QString& locale); @@ -72,10 +74,15 @@ class Sonos : public QObject Q_INVOKABLE bool joinRoom(const QVariant& roomPayload, const QVariant& toZonePayload); Q_INVOKABLE bool joinZone(const QVariant& zonePayload, const QVariant& toZonePayload); + Q_INVOKABLE bool joinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload); + Q_INVOKABLE bool startJoinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload); Q_INVOKABLE bool unjoinRoom(const QVariant& roomPayload); + Q_INVOKABLE bool unjoinRooms(const QVariantList& roomPayloads); + Q_INVOKABLE bool startUnjoinRooms(const QVariantList& roomPayloads); Q_INVOKABLE bool unjoinZone(const QVariant& zonePayload); + Q_INVOKABLE bool startUnjoinZone(const QVariant& zonePayload); const SONOS::System& getSystem() const; const SONOS::PlayerPtr& getPlayer() const; @@ -86,9 +93,17 @@ class Sonos : public QObject void runModelLoader(ListModel* model); void loadModel(ListModel* model); + void runCustomizedModelLoader(ListModel* model, int id); + void customizedLoadModel(ListModel* model, int id); + void registerModel(ListModel* model, const QString& root); void unregisterModel(ListModel* model); + bool startJob(SONOS::OS::CWorker* worker); + int jobCount() { return *(m_jobCount.Get()); } + void beginJob(); + void endJob(); + // Define singleton provider functions static QObject* sonos_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { @@ -160,12 +175,15 @@ class Sonos : public QObject } signals: + void initDone(bool succeeded); void loadingStarted(); void loadingFinished(); void transportChanged(); void renderingControlChanged(); void topologyChanged(); + void jobCountChanged(); + private: struct RegisteredContent { @@ -181,6 +199,7 @@ class Sonos : public QObject SONOS::System m_system; SONOS::OS::CThreadPool m_threadpool; + SONOS::LockedNumber m_jobCount; SONOS::Locked m_locale; // language_COUNTRY diff --git a/backend/modules/NosonApp/tracksmodel.cpp b/backend/modules/NosonApp/tracksmodel.cpp index 63068bda..0d50cd86 100644 --- a/backend/modules/NosonApp/tracksmodel.cpp +++ b/backend/modules/NosonApp/tracksmodel.cpp @@ -65,7 +65,9 @@ TracksModel::TracksModel(QObject* parent) TracksModel::~TracksModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); SAFE_DELETE(m_contentList) SAFE_DELETE(m_contentDirectory); } @@ -161,86 +163,104 @@ bool TracksModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void TracksModel::clear() +void TracksModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - m_totalCount = 0; - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool TracksModel::load() +bool TracksModel::loadData() { setUpdateSignaled(false); if (!m_provider) - return false; - clear(); { - SONOS::LockGuard lock(m_lock); - SAFE_DELETE(m_contentList); - SAFE_DELETE(m_contentDirectory); + emit loaded(false); + return false; } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + + SONOS::LockGuard lock(m_lock); + SAFE_DELETE(m_contentList); + SAFE_DELETE(m_contentDirectory); + m_contentDirectory = new SONOS::ContentDirectory(player->GetHost(), player->GetPort()); + if (m_contentDirectory) + m_contentList = new SONOS::ContentList(*m_contentDirectory, m_root.isEmpty() ? SONOS::ContentSearch(SONOS::SearchTrack,"").Root() : m_root.toUtf8().constData()); + if (!m_contentList) { - SONOS::LockGuard lock(m_lock); - m_contentDirectory = new SONOS::ContentDirectory(player->GetHost(), player->GetPort()); - if (m_contentDirectory) - m_contentList = new SONOS::ContentList(*m_contentDirectory, m_root.isEmpty() ? SONOS::ContentSearch(SONOS::SearchTrack,"").Root() : m_root.toUtf8().constData()); - if (!m_contentList) - return false; - m_totalCount = m_contentList->size(); - m_iterator = m_contentList->begin(); + emit loaded(false); + return false; } - emit totalCountChanged(); + m_totalCount = m_contentList->size(); + m_iterator = m_contentList->begin(); QString port; port.setNum(m_contentDirectory->GetPort()); QString url = "http://"; url.append(m_contentDirectory->GetHost().c_str()).append(":").append(port); + clearData(); + m_dataState = ListModel::NoData; unsigned cnt = 0; while (cnt < LOAD_BULKSIZE && m_iterator != m_contentList->end()) { TrackItem* item = new TrackItem(*m_iterator, url); if (item->isValid()) { - addItem(item); + m_data << item; ++cnt; } else { delete item; // Also decrease total count - if (m_totalCount) - { + if (m_totalCount > 0) --m_totalCount; - emit totalCountChanged(); - } } ++m_iterator; } if (m_contentList->failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = m_contentList->GetUpdateID(); // sync new baseline - return m_loaded = true; + emit totalCountChanged(); + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } -bool TracksModel::loadMore() +bool TracksModel::asyncLoad() +{ + if (m_provider) + { + m_provider->runModelLoader(this); + return true; + } + return false; +} + +bool TracksModel::loadMoreData() { SONOS::LockGuard lock(m_lock); if (!m_contentDirectory || !m_contentList) + { + emit loadedMore(false); return false; + } // At end return false if (m_iterator == m_contentList->end()) + { + emit loadedMore(false); return false; + } QString port; port.setNum(m_contentDirectory->GetPort()); @@ -253,15 +273,14 @@ bool TracksModel::loadMore() TrackItem* item = new TrackItem(*m_iterator, url); if (item->isValid()) { - addItem(item); + m_data << item; ++cnt; } else { delete item; // Also decrease total count - if (m_totalCount) - { + if (m_totalCount) { --m_totalCount; emit totalCountChanged(); } @@ -269,14 +288,73 @@ bool TracksModel::loadMore() ++m_iterator; } if (m_contentList->failure()) + { + emit loadedMore(false); return false; + } + m_dataState = ListModel::Loaded; + emit loadedMore(true); return true; } -bool TracksModel::asyncLoad() +bool TracksModel::asyncLoadMore() { - if (m_provider) - m_provider->runModelLoader(this); + if (!m_provider) + return false; + m_provider->runCustomizedModelLoader(this, 1); + return true; +} + +void TracksModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (TrackItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); +} + +void TracksModel::appendModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + int cnt = m_items.count(); + beginInsertRows(QModelIndex(), cnt, cnt + m_data.count()-1); + foreach (TrackItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + } + emit countChanged(); +} + +bool TracksModel::customizedLoad(int id) +{ + switch (id) + { + case 0: + return loadData(); + case 1: + return loadMoreData(); + default: + return false; + } } void TracksModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/tracksmodel.h b/backend/modules/NosonApp/tracksmodel.h index 79fd8f68..5b1a753d 100644 --- a/backend/modules/NosonApp/tracksmodel.h +++ b/backend/modules/NosonApp/tracksmodel.h @@ -95,16 +95,24 @@ class TracksModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); int totalCount() const { return m_totalCount; } - Q_INVOKABLE bool loadMore(); - Q_INVOKABLE bool asyncLoad(); + virtual bool loadMoreData(); + + Q_INVOKABLE bool asyncLoadMore(); + + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel(); + + virtual bool customizedLoad(int id); + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -113,12 +121,15 @@ class TracksModel : public QAbstractListModel, public ListModel void dataUpdated(); void countChanged(); void totalCountChanged(); + void loaded(bool succeeded); + void loadedMore(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; SONOS::ContentDirectory* m_contentDirectory; SONOS::ContentList* m_contentList; diff --git a/backend/modules/NosonApp/zonesmodel.cpp b/backend/modules/NosonApp/zonesmodel.cpp index e1740a42..9b8be67c 100644 --- a/backend/modules/NosonApp/zonesmodel.cpp +++ b/backend/modules/NosonApp/zonesmodel.cpp @@ -53,7 +53,9 @@ ZonesModel::ZonesModel(QObject* parent) ZonesModel::~ZonesModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void ZonesModel::addItem(ZoneItem* item) @@ -75,6 +77,7 @@ int ZonesModel::rowCount(const QModelIndex& parent) const QVariant ZonesModel::data(const QModelIndex& index, int role) const { + SONOS::LockGuard lock(m_lock); if (index.row() < 0 || index.row() >= m_items.count()) return QVariant(); @@ -132,36 +135,70 @@ bool ZonesModel::init(QObject* sonos, bool fill) return ListModel::init(sonos, "", fill); } -void ZonesModel::clear() +void ZonesModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool ZonesModel::load() +bool ZonesModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); - SONOS::ZoneList zones = m_provider->getSystem().GetZoneList(); + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; + SONOS::ZoneList zones = m_provider->getSystem().GetZoneList(); for (SONOS::ZoneList::iterator it = zones.begin(); it != zones.end(); ++it) { ZoneItem* item = new ZoneItem(it->second); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; +} + +bool ZonesModel::asyncLoad() +{ + if (m_provider) + { + m_provider->runModelLoader(this); + return true; + } + return false; +} + +void ZonesModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (ZoneItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void ZonesModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/zonesmodel.h b/backend/modules/NosonApp/zonesmodel.h index 303302f4..04678508 100644 --- a/backend/modules/NosonApp/zonesmodel.h +++ b/backend/modules/NosonApp/zonesmodel.h @@ -86,21 +86,29 @@ class ZonesModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); + + Q_INVOKABLE bool asyncLoad(); + + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } virtual void handleDataUpdate(); signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* ZONESMODEL_H */ From c7c341fec3f2dcbf53c34dd6a75fb1c825b990b7 Mon Sep 17 00:00:00 2001 From: janbar Date: Thu, 28 Dec 2017 12:28:25 +0100 Subject: [PATCH 2/9] fix QML install path --- backend/CMakeLists.txt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 4e6ed479..f4276673 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -49,6 +49,15 @@ add_custom_target(NosonAppbackend-qmldir ALL ) # Install plugin file -install(TARGETS NosonAppbackend DESTINATION ${QT_IMPORTS_DIR}/NosonApp/) -install(FILES modules/NosonApp/qmldir DESTINATION ${QT_IMPORTS_DIR}/NosonApp/) +FIND_PROGRAM(QMAKE NAMES qmake-qt5 qmake) +IF(NOT QMAKE) + MESSAGE(FATAL_ERROR "qmake not found") +ENDIF() +EXECUTE_PROCESS( + COMMAND ${QMAKE} -query QT_INSTALL_QML + OUTPUT_VARIABLE QT_INSTALL_QML OUTPUT_STRIP_TRAILING_WHITESPACE +) +MESSAGE(STATUS "QML install path: ${QT_INSTALL_QML}") +install(TARGETS NosonAppbackend DESTINATION ${QT_INSTALL_QML}/NosonApp/) +install(FILES modules/NosonApp/qmldir DESTINATION ${QT_INSTALL_QML}/NosonApp/) From 4df37dd14495b5bf51a1aa926c2b6983efe5ff4b Mon Sep 17 00:00:00 2001 From: janbar Date: Thu, 28 Dec 2017 13:33:54 +0100 Subject: [PATCH 3/9] fix QML install path - 2 --- backend/CMakeLists.txt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index f4276673..96407812 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -49,14 +49,7 @@ add_custom_target(NosonAppbackend-qmldir ALL ) # Install plugin file -FIND_PROGRAM(QMAKE NAMES qmake-qt5 qmake) -IF(NOT QMAKE) - MESSAGE(FATAL_ERROR "qmake not found") -ENDIF() -EXECUTE_PROCESS( - COMMAND ${QMAKE} -query QT_INSTALL_QML - OUTPUT_VARIABLE QT_INSTALL_QML OUTPUT_STRIP_TRAILING_WHITESPACE -) +SET(QT_INSTALL_QML ${QT_IMPORTS_DIR}/qt5/qml) MESSAGE(STATUS "QML install path: ${QT_INSTALL_QML}") install(TARGETS NosonAppbackend DESTINATION ${QT_INSTALL_QML}/NosonApp/) install(FILES modules/NosonApp/qmldir DESTINATION ${QT_INSTALL_QML}/NosonApp/) From dfb12de6d0775beeb69b19a34618d336247f2f09 Mon Sep 17 00:00:00 2001 From: janbar Date: Tue, 2 Jan 2018 15:49:03 +0100 Subject: [PATCH 4/9] review cmake configuration for lib --- backend/CMakeLists.txt | 22 ++++++++++++++-------- backend/lib/CMakeLists.txt | 8 ++++++++ backend/lib/noson/noson/CMakeLists.txt | 8 ++++++-- 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 backend/lib/CMakeLists.txt diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 96407812..1e9597e3 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -1,12 +1,19 @@ -cmake_policy (VERSION 3.0) -add_subdirectory (${CMAKE_CURRENT_SOURCE_DIR}/lib/noson/noson) +cmake_minimum_required(VERSION 2.8.9) +# Automatically create moc files +set(CMAKE_AUTOMOC ON) + +find_package(Qt5Core REQUIRED) +find_package(Qt5Gui REQUIRED) +find_package(Qt5Qml REQUIRED) +find_package(Qt5Quick REQUIRED) + +add_subdirectory (${CMAKE_CURRENT_SOURCE_DIR}/lib) ############################################################################### # configure include_directories( ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR}/lib/noson/noson/include/. - ${CMAKE_CURRENT_SOURCE_DIR}/lib/noson/src/. + ${CMAKE_CURRENT_BINARY_DIR}/lib/noson/noson/include ) set( @@ -49,8 +56,7 @@ add_custom_target(NosonAppbackend-qmldir ALL ) # Install plugin file -SET(QT_INSTALL_QML ${QT_IMPORTS_DIR}/qt5/qml) -MESSAGE(STATUS "QML install path: ${QT_INSTALL_QML}") -install(TARGETS NosonAppbackend DESTINATION ${QT_INSTALL_QML}/NosonApp/) -install(FILES modules/NosonApp/qmldir DESTINATION ${QT_INSTALL_QML}/NosonApp/) +MESSAGE(STATUS "PlugIns install path: ${PLUGINS_DIR}") +install(TARGETS NosonAppbackend DESTINATION ${PLUGINS_DIR}/NosonApp/) +install(FILES modules/NosonApp/qmldir DESTINATION ${PLUGINS_DIR}/NosonApp/) diff --git a/backend/lib/CMakeLists.txt b/backend/lib/CMakeLists.txt new file mode 100644 index 00000000..33e2bd04 --- /dev/null +++ b/backend/lib/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(ZLIB) +find_package(OpenSSL) + +# Provides noson +add_subdirectory( + ${CMAKE_CURRENT_SOURCE_DIR}/noson/noson + EXCLUDE_FROM_ALL +) diff --git a/backend/lib/noson/noson/CMakeLists.txt b/backend/lib/noson/noson/CMakeLists.txt index 96072916..baf02965 100644 --- a/backend/lib/noson/noson/CMakeLists.txt +++ b/backend/lib/noson/noson/CMakeLists.txt @@ -95,7 +95,9 @@ else () set (HAVE_GMTIME_R 0) endif () -find_package (ZLIB REQUIRED) +if (NOT ZLIB_FOUND) + find_package (ZLIB REQUIRED) +endif() if (ZLIB_FOUND) include_directories (${ZLIB_INCLUDE_DIRS}) set (HAVE_ZLIB 1) @@ -103,7 +105,9 @@ else () set (HAVE_ZLIB 0) endif () -find_package(OpenSSL REQUIRED) +if (NOT OPENSSL_FOUND) + find_package(OpenSSL REQUIRED) +endif() if (OPENSSL_FOUND) include_directories (${OPENSSL_INCLUDE_DIR}) set (HAVE_OPENSSL 1) From 0a2c40f15330e06f99fadb802187610293563e17 Mon Sep 17 00:00:00 2001 From: janbar Date: Thu, 4 Jan 2018 01:44:11 +0100 Subject: [PATCH 5/9] fix compilation warnings in backend plugin --- backend/lib/CMakeLists.txt | 2 ++ backend/modules/NosonApp/artistsmodel.cpp | 2 +- backend/modules/NosonApp/genresmodel.cpp | 2 +- backend/modules/NosonApp/listmodel.h | 4 +++- backend/modules/NosonApp/mediamodel.cpp | 2 +- backend/modules/NosonApp/mediamodel.h | 4 ++-- backend/modules/NosonApp/player.cpp | 2 +- backend/modules/NosonApp/playlistsmodel.h | 2 +- backend/modules/NosonApp/radiosmodel.cpp | 1 + backend/modules/NosonApp/roomsmodel.cpp | 2 +- backend/modules/NosonApp/servicesmodel.cpp | 5 ----- backend/modules/NosonApp/servicesmodel.h | 2 +- backend/modules/NosonApp/zonesmodel.cpp | 5 ----- backend/modules/NosonApp/zonesmodel.h | 2 +- 14 files changed, 16 insertions(+), 21 deletions(-) diff --git a/backend/lib/CMakeLists.txt b/backend/lib/CMakeLists.txt index 33e2bd04..024b1e51 100644 --- a/backend/lib/CMakeLists.txt +++ b/backend/lib/CMakeLists.txt @@ -1,3 +1,5 @@ +cmake_minimum_required(VERSION 2.8.9) + find_package(ZLIB) find_package(OpenSSL) diff --git a/backend/modules/NosonApp/artistsmodel.cpp b/backend/modules/NosonApp/artistsmodel.cpp index a8a9dba4..d959561d 100644 --- a/backend/modules/NosonApp/artistsmodel.cpp +++ b/backend/modules/NosonApp/artistsmodel.cpp @@ -32,7 +32,7 @@ ArtistItem::ArtistItem(const SONOS::DigitalItemPtr& ptr, const QString& baseURL) { m_artist = QString::fromUtf8(ptr->GetValue("dc:title").c_str()); m_normalized = normalizedString(m_artist); - //m_art.append(baseURL).append(uri); + (void)baseURL; //m_art.append(baseURL).append(uri); m_valid = true; } } diff --git a/backend/modules/NosonApp/genresmodel.cpp b/backend/modules/NosonApp/genresmodel.cpp index 28831aca..ccfb60ab 100644 --- a/backend/modules/NosonApp/genresmodel.cpp +++ b/backend/modules/NosonApp/genresmodel.cpp @@ -32,7 +32,7 @@ GenreItem::GenreItem(const SONOS::DigitalItemPtr& ptr, const QString& baseURL) { m_genre = QString::fromUtf8(ptr->GetValue("dc:title").c_str()); m_normalized = normalizedString(m_genre); - //m_art.append(baseURL).append(uri); + (void)baseURL; //m_art.append(baseURL).append(uri); m_valid = true; } } diff --git a/backend/modules/NosonApp/listmodel.h b/backend/modules/NosonApp/listmodel.h index 43e5afc8..8bf4b916 100644 --- a/backend/modules/NosonApp/listmodel.h +++ b/backend/modules/NosonApp/listmodel.h @@ -63,11 +63,13 @@ class ListModel dataState m_dataState; virtual bool init(QObject* sonos, const QString& root, bool fill = false); + virtual bool init(QObject* sonos, bool fill = false) { return init(sonos, QString(""), fill); } + virtual bool init(QObject* sonos, const QVariant&, bool fill = false) { return init(sonos, QString(""), fill); } bool updateSignaled() { return m_updateSignaled.Load(); } void setUpdateSignaled(bool val) { m_updateSignaled.Store(val); } - virtual bool customizedLoad(int id) { return false; } + virtual bool customizedLoad(int id) { (void)id; return false; } private: SONOS::Locked m_updateSignaled; diff --git a/backend/modules/NosonApp/mediamodel.cpp b/backend/modules/NosonApp/mediamodel.cpp index d7837ab5..28a071c8 100644 --- a/backend/modules/NosonApp/mediamodel.cpp +++ b/backend/modules/NosonApp/mediamodel.cpp @@ -247,7 +247,7 @@ bool MediaModel::init(QObject* sonos, const QVariant& service, bool fill) m_auth.token = oa.token; // initialize path to root m_path.clear(); - return ListModel::init(sonos, "", fill); + return ListModel::init(sonos, fill); } void MediaModel::clearData() diff --git a/backend/modules/NosonApp/mediamodel.h b/backend/modules/NosonApp/mediamodel.h index 44696cb0..126e2ea7 100644 --- a/backend/modules/NosonApp/mediamodel.h +++ b/backend/modules/NosonApp/mediamodel.h @@ -84,9 +84,9 @@ class MediaItem const QString& objectId() const { return m_objectId; } - const int displayType() const { return m_displayType; } + int displayType() const { return m_displayType; } - const bool isContainer() const { return m_isContainer; } + bool isContainer() const { return m_isContainer; } private: SONOS::DigitalItemPtr m_ptr; diff --git a/backend/modules/NosonApp/player.cpp b/backend/modules/NosonApp/player.cpp index e8f1c6e7..829abc3f 100644 --- a/backend/modules/NosonApp/player.cpp +++ b/backend/modules/NosonApp/player.cpp @@ -21,7 +21,7 @@ #include "player.h" #include "sonos.h" #include "tools.h" -#include "lib/noson/noson/src/private/debug.h" +#include "../../lib/noson/noson/src/private/debug.h" #include // for sscanf #include diff --git a/backend/modules/NosonApp/playlistsmodel.h b/backend/modules/NosonApp/playlistsmodel.h index 2fa4b395..68ecfbe4 100644 --- a/backend/modules/NosonApp/playlistsmodel.h +++ b/backend/modules/NosonApp/playlistsmodel.h @@ -42,7 +42,7 @@ class PlaylistItem int artsCount() const { return m_arts.size(); } - QString art(unsigned index) const { return (artsCount() > index ? m_arts[index] : ""); } + QString art(int index) const { return (m_arts.size() > index ? m_arts[index] : ""); } QStringList arts() const { return QStringList(m_arts); } diff --git a/backend/modules/NosonApp/radiosmodel.cpp b/backend/modules/NosonApp/radiosmodel.cpp index be4797ba..969d87bd 100644 --- a/backend/modules/NosonApp/radiosmodel.cpp +++ b/backend/modules/NosonApp/radiosmodel.cpp @@ -30,6 +30,7 @@ RadioItem::RadioItem(const SONOS::DigitalItemPtr& ptr, const QString& baseURL) : m_ptr(ptr) , m_valid(false) { + (void)baseURL; m_id = QString::fromUtf8(ptr->GetObjectID().c_str()); if (ptr->subType() == SONOS::DigitalItem::SubType_audioItem) { diff --git a/backend/modules/NosonApp/roomsmodel.cpp b/backend/modules/NosonApp/roomsmodel.cpp index 369b8e6c..46187b59 100644 --- a/backend/modules/NosonApp/roomsmodel.cpp +++ b/backend/modules/NosonApp/roomsmodel.cpp @@ -155,7 +155,7 @@ bool RoomsModel::loadData() bool RoomsModel::load(QObject* sonos) { m_provider = reinterpret_cast (sonos); - m_zoneId = QString::null; + m_zoneId = QString(); if (!loadData()) return false; resetModel(); diff --git a/backend/modules/NosonApp/servicesmodel.cpp b/backend/modules/NosonApp/servicesmodel.cpp index 09177cbf..f8d21de0 100644 --- a/backend/modules/NosonApp/servicesmodel.cpp +++ b/backend/modules/NosonApp/servicesmodel.cpp @@ -129,11 +129,6 @@ QVariantMap ServicesModel::get(int row) return model; } -bool ServicesModel::init(QObject* sonos, bool fill) -{ - return ListModel::init(sonos, "", fill); -} - void ServicesModel::clearData() { SONOS::LockGuard lock(m_lock); diff --git a/backend/modules/NosonApp/servicesmodel.h b/backend/modules/NosonApp/servicesmodel.h index fd7d37c7..6aae5d16 100644 --- a/backend/modules/NosonApp/servicesmodel.h +++ b/backend/modules/NosonApp/servicesmodel.h @@ -84,7 +84,7 @@ class ServicesModel : public QAbstractListModel, public ListModel Q_INVOKABLE QVariantMap get(int row); - Q_INVOKABLE bool init(QObject* sonos, bool fill = false); + Q_INVOKABLE bool init(QObject* sonos, bool fill = false) { return ListModel::init(sonos, fill); } virtual void clearData(); diff --git a/backend/modules/NosonApp/zonesmodel.cpp b/backend/modules/NosonApp/zonesmodel.cpp index 9b8be67c..852e47a9 100644 --- a/backend/modules/NosonApp/zonesmodel.cpp +++ b/backend/modules/NosonApp/zonesmodel.cpp @@ -130,11 +130,6 @@ QVariantMap ZonesModel::get(int row) return model; } -bool ZonesModel::init(QObject* sonos, bool fill) -{ - return ListModel::init(sonos, "", fill); -} - void ZonesModel::clearData() { SONOS::LockGuard lock(m_lock); diff --git a/backend/modules/NosonApp/zonesmodel.h b/backend/modules/NosonApp/zonesmodel.h index 04678508..a2494723 100644 --- a/backend/modules/NosonApp/zonesmodel.h +++ b/backend/modules/NosonApp/zonesmodel.h @@ -84,7 +84,7 @@ class ZonesModel : public QAbstractListModel, public ListModel Q_INVOKABLE QVariantMap get(int row); - Q_INVOKABLE bool init(QObject* sonos, bool fill = false); + Q_INVOKABLE bool init(QObject* sonos, bool fill = false) { return ListModel::init(sonos, fill); } virtual void clearData(); From f7059ead0a4b70bf761115ee44b39bc0b580b655 Mon Sep 17 00:00:00 2001 From: janbar Date: Sat, 6 Jan 2018 19:10:31 +0100 Subject: [PATCH 6/9] allow to reset invalid art url from models --- backend/modules/NosonApp/albumsmodel.cpp | 17 +++++++++++++++++ backend/modules/NosonApp/albumsmodel.h | 4 ++++ backend/modules/NosonApp/favoritesmodel.cpp | 17 +++++++++++++++++ backend/modules/NosonApp/favoritesmodel.h | 4 ++++ backend/modules/NosonApp/queuemodel.cpp | 17 +++++++++++++++++ backend/modules/NosonApp/queuemodel.h | 2 ++ backend/modules/NosonApp/radiosmodel.cpp | 18 ++++++++++++++++++ backend/modules/NosonApp/radiosmodel.h | 4 ++++ backend/modules/NosonApp/tracksmodel.cpp | 17 +++++++++++++++++ backend/modules/NosonApp/tracksmodel.h | 4 ++++ 10 files changed, 104 insertions(+) diff --git a/backend/modules/NosonApp/albumsmodel.cpp b/backend/modules/NosonApp/albumsmodel.cpp index 676148e6..b6506840 100644 --- a/backend/modules/NosonApp/albumsmodel.cpp +++ b/backend/modules/NosonApp/albumsmodel.cpp @@ -103,6 +103,23 @@ QVariant AlbumsModel::data(const QModelIndex& index, int role) const } } +bool AlbumsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + AlbumItem* item = m_items[index.row()]; + switch (role) + { + case ArtRole: + item->setArt(value.toString()); + return true; + default: + return false; + } +} + QHash AlbumsModel::roleNames() const { QHash roles; diff --git a/backend/modules/NosonApp/albumsmodel.h b/backend/modules/NosonApp/albumsmodel.h index 1fc46a59..5b8d8ce4 100644 --- a/backend/modules/NosonApp/albumsmodel.h +++ b/backend/modules/NosonApp/albumsmodel.h @@ -46,6 +46,8 @@ class AlbumItem const QString& normalized() const { return m_normalized; } + void setArt(const QString& art) { m_art = art; } + private: SONOS::DigitalItemPtr m_ptr; bool m_valid; @@ -81,6 +83,8 @@ class AlbumsModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); diff --git a/backend/modules/NosonApp/favoritesmodel.cpp b/backend/modules/NosonApp/favoritesmodel.cpp index 1a2448f7..f79cb50f 100644 --- a/backend/modules/NosonApp/favoritesmodel.cpp +++ b/backend/modules/NosonApp/favoritesmodel.cpp @@ -162,6 +162,23 @@ QVariant FavoritesModel::data(const QModelIndex& index, int role) const } } +bool FavoritesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + FavoriteItem* item = m_items[index.row()]; + switch (role) + { + case ArtRole: + item->setArt(value.toString()); + return true; + default: + return false; + } +} + QHash FavoritesModel::roleNames() const { QHash roles; diff --git a/backend/modules/NosonApp/favoritesmodel.h b/backend/modules/NosonApp/favoritesmodel.h index 61b8cc2d..769db0a8 100644 --- a/backend/modules/NosonApp/favoritesmodel.h +++ b/backend/modules/NosonApp/favoritesmodel.h @@ -81,6 +81,8 @@ class FavoriteItem bool isService() const { return m_isService; } + void setArt(const QString& art) { m_art = art; } + private: SONOS::DigitalItemPtr m_ptr; bool m_valid; @@ -130,6 +132,8 @@ class FavoritesModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); diff --git a/backend/modules/NosonApp/queuemodel.cpp b/backend/modules/NosonApp/queuemodel.cpp index 1499b0a3..9347041f 100644 --- a/backend/modules/NosonApp/queuemodel.cpp +++ b/backend/modules/NosonApp/queuemodel.cpp @@ -82,6 +82,23 @@ QVariant QueueModel::data(const QModelIndex& index, int role) const } } +bool QueueModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + TrackItem* item = m_items[index.row()]; + switch (role) + { + case ArtRole: + item->setArt(value.toString()); + return true; + default: + return false; + } +} + QHash QueueModel::roleNames() const { QHash roles; diff --git a/backend/modules/NosonApp/queuemodel.h b/backend/modules/NosonApp/queuemodel.h index 9385fa23..58c32362 100644 --- a/backend/modules/NosonApp/queuemodel.h +++ b/backend/modules/NosonApp/queuemodel.h @@ -51,6 +51,8 @@ class QueueModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); diff --git a/backend/modules/NosonApp/radiosmodel.cpp b/backend/modules/NosonApp/radiosmodel.cpp index 969d87bd..60b91212 100644 --- a/backend/modules/NosonApp/radiosmodel.cpp +++ b/backend/modules/NosonApp/radiosmodel.cpp @@ -51,6 +51,7 @@ RadioItem::RadioItem(const SONOS::DigitalItemPtr& ptr, const QString& baseURL) char* end = beg; while (isdigit(*(++end))); m_streamId = QString::fromUtf8(beg, end - beg); + m_icon = QString("http://cdn-radiotime-logos.tunein.com/").append(m_streamId).append("q.png"); } } } @@ -120,6 +121,23 @@ QVariant RadiosModel::data(const QModelIndex& index, int role) const } } +bool RadiosModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + RadioItem* item = m_items[index.row()]; + switch (role) + { + case IconRole: + item->setIcon(value.toString()); + return true; + default: + return false; + } +} + QHash RadiosModel::roleNames() const { QHash roles; diff --git a/backend/modules/NosonApp/radiosmodel.h b/backend/modules/NosonApp/radiosmodel.h index 83c2882c..26f32037 100644 --- a/backend/modules/NosonApp/radiosmodel.h +++ b/backend/modules/NosonApp/radiosmodel.h @@ -48,6 +48,8 @@ class RadioItem const QString& normalized() const { return m_normalized; } + void setIcon(const QString& icon) { m_icon = icon; } + private: SONOS::DigitalItemPtr m_ptr; bool m_valid; @@ -85,6 +87,8 @@ class RadiosModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); diff --git a/backend/modules/NosonApp/tracksmodel.cpp b/backend/modules/NosonApp/tracksmodel.cpp index 0d50cd86..0cf69842 100644 --- a/backend/modules/NosonApp/tracksmodel.cpp +++ b/backend/modules/NosonApp/tracksmodel.cpp @@ -120,6 +120,23 @@ QVariant TracksModel::data(const QModelIndex& index, int role) const } } +bool TracksModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + TrackItem* item = m_items[index.row()]; + switch (role) + { + case ArtRole: + item->setArt(value.toString()); + return true; + default: + return false; + } +} + QHash TracksModel::roleNames() const { QHash roles; diff --git a/backend/modules/NosonApp/tracksmodel.h b/backend/modules/NosonApp/tracksmodel.h index 5b1a753d..59a3d7c2 100644 --- a/backend/modules/NosonApp/tracksmodel.h +++ b/backend/modules/NosonApp/tracksmodel.h @@ -51,6 +51,8 @@ class TrackItem bool isService() const { return m_isService; } + void setArt(const QString& art) { m_art = art; } + private: SONOS::DigitalItemPtr m_ptr; bool m_valid; @@ -91,6 +93,8 @@ class TracksModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); From 9aab7c45425aaf13b3db1bebc94ff8789dc9a37e Mon Sep 17 00:00:00 2001 From: janbar Date: Thu, 11 Jan 2018 00:41:45 +0100 Subject: [PATCH 7/9] including headers in project --- backend/CMakeLists.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 1e9597e3..629a9866 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -18,23 +18,41 @@ include_directories( set( NosonAppbackend_SRCS + modules/NosonApp/tools.h modules/NosonApp/backend.cpp + modules/NosonApp/backend.h modules/NosonApp/sonos.cpp + modules/NosonApp/sonos.h modules/NosonApp/player.cpp + modules/NosonApp/player.h modules/NosonApp/listmodel.cpp + modules/NosonApp/listmodel.h modules/NosonApp/albumsmodel.cpp + modules/NosonApp/albumsmodel.h modules/NosonApp/artistsmodel.cpp + modules/NosonApp/artistsmodel.h modules/NosonApp/genresmodel.cpp + modules/NosonApp/genresmodel.h modules/NosonApp/tracksmodel.cpp + modules/NosonApp/tracksmodel.h modules/NosonApp/queuemodel.cpp + modules/NosonApp/queuemodel.h modules/NosonApp/radiosmodel.cpp + modules/NosonApp/radiosmodel.h modules/NosonApp/playlistsmodel.cpp + modules/NosonApp/playlistsmodel.h modules/NosonApp/zonesmodel.cpp + modules/NosonApp/zonesmodel.h modules/NosonApp/renderingmodel.cpp + modules/NosonApp/renderingmodel.h modules/NosonApp/roomsmodel.cpp + modules/NosonApp/roomsmodel.h modules/NosonApp/favoritesmodel.cpp + modules/NosonApp/favoritesmodel.h modules/NosonApp/servicesmodel.cpp + modules/NosonApp/servicesmodel.h modules/NosonApp/mediamodel.cpp + modules/NosonApp/mediamodel.h ) add_library(NosonAppbackend MODULE From c6b7518e3c45339d34b69993b499562fb64bbb36 Mon Sep 17 00:00:00 2001 From: janbar Date: Sun, 14 Jan 2018 01:40:07 +0100 Subject: [PATCH 8/9] refactor with lastest backend --- app/Main.qml | 213 +++++++++++------- app/components/Dialog/DialogSearchMusic.qml | 2 +- app/components/Dialog/DialogSelectSource.qml | 46 ++-- .../HeadState/AlbumSongsHeadState.qml | 2 +- .../HeadState/PlaylistHeadState.qml | 2 +- .../HeadState/SearchableHeadState.qml | 4 +- app/components/HeadState/ServiceHeadState.qml | 4 +- app/components/Player.qml | 15 +- app/components/Queue.qml | 6 - app/components/ServiceLogin.qml | 4 +- app/components/ServiceRegistration.qml | 4 +- app/components/TrackQueue.qml | 39 ++-- app/components/ViewButton/PlayAllButton.qml | 2 - app/components/ViewButton/QueueAllButton.qml | 2 - app/components/ViewButton/ShuffleButton.qml | 2 - app/ui/Albums.qml | 9 + app/ui/ArtistView.qml | 30 ++- app/ui/Artists.qml | 9 + app/ui/Favorites.qml | 5 - app/ui/Genres.qml | 10 + app/ui/Group.qml | 64 +++--- app/ui/NoZoneState.qml | 12 +- app/ui/NowPlaying.qml | 2 +- app/ui/Playlists.qml | 9 + app/ui/Radios.qml | 36 +-- app/ui/Service.qml | 198 +++++++--------- app/ui/SongsView.qml | 121 +++++----- app/ui/Zones.qml | 70 +----- 28 files changed, 433 insertions(+), 489 deletions(-) diff --git a/app/Main.qml b/app/Main.qml index fc509bd8..33f63d09 100644 --- a/app/Main.qml +++ b/app/Main.qml @@ -39,6 +39,7 @@ MainView { applicationName: "noson.janbar" id: mainView + focus: true backgroundColor: styleMusic.mainView.backgroundColor Binding { @@ -119,7 +120,7 @@ MainView { property bool wideAspect: width >= units.gu(100) && loadedUI // property to enable pop info on index loaded - property bool infoLoadedIndex: false + property bool infoLoadedIndex: true // enabled at startup // Constants readonly property int queueBatchSize: 100 @@ -129,13 +130,81 @@ MainView { //// Events //// + Connections { + target: Sonos + + onJobCountChanged: jobRunning = Sonos.jobCount > 0 ? true : false + + onInitDone: { + if (succeeded) { + if (noZone) + noZone = false; + } else { + if (!noZone) + noZone = true; + } + } + + onLoadingFinished: { + if (infoLoadedIndex) { + infoLoadedIndex = false; + popInfo.open(i18n.tr("Index loaded")); + } + } + + onTopologyChanged: { + AllZonesModel.asyncLoad() + delayReloadZone.start() + } + } + + Timer { + id: delayReloadZone + interval: 250 + onTriggered: { + if (jobRunning) { + restart(); + } else { + // Reload the zone and start the content loader thread + customdebug("Reloading the zone ..."); + if (connectZone(currentZone)) { + Sonos.runLoader(); + } + } + } + } + // Run on startup Component.onCompleted: { - currentlyWorking = true - if (args.values.debug) { debugLevel = 4 } - delayStartup.start() - + if (args.values.debug) { + mainView.debugLevel = 4 + } customdebug("LANG=" + Qt.locale().name); + Sonos.setLocale(Qt.locale().name); + // initialize all data models + AllZonesModel.init(Sonos, "", false); + AllFavoritesModel.init(Sonos, "", false); + AllServicesModel.init(Sonos, false); + AllAlbumsModel.init(Sonos, "", false); + AllArtistsModel.init(Sonos, "", false); + AllGenresModel.init(Sonos, "", false); + AllRadiosModel.init(Sonos, "R:0/0", false); + AllPlaylistsModel.init(Sonos, "", false); + + // push the page to view + mainPageStack.push(tabs) + + // launch connection + connectSonos(); + + // if a tab index exists restore it, otherwise goto Recent if there are items otherwise go to Albums + tabs.selectedTabIndex = startupSettings.tabIndex === -1 + ? servicesTab.index + : (startupSettings.tabIndex > tabs.count - 1 + ? tabs.count - 1 : startupSettings.tabIndex) + + // signal UI has finished + loadedUI = true; // resize main view according to user settings mainView.width = (startupSettings.width >= units.gu(44) ? startupSettings.width : units.gu(44)); @@ -146,9 +215,13 @@ MainView { customdebug("register account: type=" + acls[i].type + " sn=" + acls[i].sn + " token=" + acls[i].token.substr(0, 1) + "..."); Sonos.addServiceOAuth(acls[i].type, acls[i].sn, acls[i].key, acls[i].token); } + + //@TODO add url to play list + //if (args.values.url) { + //} } - Timer { +/* Timer { id: delayStartup interval: 100 onTriggered: { @@ -171,12 +244,11 @@ MainView { //@TODO add url to play list } } - } + }*/ // Show/hide page NoZoneState onNoZoneChanged: { if (noZone) { - currentlyWorking = false // hide actvity indicator emptyPage = mainPageStack.push(Qt.resolvedUrl("ui/NoZoneState.qml"), {}) } else { mainPageStack.popPage(emptyPage) @@ -186,7 +258,7 @@ MainView { // Refresh player state when application becomes active onApplicationStateChanged: { if (!noZone && applicationState && player.connected) { - mainView.currentlyWorking = true + mainView.jobRunning = true delayPlayerWakeUp.start() } } @@ -201,7 +273,7 @@ MainView { Sonos.renewSubscriptions() noZone = false } - mainView.currentlyWorking = false + mainView.jobRunning = false } } @@ -212,55 +284,57 @@ MainView { Connections { target: AllZonesModel onDataUpdated: AllZonesModel.asyncLoad() + onLoaded: AllZonesModel.resetModel() } Connections { - target: AllAlbumsModel - onDataUpdated: AllAlbumsModel.asyncLoad() + target: AllServicesModel + onDataUpdated: AllServicesModel.asyncLoad() + onLoaded: AllServicesModel.resetModel() } Connections { - target: AllArtistsModel - onDataUpdated: AllArtistsModel.asyncLoad() + target: AllRadiosModel + onDataUpdated: AllRadiosModel.asyncLoad() + onLoaded: AllRadiosModel.resetModel() } Connections { - target: AllGenresModel - onDataUpdated: AllGenresModel.asyncLoad() + target: AllFavoritesModel + onDataUpdated: AllFavoritesModel.asyncLoad() + onLoaded: AllFavoritesModel.resetModel() } Connections { - target: AllRadiosModel - onDataUpdated: AllRadiosModel.asyncLoad() + target: AllArtistsModel + onDataUpdated: AllArtistsModel.asyncLoad() + onLoaded: AllArtistsModel.resetModel() } Connections { - target: AllPlaylistsModel - onDataUpdated: AllPlaylistsModel.asyncLoad() + target: AllAlbumsModel + onDataUpdated: AllAlbumsModel.asyncLoad() + onLoaded: AllAlbumsModel.resetModel() } Connections { - target: AllFavoritesModel - onDataUpdated: AllFavoritesModel.asyncLoad() + target: AllGenresModel + onDataUpdated: AllGenresModel.asyncLoad() + onLoaded: AllGenresModel.resetModel() } Connections { - target: Sonos - onLoadingFinished: { - if (infoLoadedIndex) { - infoLoadedIndex = false; - popInfo.open(i18n.tr("Index loaded")); - } - currentlyWorking = false; // hide actvity indicator - } - - onTopologyChanged: { - reloadZone() - } + target: AllPlaylistsModel + onDataUpdated: AllPlaylistsModel.asyncLoad() + onLoaded: AllPlaylistsModel.resetModel() } - // Global keyboard shortcuts - focus: true + + //////////////////////////////////////////////////////////////////////////// + //// + //// Global keyboard shortcuts + //// + Keys.onPressed: { if(event.key === Qt.Key_Escape) { if (mainPageStack.currentMusicPage.currentDialog !== null) { @@ -366,24 +440,15 @@ MainView { Connections { target: ContentHub onShareRequested: { - delayPlayURL.url = transfer.items[0].url - delayPlayURL.start() - } - } - - Timer { - id: delayPlayURL - interval: 100 - property string url: "" - onTriggered: { - if (!player.playStream(url, "")) + var url = transfer.items[0].url + if (!player.startPlayStream(url, "")) popInfo.open(i18n.tr("Action can't be performed")) else inputStreamUrl = url - mainView.currentlyWorking = false } } + //////////////////////////////////////////////////////////////////////////// //// //// Global actions & helpers @@ -423,48 +488,30 @@ MainView { return acls; } - // Try to connect to SONOS system - // On failure: noZone is set to true + // Try connect to SONOS system function connectSonos() { - if (Sonos.init(debugLevel)) { - Sonos.setLocale(Qt.locale().name); - AllFavoritesModel.init(Sonos, ""); - AllServicesModel.init(Sonos); - AllAlbumsModel.init(Sonos, ""); - AllArtistsModel.init(Sonos, ""); - AllGenresModel.init(Sonos, ""); - AllRadiosModel.init(Sonos, "R:0/0"); - AllPlaylistsModel.init(Sonos, ""); - // enable info on index loaded - infoLoadedIndex = true; - return true; - } - // Signal change if any - if (!noZone) - noZone = true; - return false; + return Sonos.startInit(mainView.debugLevel); } - // Reload zones and try connect - // On success: noZone is set to false and content loader thread is started - // to fill data in global models - function reloadZone() { - AllZonesModel.init(Sonos, true); // force load now - customdebug("Reloading zone ..."); - if ((Sonos.connectZone(currentZone) || Sonos.connectZone("")) && player.connect()) { + signal zoneChanged + + // Try to change zone + // On success noZone is set to false + function connectZone(name) { + var oldZone = currentZone; + customdebug("Connecting zone '" + name + "'"); + if ((Sonos.connectZone(name) || Sonos.connectZone("")) && player.connect()) { currentZone = Sonos.getZoneName(); currentZoneTag = Sonos.getZoneShortName(); - customdebug("Connected zone is '" + currentZone + "'"); - // It is time to fill models - Sonos.runLoader(); - // Signal change if any + if (currentZone !== oldZone) + zoneChanged(); if (noZone) noZone = false; return true; + } else { + if (!noZone) + noZone = true; } - // Signal change if any - if (!noZone) - noZone = true; return false; } @@ -1035,9 +1082,11 @@ MainView { height: status === Loader.Ready ? item.height : 0 } - property alias currentlyWorking: loading.visible + + property bool jobRunning: false LoadingSpinnerComponent { id: loading + visible: jobRunning } } diff --git a/app/components/Dialog/DialogSearchMusic.qml b/app/components/Dialog/DialogSearchMusic.qml index c4c9c96a..39eacd52 100644 --- a/app/components/Dialog/DialogSearchMusic.qml +++ b/app/components/Dialog/DialogSearchMusic.qml @@ -96,7 +96,7 @@ DialogBase { color: styleMusic.dialog.confirmButtonColor onClicked: { if (searchableModel !== null && selector.selectedIndex >= 0 && searchField.text.length) { - searchableModel.loadSearch(selectorModel.get(selector.selectedIndex).id, searchField.text); + searchableModel.asyncLoadSearch(selectorModel.get(selector.selectedIndex).id, searchField.text); } PopupUtils.close(dialogSearchMusic); } diff --git a/app/components/Dialog/DialogSelectSource.qml b/app/components/Dialog/DialogSelectSource.qml index d53b4dc8..8d877b74 100644 --- a/app/components/Dialog/DialogSelectSource.qml +++ b/app/components/Dialog/DialogSelectSource.qml @@ -26,7 +26,7 @@ DialogBase { title: i18n.tr("Select source") Label { - id: urlOutput + id: sourceOutput anchors.left: parent.left anchors.right: parent.right wrapMode: Text.WordWrap @@ -35,6 +35,7 @@ DialogBase { font.weight: Font.Normal visible: false // should only be visible when an error is made. } + TextField { id: url text: inputStreamUrl @@ -42,36 +43,37 @@ DialogBase { inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase color: theme.palette.selected.baseText } + + Connections { + target: player.zonePlayer + onJobFailed: { + sourceOutput.color = UbuntuColors.red + sourceOutput.text = i18n.tr("Playing failed.") + sourceOutput.visible = true + } + } + Button { id: buttonPlayStream text: i18n.tr("Play stream") color: UbuntuColors.green onClicked: { - urlOutput.visible = false // make sure its hidden now if there was an error last time + sourceOutput.visible = false // make sure its hidden now if there was an error last time if (url.text.length > 0) { // make sure something is acually inputed color = UbuntuColors.lightGrey - delayPlayStream.start() - } - else { - urlOutput.visible = true - urlOutput.text = i18n.tr("Please type in an URL.") - } - } - } - - Timer { - id: delayPlayStream - interval: 100 - onTriggered: { - if (player.playStream(url.text, "")) { - inputStreamUrl = url.text - PopupUtils.close(dialogSelectSource) + if (player.playStream(url.text, "")) { + inputStreamUrl = url.text + } + else { + sourceOutput.color = UbuntuColors.red + sourceOutput.text = i18n.tr("Playing failed.") + sourceOutput.visible = true + } + color = UbuntuColors.green } else { - urlOutput.color = UbuntuColors.red - urlOutput.text = i18n.tr("Playing failed.") - urlOutput.visible = true - buttonPlayStream.color = UbuntuColors.green + sourceOutput.visible = true + sourceOutput.text = i18n.tr("Please type in an URL.") } } } diff --git a/app/components/HeadState/AlbumSongsHeadState.qml b/app/components/HeadState/AlbumSongsHeadState.qml index b5c3993b..a4021466 100644 --- a/app/components/HeadState/AlbumSongsHeadState.qml +++ b/app/components/HeadState/AlbumSongsHeadState.qml @@ -44,7 +44,7 @@ State { onTriggered: { if (isFavorite && removeFromFavorites(containerItem.payload)) isFavorite = false - else if (!isFavorite && addItemToFavorites(containerItem, title, albumtrackslist.headerItem.firstSource)) + else if (!isFavorite && addItemToFavorites(containerItem, title, songList.headerItem.firstSource)) isFavorite = true } } diff --git a/app/components/HeadState/PlaylistHeadState.qml b/app/components/HeadState/PlaylistHeadState.qml index c271bd90..6a56b921 100644 --- a/app/components/HeadState/PlaylistHeadState.qml +++ b/app/components/HeadState/PlaylistHeadState.qml @@ -47,7 +47,7 @@ State { onTriggered: { if (isFavorite && removeFromFavorites(containerItem.payload)) isFavorite = false - else if (!isFavorite && addItemToFavorites(containerItem, title, albumtrackslist.headerItem.firstSource)) + else if (!isFavorite && addItemToFavorites(containerItem, title, songList.headerItem.firstSource)) isFavorite = true } }, diff --git a/app/components/HeadState/SearchableHeadState.qml b/app/components/HeadState/SearchableHeadState.qml index 2fd53ce9..71b2f94c 100644 --- a/app/components/HeadState/SearchableHeadState.qml +++ b/app/components/HeadState/SearchableHeadState.qml @@ -50,8 +50,8 @@ State { onTriggered: { thisPage.isListView = !thisPage.isListView if (thisPage.taintedView !== undefined && thisPage.taintedView) { - mainView.currentlyWorking = true; - delayLoadModel.start(); + thisPage.model.asyncLoad(); + thisPage.taintedView = false; // reset } } }, diff --git a/app/components/HeadState/ServiceHeadState.qml b/app/components/HeadState/ServiceHeadState.qml index b13396f4..3f9b7e75 100644 --- a/app/components/HeadState/ServiceHeadState.qml +++ b/app/components/HeadState/ServiceHeadState.qml @@ -47,8 +47,8 @@ State { onTriggered: { thisPage.isListView = !thisPage.isListView; if (thisPage.taintedView) { - mainView.currentlyWorking = true; - delayLoadModel.start(); + thisPage.model.asyncLoad(); + thisPage.taintedView = false; // reset } } }, diff --git a/app/components/Player.qml b/app/components/Player.qml index 2e5f2b8b..5b64094d 100644 --- a/app/components/Player.qml +++ b/app/components/Player.qml @@ -28,6 +28,7 @@ import NosonApp 1.0 Item { id: player objectName: "controller" + property alias zonePlayer: playerLoader.item property bool connected: false property string currentMetaAlbum: "" property string currentMetaArt: "" @@ -72,7 +73,8 @@ Item { if (trackQueue.canLoad) { // When switching zone, updateid cannot drive correctly the queue refreshing // so new force refreshing of queue - return trackQueue.loadQueue(); + trackQueue.loadQueue(); + return true; } return trackQueue.canLoad = true; } @@ -118,8 +120,12 @@ Item { return play(); } + function playSource(modelItem) { + return playerLoader.item.startPlaySource(modelItem.payload); + } + function playSong(modelItem) { - return (setSource(modelItem) && play()); + return playSource(modelItem); } function previousSong(startPlaying) { @@ -260,7 +266,7 @@ Item { } function playStream(url, title) { - return playerLoader.item.playStream(url, (title === "" ? i18n.tr("Untitled") : title)) + return playerLoader.item.startPlayStream(url, (title === "" ? i18n.tr("Untitled") : title)) } function playLineIN() { @@ -280,7 +286,7 @@ Item { } function playFavorite(modelItem) { - return playerLoader.item.playFavorite(modelItem.payload); + return playerLoader.item.startPlayFavorite(modelItem.payload); } property alias renderingModel: renderingModelLoader.item @@ -299,6 +305,7 @@ Item { asynchronous: true sourceComponent: Component { ZonePlayer { + onJobFailed: popInfo.open(i18n.tr("Action can't be performed")); onConnectedChanged: player.connected = connected onRenderingGroupChanged: player.refreshRenderingGroup() onRenderingChanged: player.refreshRendering() diff --git a/app/components/Queue.qml b/app/components/Queue.qml index d52437a9..292e7609 100644 --- a/app/components/Queue.qml +++ b/app/components/Queue.qml @@ -74,7 +74,6 @@ Item { actions: [ Remove { onTriggered: { - mainView.currentlyWorking = true delayRemoveTrackFromQueue.start() } } @@ -105,12 +104,10 @@ Item { interval: 100 onTriggered: { removeTrackFromQueue(model) - mainView.currentlyWorking = false } } onItemClicked: { - mainView.currentlyWorking = true delayIndexQueueClicked.start() } @@ -119,7 +116,6 @@ Item { interval: 100 onTriggered: { indexQueueClicked(index) // toggle track state - mainView.currentlyWorking = false } } } @@ -127,7 +123,6 @@ Item { onReorder: { delayReorderTrackInQueue.argFrom = from delayReorderTrackInQueue.argTo = to - mainView.currentlyWorking = true delayReorderTrackInQueue.start() } @@ -138,7 +133,6 @@ Item { property int argTo: 0 onTriggered: { reorderTrackInQueue(argFrom, argTo) - mainView.currentlyWorking = false } } diff --git a/app/components/ServiceLogin.qml b/app/components/ServiceLogin.qml index ce3e9e0d..d86fb399 100644 --- a/app/components/ServiceLogin.qml +++ b/app/components/ServiceLogin.qml @@ -108,7 +108,7 @@ Rectangle { width: parent.width onClicked: { - mainView.currentlyWorking = true + mainView.jobRunning = true delayLoginService.start() } } @@ -119,7 +119,7 @@ Rectangle { onTriggered: { loginOutput.visible = false; var ret = mediaModel.requestSessionId(username.text, password.text); - mainView.currentlyWorking = false; + mainView.jobRunning = false; if (ret === 0) { customdebug("Service login failed."); loginOutput.text = i18n.tr("Login failed."); diff --git a/app/components/ServiceRegistration.qml b/app/components/ServiceRegistration.qml index d8dd7c33..03e7f924 100644 --- a/app/components/ServiceRegistration.qml +++ b/app/components/ServiceRegistration.qml @@ -94,7 +94,7 @@ Rectangle { width: parent.width onClicked: { - mainView.currentlyWorking = true + mainView.jobRunning = true delayRegisterService.start() } } @@ -110,7 +110,7 @@ Rectangle { regUrl.text = "" + mediaModel.regURL + ""; requestAuthForTime.start(); } - mainView.currentlyWorking = false + mainView.jobRunning = false } } diff --git a/app/components/TrackQueue.qml b/app/components/TrackQueue.qml index 976ceacd..57c48bd1 100644 --- a/app/components/TrackQueue.qml +++ b/app/components/TrackQueue.qml @@ -27,36 +27,31 @@ Item { } function loadQueue() { - if (canLoad) { - if (model.load()) { - player.currentCount = model.count - return completed = true - } - player.currentCount = 0 - return completed = false - } - return false + if (canLoad) + model.asyncLoad() } onCanLoadChanged: { - mainView.currentlyWorking = true - delayLoadQueue.start() - } - - Timer { - id: delayLoadQueue - interval: 100 - onTriggered: { - loadQueue() - mainView.currentlyWorking = false - } + if (canLoad) + model.asyncLoad() } Connections { target: model onDataUpdated: { - mainView.currentlyWorking = true - delayLoadQueue.start() + if (canLoad) + model.asyncLoad() + } + onLoaded: { + if (succeeded) { + model.resetModel() + player.currentCount = model.count + completed = true + } else { + model.resetModel() + player.currentCount = 0 + completed = false + } } } } diff --git a/app/components/ViewButton/PlayAllButton.qml b/app/components/ViewButton/PlayAllButton.qml index 8f0ea175..3578e3ad 100644 --- a/app/components/ViewButton/PlayAllButton.qml +++ b/app/components/ViewButton/PlayAllButton.qml @@ -40,7 +40,6 @@ Button { } onClicked: { - mainView.currentlyWorking = true delayPlayAll.start() } @@ -49,7 +48,6 @@ Button { interval: 100 onTriggered: { playAll(containerItem) - mainView.currentlyWorking = false } } diff --git a/app/components/ViewButton/QueueAllButton.qml b/app/components/ViewButton/QueueAllButton.qml index e4df605f..c95d1040 100644 --- a/app/components/ViewButton/QueueAllButton.qml +++ b/app/components/ViewButton/QueueAllButton.qml @@ -40,7 +40,6 @@ Button { } onClicked: { - mainView.currentlyWorking = true delayAddQueue.start() } @@ -49,7 +48,6 @@ Button { interval: 100 onTriggered: { addQueue(containerItem) - mainView.currentlyWorking = false } } diff --git a/app/components/ViewButton/ShuffleButton.qml b/app/components/ViewButton/ShuffleButton.qml index de7e475f..b2c023b4 100644 --- a/app/components/ViewButton/ShuffleButton.qml +++ b/app/components/ViewButton/ShuffleButton.qml @@ -39,7 +39,6 @@ Button { } onClicked: { - mainView.currentlyWorking = true delayShuffleModel.start() } @@ -48,7 +47,6 @@ Button { interval: 100 onTriggered: { shuffleModel(model) - mainView.currentlyWorking = false } } diff --git a/app/ui/Albums.qml b/app/ui/Albums.qml index 64058c3b..48d48e83 100644 --- a/app/ui/Albums.qml +++ b/app/ui/Albums.qml @@ -108,6 +108,15 @@ MusicPage { "line2": model.title !== undefined && model.title !== "" ? model.title : i18n.tr("Unknown Album") }) } + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false diff --git a/app/ui/ArtistView.qml b/app/ui/ArtistView.qml index d63ed87e..9f3611ef 100644 --- a/app/ui/ArtistView.qml +++ b/app/ui/ArtistView.qml @@ -160,10 +160,17 @@ MusicPage { } } } + model: AlbumsModel { id: albumsModel - Component.onCompleted: init(Sonos, artistSearch, true) + onDataUpdated: albumsModel.asyncLoad() + onLoaded: albumsModel.resetModel() + Component.onCompleted: { + init(Sonos, artistSearch, false) + albumsModel.asyncLoad() + } } + delegate: Card { id: albumCard coverSources: [ @@ -192,24 +199,15 @@ MusicPage { } } + // Query total count of artist's songs TracksModel { id: songArtistModel - } - - Timer { - id: delayInitModel - interval: 100 - onTriggered: { - isFavorite = (AllFavoritesModel.findFavorite(containerItem.payload) !== "") - songArtistModel.init(Sonos, artistSearch + "/", true) - mainView.currentlyWorking = false - loaded = true + onDataUpdated: songArtistModel.asyncLoad() + onLoaded: songArtistModel.resetModel() + Component.onCompleted: { + songArtistModel.init(Sonos, artistSearch + "/", false) + songArtistModel.asyncLoad() } } - - Component.onCompleted: { - mainView.currentlyWorking = true - delayInitModel.start() - } } diff --git a/app/ui/Artists.qml b/app/ui/Artists.qml index c8a3d644..f9de28e8 100644 --- a/app/ui/Artists.qml +++ b/app/ui/Artists.qml @@ -103,6 +103,15 @@ MusicPage { "pageTitle": i18n.tr("Artist") }) } + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false diff --git a/app/ui/Favorites.qml b/app/ui/Favorites.qml index 5dfe3ee4..8a0e0b4f 100644 --- a/app/ui/Favorites.qml +++ b/app/ui/Favorites.qml @@ -134,7 +134,6 @@ MusicPage { actions: [ Remove { onTriggered: { - mainView.currentlyWorking = true delayRemoveFavorite.start() } } @@ -185,7 +184,6 @@ MusicPage { onTriggered: { if (!player.removeFavorite(model.id)) popInfo.open(i18n.tr("Action can't be performed")); - mainView.currentlyWorking = false } } @@ -255,7 +253,6 @@ MusicPage { property QtObject model onTriggered: { player.playFavorite(model) // play favorite - mainView.currentlyWorking = false } } @@ -315,12 +312,10 @@ MusicPage { }) } else if (model.type === 5) { - mainView.currentlyWorking = true delayfavoriteClicked.model = model delayfavoriteClicked.start() } } else { - mainView.currentlyWorking = true delayfavoriteClicked.model = model delayfavoriteClicked.start() } diff --git a/app/ui/Genres.qml b/app/ui/Genres.qml index 25fe1d44..e745ad97 100644 --- a/app/ui/Genres.qml +++ b/app/ui/Genres.qml @@ -85,6 +85,7 @@ MusicPage { var root = modelItem.id + "/"; // register and load directory content for root childModel.init(Sonos, root, true); + childModel.resetModel(); var count = childModel.count; var index = 0; while (index < count && index < 4) { @@ -146,6 +147,15 @@ MusicPage { "line2": model.genre }) } + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false diff --git a/app/ui/Group.qml b/app/ui/Group.qml index 4a293db6..4eade268 100644 --- a/app/ui/Group.qml +++ b/app/ui/Group.qml @@ -44,8 +44,8 @@ Page { iconName: "back" objectName: "backAction" onTriggered: { - mainView.currentlyWorking = true - delayHandleGroupChanges.start() + if (handleUnjoinRooms()) + mainPageStack.pop(); } } ] @@ -76,41 +76,33 @@ Page { } } - Timer { - id: delayHandleGroupChanges - interval: 100 - onTriggered: { - var failure = false; - var items = []; - var indicies = groupList.getSelectedIndices(); - for (var i = 0; i < groupList.model.count; ++i) { - var keep = false; - for (var j = 0; j < indicies.length; j++) { - if (indicies[j] === i) { - keep = true; - break; - } - } - if (!keep) { - items.push(groupList.model.get(i)); + function handleUnjoinRooms() { + // keep back unselected rooms + var rooms = []; + var indicies = groupList.getSelectedIndices(); + var keep = false; + for (var i = 0; i < roomsModel.count; ++i) { + keep = false; + for (var j = 0; j < indicies.length; j++) { + if (indicies[j] === i) { + keep = true; + break; } } - if (items.length == 0) { - mainView.currentlyWorking = false; - } - else { - for (var i = 0; i < items.length; ++i) { - if (!Sonos.unjoinRoom(items[i].payload)) { - failure = true; - break; - } - } - // Zones will be reloaded on signal topologyChanged - // Signal is handled in MainView - mainView.currentlyWorking = false + if (!keep) { + rooms.push(roomsModel.get(i).payload); } - mainPageStack.pop() } + // start unjoin rooms + if (rooms.length > 0) { + if (!Sonos.startUnjoinRooms(rooms)) + return false; + } + return true; + } + + RoomsModel { + id: roomsModel } MultiSelectListView { @@ -121,12 +113,10 @@ Page { footer: Item { height: mainView.height - (styleMusic.common.expandHeight + groupList.currentHeight) + units.gu(8) } - model: RoomsModel { - } + model: roomsModel Component.onCompleted: { - model.init(Sonos, false) - model.load(zoneId) + roomsModel.load(Sonos, zoneId) selectAll() } diff --git a/app/ui/NoZoneState.qml b/app/ui/NoZoneState.qml index b783c73c..a273deb3 100644 --- a/app/ui/NoZoneState.qml +++ b/app/ui/NoZoneState.qml @@ -129,17 +129,7 @@ Page { width: parent.width onClicked: { - mainView.currentlyWorking = true - delayConnectSonos.start() - } - - Timer { - id: delayConnectSonos - interval: 100 - onTriggered: { - connectSonos() - mainView.currentlyWorking = false - } + connectSonos() } } } diff --git a/app/ui/NowPlaying.qml b/app/ui/NowPlaying.qml index 1db54330..3c55fbb6 100644 --- a/app/ui/NowPlaying.qml +++ b/app/ui/NowPlaying.qml @@ -140,7 +140,7 @@ MusicPage { }, MultiSelectHeadState { addToQueue: false - listview: queueLoader.item.listview + listview: queueLoader.status === Loader.Ready ? queueLoader.item.listview : null removable: true thisPage: nowPlaying thisHeader { diff --git a/app/ui/Playlists.qml b/app/ui/Playlists.qml index 269a2439..9980ebcf 100644 --- a/app/ui/Playlists.qml +++ b/app/ui/Playlists.qml @@ -114,6 +114,15 @@ MusicPage { "line2": model.title, }) } + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onLoaded: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false diff --git a/app/ui/Radios.qml b/app/ui/Radios.qml index ae694263..79ad459e 100644 --- a/app/ui/Radios.qml +++ b/app/ui/Radios.qml @@ -85,16 +85,6 @@ MusicPage { filterCaseSensitivity: Qt.CaseInsensitive } - Timer { - id: delayLoadModel - interval: 100 - onTriggered: { - AllRadiosModel.load(); - radiosPage.taintedView = false; // reset - mainView.currentlyWorking = false; - } - } - // Hack for autopilot otherwise Albums appears as MusicPage // due to bug 1341671 it is required that there is a property so that // qml doesn't optimise using the parent type @@ -177,9 +167,7 @@ MusicPage { } onItemClicked: { - mainView.currentlyWorking = true - delayRadioClicked.model = model - delayRadioClicked.start() + radioClicked(model) // play radio } } @@ -227,10 +215,17 @@ MusicPage { ""}] onClicked: { - mainView.currentlyWorking = true - delayRadioClicked.model = model - delayRadioClicked.start() + radioClicked(model) // play radio } + + // check favorite on data updated + Connections { + target: AllFavoritesModel + onLoaded: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false; @@ -247,14 +242,5 @@ MusicPage { } } - Timer { - id: delayRadioClicked - interval: 100 - property QtObject model - onTriggered: { - radioClicked(model) // play radio - mainView.currentlyWorking = false - } - } } diff --git a/app/ui/Service.qml b/app/ui/Service.qml index 32605f06..3eef05a7 100644 --- a/app/ui/Service.qml +++ b/app/ui/Service.qml @@ -31,11 +31,13 @@ MusicPage { id: servicePage objectName: "servicePage" + property bool isListView: false property var serviceItem: null property bool loaded: false // used to detect difference between first and further loads property bool isRoot: mediaModel.isRoot property int displayType: 3 // display type for root - property bool isListView: false + property int parentDisplayType: 0 + property bool focusViewIndex: false // the model handles search property alias searchableModel: mediaModel @@ -77,59 +79,54 @@ MusicPage { art: serviceItem.id === "SA_RINCON65031_" ? Qt.resolvedUrl("../graphics/tunein.png") : serviceItem.icon } + property alias model: mediaModel // used in ServiceHeadState + MediaModel { id: mediaModel } - Timer { - id: delayInitModel - interval: 100 - onTriggered: { - mediaModel.init(Sonos, serviceItem.payload, true) - mainView.currentlyWorking = false - servicePage.loaded = true; - } - } - - Timer { - id: delayLoadModel - interval: 100 - onTriggered: { - mediaModel.load(); - servicePage.taintedView = false; // reset - mainView.currentlyWorking = false; - } - } - - Timer { - id: delayLoadMore - interval: 100 - onTriggered: { - mediaModel.loadMore() - mainView.currentlyWorking = false - } - } - - Timer { - id: delayLoadRootModel - interval: 100 - onTriggered: { - mediaModel.loadRoot(); - servicePage.taintedView = false; // reset - mainView.currentlyWorking = false; + function restoreFocusViewIndex() { + var idx = mediaModel.viewIndex() + if (mediaModel.count <= idx) { + mediaModel.asyncLoadMore() // load more !!! + } else { + focusViewIndex = false; + mediaList.positionViewAtIndex(idx, ListView.Center); + mediaGrid.positionViewAtIndex(idx, GridView.Center); } } Connections { target: mediaModel - onDataUpdated: { - mainView.currentlyWorking = true - delayLoadModel.start() + onDataUpdated: mediaModel.asyncLoad() + onLoaded: { + if (succeeded) { + mediaModel.resetModel() + servicePage.displayType = servicePage.parentDisplayType // apply displayType + servicePage.taintedView = false // reset + if (focusViewIndex) { + // restore index position in view + restoreFocusViewIndex() + } else { + mediaList.positionViewAtIndex(0, ListView.Top); + mediaGrid.positionViewAtIndex(0, GridView.Top); + } + } + } + onLoadedMore: { + if (succeeded) { + mediaModel.appendModel() + if (focusViewIndex) { + // restore index position in view + restoreFocusViewIndex() + } + } else if (focusViewIndex) { + focusViewIndex = false; + mediaList.positionViewAtEnd(); + mediaGrid.positionViewAtEnd(); + } } - } - Connections { - target: mediaModel onPathChanged: { if (mediaModel.isRoot) { pageTitle = serviceItem.title; @@ -207,7 +204,7 @@ MusicPage { : model.canPlay && !model.canQueue ? Qt.resolvedUrl("../graphics/radio.png") : Qt.resolvedUrl("../graphics/no_cover.png") - imageSource: model.art !== "" ? model.art + imageSource: model.art !== undefined && model.art.length > 0 ? model.art : model.type === 2 ? Qt.resolvedUrl("../graphics/none.png") : model.canPlay && !model.canQueue ? Qt.resolvedUrl("../graphics/radio.png") : Qt.resolvedUrl("../graphics/no_cover.png") @@ -272,8 +269,8 @@ MusicPage { onAtYEndChanged: { if (mediaList.atYEnd && mediaModel.totalCount > mediaModel.count) { - mainView.currentlyWorking = true - delayLoadMore.start() + focusViewIndex = true; + mediaModel.asyncLoadMore() } } } @@ -297,7 +294,7 @@ MusicPage { delegate: Card { id: favoriteCard primaryText: model.title - secondaryText: model.description.length > 0 ? model.description + secondaryText: model.description !== undefined && model.description.length > 0 ? model.description : model.type === 1 ? model.artist.length > 0 ? model.artist : i18n.tr("Album") : model.type === 2 ? i18n.tr("Artist") : model.type === 3 ? i18n.tr("Genre") @@ -318,6 +315,15 @@ MusicPage { : [{art: Qt.resolvedUrl("../graphics/no_cover.png")}] onClicked: clickItem(model) + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (model.canPlay) { if (isFavorite && removeFromFavorites(model.payload)) @@ -336,89 +342,42 @@ MusicPage { onAtYEndChanged: { if (mediaGrid.atYEnd && mediaModel.totalCount > mediaModel.count) { - mainView.currentlyWorking = true - delayLoadMore.start() + mediaModel.asyncLoadMore() } } } Component.onCompleted: { - mainView.currentlyWorking = true; - delayInitModel.start(); - } - - Timer { - id: delayGoUp - interval: 100 - onTriggered: { - // change view depending of parent display type - servicePage.displayType = mediaModel.parentDisplayType(); - mediaModel.loadParent(); - // restore index position in view - var idx = mediaModel.viewIndex(); - while (mediaModel.count <= idx && mediaModel.loadMore()); - if (idx < mediaModel.count) { - mediaList.positionViewAtIndex(idx, ListView.Center); - mediaGrid.positionViewAtIndex(idx, GridView.Center); - } else { - mediaList.positionViewAtEnd(); - mediaGrid.positionViewAtEnd(); - } - mainView.currentlyWorking = false; - } + mediaModel.init(Sonos, serviceItem.payload, false) + mediaModel.asyncLoad() } function goUp() { - mainView.currentlyWorking = true - delayGoUp.start() - } - - Timer { - id: delayMediaClicked - interval: 100 - property QtObject model - onTriggered: { - if (model.isContainer) { - var pdt = servicePage.displayType; - servicePage.displayType = model.displayType; - mediaModel.loadChild(model.id, model.title, pdt, model.index); - mediaList.positionViewAtIndex(0, ListView.Top); - mediaGrid.positionViewAtIndex(0, GridView.Top); - } else if (model.canPlay) { - if (model.canQueue) - trackClicked(model); - else - radioClicked(model); - } - mainView.currentlyWorking = false - } + // change view depending of parent display type + servicePage.parentDisplayType = mediaModel.parentDisplayType(); + focusViewIndex = true; + mediaModel.asyncLoadParent(); } function clickItem(model) { - mainView.currentlyWorking = true - delayMediaClicked.model = model - delayMediaClicked.start() - } - - Timer { - id: delayPlayMedia - interval: 100 - property QtObject model - onTriggered: { - if (model.canPlay) { - if (model.canQueue) - trackClicked(model); - else - radioClicked(model); - } - mainView.currentlyWorking = false + if (model.isContainer) { + servicePage.parentDisplayType = model.displayType; + mediaModel.asyncLoadChild(model.id, model.title, servicePage.displayType, model.index); + } else if (model.canPlay) { + if (model.canQueue) + trackClicked(model); + else + radioClicked(model); } } function playItem(model) { - mainView.currentlyWorking = true - delayPlayMedia.model = model - delayPlayMedia.start() + if (model.canPlay) { + if (model.canQueue) + trackClicked(model); + else + radioClicked(model); + } } //////////////////////////////////////////////////////////////////////////// @@ -446,27 +405,26 @@ MusicPage { if (mediaModel.isAuthExpired) { if (mediaModel.policyAuth == 1) { if (!loginService.active) { - mediaModel.clear(); // first try with saved login/password var auth = mediaModel.getDeviceAuth(); if (auth.key.length === 0 || mediaModel.requestSessionId(mediaModel.username, auth.key) === 0) loginService.active = true; // show login registration else { // refresh the model - delayLoadModel.start(); + mediaModel.asyncLoad(); } } } else if (mediaModel.policyAuth == 2 || mediaModel.policyAuth == 3) { if (registeringService.active) registeringService.active = false; // restart new registration else - mediaModel.clear(); + mediaModel.clearData(); registeringService.active = true; } } else { loginService.active = false; registeringService.active = false; - mainView.currentlyWorking = true; + mainView.jobRunning = true; // save new incarnation of accounts settings var auth = mediaModel.getDeviceAuth(); var acls = deserializeACLS(startupSettings.accounts); @@ -480,7 +438,7 @@ MusicPage { _acls.push({type: auth.type, sn: auth.serialNum, key: auth.key, token: auth.token}); startupSettings.accounts = serializeACLS(_acls); // refresh the model - delayLoadModel.start(); + mediaModel.asyncLoad(); } } } diff --git a/app/ui/SongsView.qml b/app/ui/SongsView.qml index affe0705..1395a3a7 100644 --- a/app/ui/SongsView.qml +++ b/app/ui/SongsView.qml @@ -35,7 +35,7 @@ MusicPage { id: songStackPage objectName: "songsPage" visible: false - pageFlickable: albumtrackslist + pageFlickable: songList property string line1: "" property string line2: "" @@ -51,13 +51,11 @@ MusicPage { property string artist: "" property string genre: "" - property bool loaded: false // used to detect difference between first and further loads - width: mainPageStack.width property bool isFavorite: false - state: albumtrackslist.state === "multiselectable" ? "selection" : (isPlaylist ? "playlist" : "album") + state: songList.state === "multiselectable" ? "selection" : (isPlaylist ? "playlist" : "album") states: [ AlbumSongsHeadState { thisPage: songStackPage @@ -73,7 +71,7 @@ MusicPage { }, MultiSelectHeadState { containerItem: songStackPage.containerItem - listview: albumtrackslist + listview: songList removable: isPlaylist thisPage: songStackPage thisHeader { @@ -81,7 +79,6 @@ MusicPage { } onRemoved: { - mainView.currentlyWorking = false delayRemoveSelectedFromPlaylist.selectedIndices = selectedIndices delayRemoveSelectedFromPlaylist.start() } @@ -93,47 +90,57 @@ MusicPage { interval: 100 property var selectedIndices: [] onTriggered: { - var cnt = songsModel.count; + songList.focusIndex = selectedIndices[selectedIndices.length-1]; if (removeTracksFromPlaylist(containerItem.id, selectedIndices, songsModel.containerUpdateID())) { - songsModel.load(); - while (songsModel.count < cnt && songsModel.loadMore()); + songsModel.asyncLoad(); } - mainView.currentlyWorking = false } } TracksModel { id: songsModel + onDataUpdated: songsModel.asyncLoad() + onLoaded: songsModel.resetModel() + Component.onCompleted: { + songsModel.init(Sonos, songSearch, false) + songsModel.asyncLoad() + } } - Timer { - id: delayInitModel - interval: 100 - onTriggered: { - isFavorite = (AllFavoritesModel.findFavorite(containerItem.payload).length > 0) - songsModel.init(Sonos, songSearch, true) - mainView.currentlyWorking = false - songStackPage.loaded = true; + function restoreFocusIndex() { + if (songsModel.count <= songList.focusIndex) { + songsModel.asyncLoadMore() // load more !!! + } else { + songList.positionViewAtIndex(songList.focusIndex, ListView.Center); + songList.focusIndex = -1 } } Connections { target: songsModel - onDataUpdated: { - mainView.currentlyWorking = true - delayLoadTrackModel.start() + onDataUpdated: songsModel.asyncLoad() + onLoaded: { + songsModel.resetModel() + if (succeeded) { + if (songList.focusIndex > 0) { + // restore index position in view + restoreFocusIndex() + } + } } - } - - Timer { - id: delayLoadTrackModel - interval: 100 - onTriggered: { - var cnt = songsModel.count; - songsModel.load(); - while (songsModel.count < cnt && songsModel.loadMore()); - mainView.currentlyWorking = false + onLoadedMore: { + if (succeeded) { + songsModel.appendModel(); + if (songList.focusIndex > 0) { + // restore index position in view + restoreFocusIndex() + } + } else if (songList.focusIndex > 0) { + songList.positionViewAtEnd(); + songList.focusIndex = -1; + } } + } Repeater { @@ -161,7 +168,7 @@ MusicPage { } MultiSelectListView { - id: albumtrackslist + id: songList anchors { fill: parent } @@ -171,7 +178,7 @@ MusicPage { rightColumn: Column { spacing: units.gu(2) ShuffleButton { - model: albumtrackslist.model + model: songList.model width: blurredHeader.width > units.gu(60) ? units.gu(23.5) : (blurredHeader.width - units.gu(13)) / 2 } QueueAllButton { @@ -256,11 +263,13 @@ MusicPage { model: songsModel + property int focusIndex: 0 + delegate: MusicListItem { id: track color: "transparent" - noCover: !songStackPage.isAlbum ? Qt.resolvedUrl("../graphics/no_cover.png") : "" - imageSource: !songStackPage.isAlbum ? makeCoverSource(model.art, model.author, model.album) : "" + noCover: Qt.resolvedUrl("../graphics/no_cover.png") + imageSource: !songStackPage.isAlbum ? makeCoverSource(model.art, model.author, model.album) : noCover column: Column { Label { id: trackTitle @@ -305,7 +314,6 @@ MusicPage { actions: [ Remove { onTriggered: { - mainView.currentlyWorking = true delayRemoveTrackFromPlaylist.start() } } @@ -317,12 +325,10 @@ MusicPage { id: delayRemoveTrackFromPlaylist interval: 100 onTriggered: { - var cnt = songsModel.count; + songList.focusIndex = index > 0 ? index - 1 : 0; if (removeTracksFromPlaylist(containerItem.id, [index], songsModel.containerUpdateID())) { - songsModel.load(); - while (songsModel.count < cnt && songsModel.loadMore()); + songsModel.asyncLoad(); } - mainView.currentlyWorking = false } } @@ -334,7 +340,9 @@ MusicPage { } onReorder: { - mainView.currentlyWorking = true + customdebug("Reorder queue item " + from + " to " + to); + songList.focusIndex = to + mainView.jobRunning = true delayReorderTrackInPlaylist.argFrom = from delayReorderTrackInPlaylist.argTo = to delayReorderTrackInPlaylist.start() @@ -347,32 +355,33 @@ MusicPage { property int argTo: 0 onTriggered: { if (reorderTrackInPlaylist(containerItem.id, argFrom, argTo, songsModel.containerUpdateID())) { - songsModel.load(); + songsModel.asyncLoad(); } - mainView.currentlyWorking = false } } onAtYEndChanged: { - if (albumtrackslist.atYEnd && songsModel.totalCount > songsModel.count) { - mainView.currentlyWorking = true - delayLoadMoreTracks.start() + if (songList.atYEnd && songsModel.totalCount > songsModel.count) { + songsModel.asyncLoadMore() } } - Timer { - id: delayLoadMoreTracks - interval: 100 - onTriggered: { - songsModel.loadMore() - mainView.currentlyWorking = false - } - } + } + + Scrollbar { + flickableItem: songList + align: Qt.AlignTrailing + } + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(containerItem.payload).length > 0) + } } Component.onCompleted: { - mainView.currentlyWorking = true - delayInitModel.start() + isFavorite = (AllFavoritesModel.findFavorite(containerItem.payload).length > 0) } } diff --git a/app/ui/Zones.qml b/app/ui/Zones.qml index 0f18eac7..c3d67542 100644 --- a/app/ui/Zones.qml +++ b/app/ui/Zones.qml @@ -93,8 +93,7 @@ BottomEdgePage { text: i18n.tr("Reload zones") visible: true onTriggered: { - mainView.currentlyWorking = true - delayResetController.start() + connectSonos() } }, Action { @@ -134,12 +133,9 @@ BottomEdgePage { iconName: "back" onTriggered: { if (zoneList.getSelectedIndices().length > 1) { - mainView.currentlyWorking = true - delayJoinZones.start() - } - else { - zoneList.closeSelection() + handleJoinZones() } + zoneList.closeSelection() } } ] @@ -177,27 +173,6 @@ BottomEdgePage { } ] - Timer { - id: delayResetController - interval: 100 - onTriggered: { - connectSonos() - // activity indicator will be hidden after finished loading - } - } - - Timer { - id: delayJoinZones - interval: 100 - onTriggered: { - handleJoinZones() - zoneList.closeSelection() - // Zones will be reloaded on signal topologyChanged - // Signal is handled in MainView - mainView.currentlyWorking = false - } - } - function handleJoinZones() { var indicies = zoneList.getSelectedIndices(); // get current as master @@ -255,8 +230,7 @@ BottomEdgePage { Clear { visible: model.isGroup onTriggered: { - mainView.currentlyWorking = true - delayUnjoinZone.start() + Sonos.unjoinZone(model.payload) } } ] @@ -283,40 +257,7 @@ BottomEdgePage { } onItemClicked: { - mainView.currentlyWorking = true - delayChangeZone.start() - } - - Timer { - id: delayChangeZone - interval: 100 - onTriggered: { - if (currentZone !== model.name) { - customdebug("Connecting zone '" + name + "'"); - if ((Sonos.connectZone(model.name) || Sonos.connectZone("")) && player.connect()) { - currentZone = Sonos.getZoneName(); - currentZoneTag = Sonos.getZoneShortName(); - if (noZone) - noZone = false; - } - else { - if (!noZone) - noZone = true; - } - } - mainView.currentlyWorking = false - } - } - - Timer { - id: delayUnjoinZone - interval: 100 - onTriggered: { - Sonos.unjoinZone(model.payload) - // Zones will be reloaded on signal topologyChanged - // Signal is handled in MainView - mainView.currentlyWorking = false - } + connectZone(model.name) } onSelectedChanged: { @@ -357,6 +298,5 @@ BottomEdgePage { } } } - } } From 23b0046008409483d9390dee8dbe49b367f102a4 Mon Sep 17 00:00:00 2001 From: janbar Date: Sun, 14 Jan 2018 01:43:04 +0100 Subject: [PATCH 9/9] bump version 2.5 --- CMakeLists.txt | 2 +- debian/changelog | 6 +- po/noson.janbar.pot | 150 +++++++++++++++++++++++--------------------- 3 files changed, 83 insertions(+), 75 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e1fb574..bb0a63df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ set(QT_IMPORTS_DIR "lib/${ARCH_TRIPLET}") option(INSTALL_TESTS "Install the tests on make install" on) option(CLICK_MODE "Installs to a contained location" on) -set(APP_VERSION 2.4.13) +set(APP_VERSION 2.5.0) set(APP_NAME noson) set(APP_ID "noson.janbar") set(MAIN_QML "Main.qml") diff --git a/debian/changelog b/debian/changelog index fc0b9ff1..5bc900b8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,8 @@ -noson-app (2.4) UNRELEASED; urgency=low +noson-app (2.5) UNRELEASED; urgency=low + + [ janbar ] + * Release 2.5 + * Refactor for latest backend [ janbar ] * Release 2.4 diff --git a/po/noson.janbar.pot b/po/noson.janbar.pot index f11ac128..1da2267e 100644 --- a/po/noson.janbar.pot +++ b/po/noson.janbar.pot @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the package. +# This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-03 01:59+0200\n" +"POT-Creation-Date: 2018-01-14 00:40+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,53 +18,54 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: ../app/Main.qml:88 +#: ../app/Main.qml:89 msgid "Noson" msgstr "" -#: ../app/Main.qml:252 +#: ../app/Main.qml:151 msgid "Index loaded" msgstr "" -#: ../app/Main.qml:380 ../app/Main.qml:479 ../app/Main.qml:498 -#: ../app/Main.qml:506 ../app/Main.qml:516 ../app/Main.qml:521 -#: ../app/Main.qml:553 ../app/Main.qml:567 ../app/Main.qml:579 -#: ../app/Main.qml:590 ../app/Main.qml:598 ../app/Main.qml:608 -#: ../app/Main.qml:618 ../app/Main.qml:625 ../app/Main.qml:632 -#: ../app/Main.qml:641 ../app/Main.qml:650 ../app/Main.qml:658 -#: ../app/Main.qml:665 ../app/Main.qml:672 ../app/Main.qml:682 -#: ../app/components/Dialog/DialogSelectSource.qml:94 -#: ../app/components/Dialog/DialogSelectSource.qml:105 -#: ../app/components/Dialog/DialogSelectSource.qml:116 -#: ../app/ui/AddToPlaylist.qml:141 ../app/ui/Favorites.qml:186 +#: ../app/Main.qml:445 ../app/Main.qml:526 ../app/Main.qml:545 +#: ../app/Main.qml:553 ../app/Main.qml:563 ../app/Main.qml:568 +#: ../app/Main.qml:600 ../app/Main.qml:614 ../app/Main.qml:626 +#: ../app/Main.qml:637 ../app/Main.qml:645 ../app/Main.qml:655 +#: ../app/Main.qml:665 ../app/Main.qml:672 ../app/Main.qml:679 +#: ../app/Main.qml:688 ../app/Main.qml:697 ../app/Main.qml:705 +#: ../app/Main.qml:712 ../app/Main.qml:719 ../app/Main.qml:729 +#: ../app/components/Dialog/DialogSelectSource.qml:96 +#: ../app/components/Dialog/DialogSelectSource.qml:107 +#: ../app/components/Dialog/DialogSelectSource.qml:118 +#: ../app/components/Player.qml:308 ../app/ui/AddToPlaylist.qml:141 +#: ../app/ui/Favorites.qml:186 msgid "Action can't be performed" msgstr "" -#: ../app/Main.qml:397 +#: ../app/Main.qml:462 msgid "Debug: " msgstr "" -#: ../app/Main.qml:476 +#: ../app/Main.qml:523 msgid "Refreshing of index is running" msgstr "" -#: ../app/Main.qml:503 ../app/Main.qml:564 ../app/Main.qml:576 -#: ../app/Main.qml:638 ../app/ui/AddToPlaylist.qml:138 +#: ../app/Main.qml:550 ../app/Main.qml:611 ../app/Main.qml:623 +#: ../app/Main.qml:685 ../app/ui/AddToPlaylist.qml:138 msgid "song added" msgstr "" -#: ../app/Main.qml:550 +#: ../app/Main.qml:597 #, qt-format msgid "%1 song added" msgid_plural "%1 songs added" msgstr[0] "" msgstr[1] "" -#: ../app/Main.qml:587 +#: ../app/Main.qml:634 msgid "Queue cleared" msgstr "" -#: ../app/Main.qml:647 +#: ../app/Main.qml:694 #, qt-format msgid "%1 song removed" msgid_plural "%1 songs removed" @@ -127,7 +128,7 @@ msgid "Close the queue management screen." msgstr "" #: ../app/components/Dialog/DialogManageQueue.qml:93 -#: ../app/components/Dialog/DialogSelectSource.qml:132 +#: ../app/components/Dialog/DialogSelectSource.qml:134 #: ../app/components/Dialog/DialogSettings.qml:54 #: ../app/components/Dialog/DialogSleepTimer.qml:104 msgid "Close" @@ -176,7 +177,8 @@ msgstr "" #. TRANSLATORS: this is the name of the playlists page shown in the tab header. #. Remember to keep the translation short to fit the screen width -#: ../app/components/Dialog/DialogSearchMusic.qml:48 ../app/ui/Playlists.qml:35 +#: ../app/components/Dialog/DialogSearchMusic.qml:48 +#: ../app/ui/Playlists.qml:35 msgid "Playlists" msgstr "" @@ -196,7 +198,7 @@ msgstr "" msgid "Composers" msgstr "" -#: ../app/components/Dialog/DialogSearchMusic.qml:95 ../app/ui/Service.qml:139 +#: ../app/components/Dialog/DialogSearchMusic.qml:95 ../app/ui/Service.qml:136 msgid "Search" msgstr "" @@ -207,41 +209,42 @@ msgstr "" msgid "Select source" msgstr "" -#: ../app/components/Dialog/DialogSelectSource.qml:41 +#: ../app/components/Dialog/DialogSelectSource.qml:42 msgid "Enter stream URL" msgstr "" -#: ../app/components/Dialog/DialogSelectSource.qml:47 -msgid "Play stream" +#: ../app/components/Dialog/DialogSelectSource.qml:51 +#: ../app/components/Dialog/DialogSelectSource.qml:69 +msgid "Playing failed." msgstr "" -#: ../app/components/Dialog/DialogSelectSource.qml:57 -msgid "Please type in an URL." +#: ../app/components/Dialog/DialogSelectSource.qml:58 +msgid "Play stream" msgstr "" -#: ../app/components/Dialog/DialogSelectSource.qml:72 -msgid "Playing failed." +#: ../app/components/Dialog/DialogSelectSource.qml:76 +msgid "Please type in an URL." msgstr "" -#: ../app/components/Dialog/DialogSelectSource.qml:82 +#: ../app/components/Dialog/DialogSelectSource.qml:84 msgid "Select the audio input." msgstr "" -#: ../app/components/Dialog/DialogSelectSource.qml:90 +#: ../app/components/Dialog/DialogSelectSource.qml:92 msgid "Play line IN" msgstr "" -#: ../app/components/Dialog/DialogSelectSource.qml:101 +#: ../app/components/Dialog/DialogSelectSource.qml:103 msgid "Play TV" msgstr "" #. TRANSLATORS: this appears in the header with limited space (around 20 characters) -#: ../app/components/Dialog/DialogSelectSource.qml:112 +#: ../app/components/Dialog/DialogSelectSource.qml:114 #: ../app/ui/NowPlaying.qml:38 msgid "Queue" msgstr "" -#: ../app/components/Dialog/DialogSelectSource.qml:125 +#: ../app/components/Dialog/DialogSelectSource.qml:127 msgid "Close this screen." msgstr "" @@ -312,14 +315,14 @@ msgid "6 hours" msgstr "" #: ../app/components/Dialog/DialogSongInfo.qml:38 ../app/ui/Albums.qml:84 -#: ../app/ui/Albums.qml:108 ../app/ui/ArtistView.qml:174 -#: ../app/ui/ArtistView.qml:189 ../app/ui/Favorites.qml:274 +#: ../app/ui/Albums.qml:108 ../app/ui/ArtistView.qml:181 +#: ../app/ui/ArtistView.qml:196 ../app/ui/Favorites.qml:273 msgid "Unknown Album" msgstr "" #: ../app/components/Dialog/DialogSongInfo.qml:39 ../app/ui/Albums.qml:85 #: ../app/ui/Albums.qml:107 ../app/ui/ArtistView.qml:94 -#: ../app/ui/ArtistView.qml:188 ../app/ui/Artists.qml:84 +#: ../app/ui/ArtistView.qml:195 ../app/ui/Artists.qml:84 msgid "Unknown Artist" msgstr "" @@ -333,19 +336,19 @@ msgid "Cancel selection" msgstr "" #: ../app/components/HeadState/MultiSelectHeadState.qml:49 -#: ../app/ui/Group.qml:58 ../app/ui/Zones.qml:152 +#: ../app/ui/Group.qml:58 ../app/ui/Zones.qml:148 msgid "Select All" msgstr "" #: ../app/components/HeadState/MultiSelectHeadState.qml:63 #: ../app/components/ListItemActions/AddToQueue.qml:27 -#: ../app/ui/Favorites.qml:162 ../app/ui/Service.qml:229 +#: ../app/ui/Favorites.qml:162 ../app/ui/Service.qml:226 msgid "Add to queue" msgstr "" #: ../app/components/HeadState/MultiSelectHeadState.qml:85 #: ../app/components/ListItemActions/AddToPlaylist.qml:27 -#: ../app/ui/Favorites.qml:169 ../app/ui/Service.qml:236 +#: ../app/ui/Favorites.qml:169 ../app/ui/Service.qml:233 msgid "Add to playlist" msgstr "" @@ -366,15 +369,15 @@ msgid "Show list" msgstr "" #: ../app/components/ListItemActions/AddToFavorites.qml:29 -#: ../app/ui/Radios.qml:160 ../app/ui/Service.qml:248 +#: ../app/ui/Radios.qml:150 ../app/ui/Service.qml:245 msgid "Favorite" msgstr "" -#: ../app/components/ListItemActions/Clear.qml:25 ../app/ui/Zones.qml:152 +#: ../app/components/ListItemActions/Clear.qml:25 ../app/ui/Zones.qml:148 msgid "Clear" msgstr "" -#: ../app/components/ListItemActions/PlaySong.qml:24 ../app/ui/Service.qml:222 +#: ../app/components/ListItemActions/PlaySong.qml:24 ../app/ui/Service.qml:219 msgid "Play" msgstr "" @@ -399,7 +402,7 @@ msgstr "" msgid "Now playing" msgstr "" -#: ../app/components/Player.qml:263 +#: ../app/components/Player.qml:269 msgid "Untitled" msgstr "" @@ -414,9 +417,9 @@ msgid "" "every mood and occasion." msgstr "" -#: ../app/components/Queue.qml:95 ../app/ui/Favorites.qml:129 -#: ../app/ui/Favorites.qml:225 ../app/ui/Service.qml:180 -#: ../app/ui/Service.qml:305 ../app/ui/SongsView.qml:292 +#: ../app/components/Queue.qml:94 ../app/ui/Favorites.qml:129 +#: ../app/ui/Favorites.qml:224 ../app/ui/Service.qml:177 +#: ../app/ui/Service.qml:302 ../app/ui/SongsView.qml:301 msgid "Song" msgstr "" @@ -486,10 +489,10 @@ msgstr "" msgid "Select playlist" msgstr "" -#: ../app/ui/Albums.qml:106 ../app/ui/Albums.qml:114 -#: ../app/ui/ArtistView.qml:187 ../app/ui/Favorites.qml:125 -#: ../app/ui/Favorites.qml:221 ../app/ui/Favorites.qml:272 -#: ../app/ui/Service.qml:176 ../app/ui/Service.qml:301 +#: ../app/ui/Albums.qml:106 ../app/ui/Albums.qml:123 +#: ../app/ui/ArtistView.qml:194 ../app/ui/Favorites.qml:125 +#: ../app/ui/Favorites.qml:220 ../app/ui/Favorites.qml:271 +#: ../app/ui/Service.qml:173 ../app/ui/Service.qml:298 msgid "Album" msgstr "" @@ -500,18 +503,18 @@ msgid_plural "%1 albums" msgstr[0] "" msgstr[1] "" -#: ../app/ui/ArtistView.qml:134 ../app/ui/SongsView.qml:246 -#: ../app/ui/SongsView.qml:247 +#: ../app/ui/ArtistView.qml:134 ../app/ui/SongsView.qml:253 +#: ../app/ui/SongsView.qml:254 #, qt-format msgid "%1 song" msgid_plural "%1 songs" msgstr[0] "" msgstr[1] "" -#: ../app/ui/Artists.qml:103 ../app/ui/Artists.qml:109 -#: ../app/ui/Favorites.qml:126 ../app/ui/Favorites.qml:222 -#: ../app/ui/Favorites.qml:284 ../app/ui/Service.qml:177 -#: ../app/ui/Service.qml:302 +#: ../app/ui/Artists.qml:103 ../app/ui/Artists.qml:118 +#: ../app/ui/Favorites.qml:126 ../app/ui/Favorites.qml:221 +#: ../app/ui/Favorites.qml:283 ../app/ui/Service.qml:174 +#: ../app/ui/Service.qml:299 msgid "Artist" msgstr "" @@ -519,24 +522,25 @@ msgstr "" msgid "Favorites" msgstr "" -#: ../app/ui/Favorites.qml:127 ../app/ui/Favorites.qml:223 -#: ../app/ui/Favorites.qml:295 ../app/ui/Genres.qml:144 -#: ../app/ui/Genres.qml:152 ../app/ui/Service.qml:178 ../app/ui/Service.qml:303 +#: ../app/ui/Favorites.qml:127 ../app/ui/Favorites.qml:222 +#: ../app/ui/Favorites.qml:294 ../app/ui/Genres.qml:145 +#: ../app/ui/Genres.qml:162 ../app/ui/Service.qml:175 +#: ../app/ui/Service.qml:300 msgid "Genre" msgstr "" -#: ../app/ui/Favorites.qml:128 ../app/ui/Favorites.qml:224 -#: ../app/ui/Favorites.qml:310 ../app/ui/Playlists.qml:112 -#: ../app/ui/Playlists.qml:120 ../app/ui/Service.qml:179 -#: ../app/ui/Service.qml:304 +#: ../app/ui/Favorites.qml:128 ../app/ui/Favorites.qml:223 +#: ../app/ui/Favorites.qml:309 ../app/ui/Playlists.qml:112 +#: ../app/ui/Playlists.qml:129 ../app/ui/Service.qml:176 +#: ../app/ui/Service.qml:301 msgid "Playlist" msgstr "" -#: ../app/ui/Genres.qml:107 +#: ../app/ui/Genres.qml:108 msgid "" msgstr "" -#: ../app/ui/Group.qml:32 ../app/ui/Zones.qml:272 +#: ../app/ui/Group.qml:32 ../app/ui/Zones.qml:246 msgid "Group" msgstr "" @@ -568,8 +572,8 @@ msgstr "" msgid "My radios" msgstr "" -#: ../app/ui/Radios.qml:155 ../app/ui/Radios.qml:237 ../app/ui/Service.qml:181 -#: ../app/ui/Service.qml:306 +#: ../app/ui/Radios.qml:145 ../app/ui/Radios.qml:232 ../app/ui/Service.qml:178 +#: ../app/ui/Service.qml:303 msgid "Radio" msgstr "" @@ -581,10 +585,10 @@ msgstr "" msgid "Reload zones" msgstr "" -#: ../app/ui/Zones.qml:103 +#: ../app/ui/Zones.qml:102 msgid "Create group" msgstr "" -#: /home/jlb/src/build-noson-app/po/noson.desktop.in.h:1 +#: /home/jlb/src/janbar/QML/build-noson-app-armhf-15-release/po/noson.desktop.in.h:1 msgid "Sonos music" msgstr ""