diff --git a/app/app.pro b/app/app.pro index d1378e08..e28c3c84 100644 --- a/app/app.pro +++ b/app/app.pro @@ -58,7 +58,8 @@ SOURCES += \ streaming/session.cpp \ streaming/audio.cpp \ streaming/video.cpp \ - gui/computermodel.cpp + gui/computermodel.cpp \ + gui/appmodel.cpp HEADERS += \ utils.h \ @@ -70,7 +71,8 @@ HEADERS += \ settings/streamingpreferences.h \ streaming/input.hpp \ streaming/session.hpp \ - gui/computermodel.h + gui/computermodel.h \ + gui/appmodel.h RESOURCES += \ resources.qrc \ diff --git a/app/backend/boxartmanager.cpp b/app/backend/boxartmanager.cpp index 2a4b1de1..cc24dfd9 100644 --- a/app/backend/boxartmanager.cpp +++ b/app/backend/boxartmanager.cpp @@ -7,8 +7,7 @@ BoxArtManager::BoxArtManager(QObject *parent) : QObject(parent), m_BoxArtDir( - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart"), - m_PlaceholderImage(":/res/no_app_image.png") + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart") { if (!m_BoxArtDir.exists()) { m_BoxArtDir.mkpath("."); @@ -32,16 +31,12 @@ BoxArtManager::getFilePathForBoxArt(NvComputer* computer, int appId) return dir.filePath(QString::number(appId) + ".png"); } -QImage BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app) +QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app) { // Try to open the cached file - QFile cacheFile(getFilePathForBoxArt(computer, app.id)); - if (cacheFile.open(QFile::ReadOnly)) { - // Return what we have if it's a valid image - QImage image = QImageReader(&cacheFile).read(); - if (!image.isNull()) { - return image; - } + QString cacheFilePath = getFilePathForBoxArt(computer, app.id); + if (QFile::exists(cacheFilePath)) { + return QUrl::fromLocalFile(cacheFilePath); } // If we get here, we need to fetch asynchronously. @@ -51,18 +46,22 @@ QImage BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app) // Return the placeholder then we can notify the caller // later when the real image is ready. - return m_PlaceholderImage; + return QUrl("qrc:/res/no_app_image.png"); } -void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QImage image) +void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image) { + if (image.isEmpty()) { + image = QUrl("qrc:/res/no_app_image.png"); + } emit boxArtLoadComplete(computer, app, image); } -QImage BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId) +QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId) { NvHTTP http(computer->activeAddress); + QString cachePath = getFilePathForBoxArt(computer, appId); QImage image; try { image = http.getBoxArt(appId); @@ -70,8 +69,10 @@ QImage BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId) // Cache the box art on disk if it loaded if (!image.isNull()) { - image.save(getFilePathForBoxArt(computer, appId)); + if (image.save(cachePath)) { + return QUrl::fromLocalFile(cachePath); + } } - return image; + return QUrl(); } diff --git a/app/backend/boxartmanager.h b/app/backend/boxartmanager.h index 5515987a..6c6c7a48 100644 --- a/app/backend/boxartmanager.h +++ b/app/backend/boxartmanager.h @@ -15,28 +15,27 @@ class BoxArtManager : public QObject public: explicit BoxArtManager(QObject *parent = nullptr); - QImage + QUrl loadBoxArt(NvComputer* computer, NvApp& app); signals: void - boxArtLoadComplete(NvComputer* computer, NvApp app, QImage image); + boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image); public slots: private slots: void - handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QImage image); + handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image); private: - QImage + QUrl loadBoxArtFromNetwork(NvComputer* computer, int appId); QString getFilePathForBoxArt(NvComputer* computer, int appId); QDir m_BoxArtDir; - QImage m_PlaceholderImage; }; class NetworkBoxArtLoadTask : public QObject, public QRunnable @@ -49,18 +48,18 @@ public: m_Computer(computer), m_App(app) { - connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QImage)), - boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QImage))); + connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)), + boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl))); } signals: - void boxArtFetchCompleted(NvComputer* computer, NvApp app, QImage image); + void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image); private: void run() { - QImage image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id); - if (image.isNull()) { + QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id); + if (image.isEmpty()) { // Give it another shot if it fails once image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id); } diff --git a/app/gui/AppView.qml b/app/gui/AppView.qml new file mode 100644 index 00000000..2ab7292c --- /dev/null +++ b/app/gui/AppView.qml @@ -0,0 +1,59 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 + +import AppModel 1.0 + +GridView { + property int computerIndex + + anchors.fill: parent + anchors.leftMargin: 5 + anchors.topMargin: 5 + anchors.rightMargin: 5 + anchors.bottomMargin: 5 + cellWidth: 225; cellHeight: 350; + focus: true + + function createModel() + { + var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, "") + model.initialize(computerIndex) + return model + } + + model: createModel() + + delegate: Item { + width: 200; height: 300; + + Image { + id: appIcon + anchors.horizontalCenter: parent.horizontalCenter; + source: model.boxart + sourceSize { + width: 150 + height: 200 + } + } + + Text { + id: appNameText + text: model.name + color: "white" + + width: parent.width + height: 100 + anchors.top: appIcon.bottom + font.pointSize: 26 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + } + + MouseArea { + anchors.fill: parent + onClicked: { + parent.GridView.view.currentIndex = index + } + } + } +} diff --git a/app/gui/appmodel.cpp b/app/gui/appmodel.cpp new file mode 100644 index 00000000..3f8f647f --- /dev/null +++ b/app/gui/appmodel.cpp @@ -0,0 +1,124 @@ +#include "appmodel.h" + +AppModel::AppModel(QObject *parent) + : QAbstractListModel(parent) +{ + connect(&m_ComputerManager, &ComputerManager::computerStateChanged, + this, &AppModel::handleComputerStateChanged); + connect(&m_BoxArtManager, &BoxArtManager::boxArtLoadComplete, + this, &AppModel::handleBoxArtLoaded); +} + +void AppModel::initialize(int computerIndex) +{ + Q_ASSERT(computerIndex < m_ComputerManager.getComputers().count()); + m_Computer = m_ComputerManager.getComputers().at(computerIndex); + m_Apps = m_Computer->appList; + m_CurrentGameId = m_Computer->currentGameId; + + m_ComputerManager.startPolling(); +} + +int AppModel::rowCount(const QModelIndex &parent) const +{ + // For list models only the root node (an invalid parent) should return the list's size. For all + // other (valid) parents, rowCount() should return 0 so that it does not become a tree model. + if (parent.isValid()) + return 0; + + return m_Apps.count(); +} + +QVariant AppModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + Q_ASSERT(index.row() < m_Apps.count()); + NvApp app = m_Apps.at(index.row()); + + switch (role) + { + case NameRole: + return app.name; + case RunningRole: + return m_Computer->currentGameId == app.id; + case BoxArtRole: + // FIXME: const-correctness + return const_cast(m_BoxArtManager).loadBoxArt(m_Computer, app); + default: + return QVariant(); + } +} + +QHash AppModel::roleNames() const +{ + QHash names; + + names[NameRole] = "name"; + names[RunningRole] = "running"; + names[BoxArtRole] = "boxart"; + + return names; +} + +void AppModel::handleComputerStateChanged(NvComputer* computer) +{ + // Ignore updates for computers that aren't ours + if (computer != m_Computer) { + return; + } + + // First, process additions/removals from the app list. This + // is required because the new game may now be running, so + // we can't check that first. + if (computer->appList != m_Apps) { + // Just reset the whole thing if the list changes + beginResetModel(); + m_Apps = computer->appList; + m_CurrentGameId = computer->currentGameId; + endResetModel(); + return; + } + + // Finally, process changes to the active app + if (computer->currentGameId != m_CurrentGameId) { + // First, invalidate the running state of newly running game + for (int i = 0; i < m_Apps.count(); i++) { + if (m_Apps[i].id == computer->currentGameId) { + emit dataChanged(createIndex(i, 0), + createIndex(i, 0), + QVector() << RunningRole); + break; + } + } + + // Next, invalidate the running state of the old game (if it exists) + if (m_CurrentGameId != 0) { + for (int i = 0; i < m_Apps.count(); i++) { + if (m_Apps[i].id == m_CurrentGameId) { + emit dataChanged(createIndex(i, 0), + createIndex(i, 0), + QVector() << RunningRole); + break; + } + } + } + + // Now update our internal state + m_CurrentGameId = m_Computer->currentGameId; + } +} + +void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image */) +{ + Q_ASSERT(computer == m_Computer); + + int index = m_Apps.indexOf(app); + Q_ASSERT(index >= 0); + + // Let our view know the box art data has changed for this app + emit dataChanged(createIndex(index, 0), + createIndex(index, 0), + QVector() << BoxArtRole); +} diff --git a/app/gui/appmodel.h b/app/gui/appmodel.h new file mode 100644 index 00000000..0d2b053a --- /dev/null +++ b/app/gui/appmodel.h @@ -0,0 +1,42 @@ +#pragma once + +#include "backend/boxartmanager.h" +#include "backend/computermanager.h" + +#include + +class AppModel : public QAbstractListModel +{ + Q_OBJECT + + enum Roles + { + NameRole = Qt::UserRole, + RunningRole, + BoxArtRole + }; + +public: + explicit AppModel(QObject *parent = nullptr); + + // Must be called before any QAbstractListModel functions + Q_INVOKABLE void initialize(int computerIndex); + + QVariant data(const QModelIndex &index, int role) const override; + + int rowCount(const QModelIndex &parent) const override; + + virtual QHash roleNames() const override; + +private slots: + void handleComputerStateChanged(NvComputer* computer); + + void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image); + +private: + NvComputer* m_Computer; + BoxArtManager m_BoxArtManager; + ComputerManager m_ComputerManager; + QVector m_Apps; + int m_CurrentGameId; +}; diff --git a/app/main.cpp b/app/main.cpp index a087c0db..b2fd4fe1 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -2,6 +2,7 @@ #include #include "gui/computermodel.h" +#include "gui/appmodel.h" // Don't let SDL hook our main function, since Qt is already // doing the same thing @@ -28,6 +29,7 @@ int main(int argc, char *argv[]) // Register our C++ types for QML qmlRegisterType("ComputerModel", 1, 0, "ComputerModel"); + qmlRegisterType("AppModel", 1, 0, "AppModel"); // Load the main.qml file QQmlApplicationEngine engine; diff --git a/app/qml.qrc b/app/qml.qrc index e5008661..08f52b4c 100644 --- a/app/qml.qrc +++ b/app/qml.qrc @@ -5,5 +5,6 @@ gui/PcView.qml gui/ic_tv_white_48px.svg gui/ic_add_to_queue_white_48px.svg + gui/AppView.qml