Add AppModel and AppView for loading apps and modify BoxArtManager to return QUrls for QML

This commit is contained in:
Cameron Gutman 2018-07-05 20:07:05 -07:00
parent 95eebdbe66
commit b0151da455
8 changed files with 257 additions and 27 deletions

View File

@ -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 \

View File

@ -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();
}

View File

@ -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);
}

59
app/gui/AppView.qml Normal file
View File

@ -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
}
}
}
}

124
app/gui/appmodel.cpp Normal file
View File

@ -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<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
default:
return QVariant();
}
}
QHash<int, QByteArray> AppModel::roleNames() const
{
QHash<int, QByteArray> 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<int>() << 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<int>() << 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<int>() << BoxArtRole);
}

42
app/gui/appmodel.h Normal file
View File

@ -0,0 +1,42 @@
#pragma once
#include "backend/boxartmanager.h"
#include "backend/computermanager.h"
#include <QAbstractListModel>
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<int, QByteArray> 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<NvApp> m_Apps;
int m_CurrentGameId;
};

View File

@ -2,6 +2,7 @@
#include <QQmlApplicationEngine>
#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>("ComputerModel", 1, 0, "ComputerModel");
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
// Load the main.qml file
QQmlApplicationEngine engine;

View File

@ -5,5 +5,6 @@
<file>gui/PcView.qml</file>
<file>gui/ic_tv_white_48px.svg</file>
<file>gui/ic_add_to_queue_white_48px.svg</file>
<file>gui/AppView.qml</file>
</qresource>
</RCC>