Compare commits

..

3 Commits

Author SHA1 Message Date
cybrneko 6a152edfb4 top screen hero and logo art 2026-08-15 03:46:53 -05:00
cybrneko e769d50a68 DATABASE SUPPORT, ARTWORK, and a title display 2026-08-14 12:44:56 -05:00
cybrneko 39850225fc separated game card into it's own .qml 2026-08-14 09:05:01 -05:00
11 changed files with 899 additions and 43 deletions
+9 -2
View File
@@ -5,13 +5,19 @@ project(nomad-shell VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Quick) find_package(Qt6 REQUIRED COMPONENTS Quick Sql)
qt_standard_project_setup() qt_standard_project_setup()
qt_add_executable(nomad-shell qt_add_executable(nomad-shell
src/main.cpp src/main.cpp
src/input/InputManager.cpp src/input/InputManager.cpp
src/library/LibraryDatabase.cpp
src/library/GameRecord.h
src/library/GameRepository.h
src/library/GameRepository.cpp
src/library/GameLibraryModel.h
src/library/GameLibraryModel.cpp
) )
qt_add_qml_module(nomad-shell qt_add_qml_module(nomad-shell
@@ -20,8 +26,9 @@ qt_add_qml_module(nomad-shell
QML_FILES QML_FILES
qml/Main.qml qml/Main.qml
qml/NavigationManager.qml qml/NavigationManager.qml
qml/GameCard.qml
) )
target_link_libraries(nomad-shell target_link_libraries(nomad-shell
PRIVATE Qt6::Quick PRIVATE Qt6::Quick Qt6::Sql
) )
+33
View File
@@ -0,0 +1,33 @@
import QtQuick
Item {
id: root
property bool highlighted: false
property string title: ""
property url artwork
width: 120
height: 180
scale: highlighted ? 1.10 : 1.0
Behavior on scale {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
Rectangle {
anchors.fill: parent
color: "white"
Image {
anchors.fill: parent
source: root.artwork
fillMode: Image.PreserveAspectCrop
}
}
}
+393 -41
View File
@@ -3,6 +3,10 @@ import QtQuick.Window
import NomadInput 1.0 import NomadInput 1.0
QtObject { QtObject {
id: root
property var selectedGame: ({})
// this is our top screen, for now, though in the future I would like to have it configurable which screen does which. // this is our top screen, for now, though in the future I would like to have it configurable which screen does which.
// I'll burn that bridge when I get to it // I'll burn that bridge when I get to it
property Window topWindow: Window { property Window topWindow: Window {
@@ -12,35 +16,314 @@ QtObject {
title: "nomad-top" // this is necesarry for Sway, which auto-assigns the windows title: "nomad-top" // this is necesarry for Sway, which auto-assigns the windows
property string lastInputText: "none" // I've never really integrated NomadInput into this debug section in the top half and now I'm too lazy to. it's just a placeholder anyway. property string lastInputText: "none" // I've never really integrated NomadInput into this debug section in the top half and now I'm too lazy to. it's just a placeholder anyway.
Connections { Rectangle {
target: inputManager
function onActionPressed(action) {
topWindow.lastInputText = inputManager.actionName(action) + " pressed"
}
function onActionReleased(action) {
topWindow.lastInputText = inputManager.actionName(action) + " released"
}
}
Rectangle { // just a placeholder for the top screen until we do something better
anchors.fill: parent anchors.fill: parent
color: "#181818" color: "black"
Column { Item {
anchors.centerIn: parent id: heroArea
spacing: 20
Text { anchors.fill: parent
text: "nomad top screen"
color: "white" property bool heroAActive: true
font.pixelSize: 50 property url requestedHero: ""
property bool logoAActive: true
property url requestedLogo: ""
readonly property int fadeTime: 200
function showHero(newSource) {
requestedHero = newSource
heroUpdateTimer.restart()
} }
Text { function loadRequestedHero() {
text: "last input: " + topWindow.lastInputText transitionToA.stop()
color: "white" transitionToB.stop()
font.pixelSize: 30
if (heroA.opacity >= heroB.opacity) {
heroAActive = true
heroA.opacity = 1
heroB.opacity = 0
heroB.source = requestedHero
// If this source was already loaded, status won't change,
// so onStatusChanged won't fire. Start the transition ourselves.
if (heroB.status === Image.Ready
&& heroB.source === requestedHero) {
transitionToB.restart()
}
} else {
heroAActive = false
heroA.opacity = 0
heroB.opacity = 1
heroA.source = requestedHero
if (heroA.status === Image.Ready
&& heroA.source === requestedHero) {
transitionToA.restart()
}
}
}
Timer {
id: heroUpdateTimer
interval: 75
onTriggered: {
heroArea.loadRequestedHero()
}
}
Connections {
target: root
function onSelectedGameChanged() {
heroArea.showHero(root.selectedGame.heroArtwork)
heroArea.showLogo(root.selectedGame.logoArtwork)
}
}
Image {
id: heroA
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
opacity: 1
onStatusChanged: {
if (status === Image.Ready && source === heroArea.requestedHero && !heroArea.heroAActive) {
transitionToA.restart()
}
}
}
Image {
id: heroB
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
opacity: 0
asynchronous: true
cache: true
onStatusChanged: {
if (status === Image.Ready && source === heroArea.requestedHero && heroArea.heroAActive) {
transitionToB.restart()
}
}
}
ParallelAnimation {
id: transitionToB
NumberAnimation {
target: heroA
property: "opacity"
to: 0
duration: heroArea.fadeTime
easing.type: Easing.OutCubic
}
NumberAnimation {
target: heroB
property: "opacity"
to: 1
duration: heroArea.fadeTime
easing.type: Easing.OutCubic
}
onFinished: {
if (heroB.source === heroArea.requestedHero) {
heroArea.heroAActive = false
heroA.opacity = 0
heroB.opacity = 1
}
}
}
ParallelAnimation {
id: transitionToA
NumberAnimation {
target: heroB
property: "opacity"
to: 0
duration: heroArea.fadeTime
easing.type: Easing.OutCubic
}
NumberAnimation {
target: heroA
property: "opacity"
to: 1
duration: heroArea.fadeTime
easing.type: Easing.OutCubic
}
onFinished: {
if (heroA.source === heroArea.requestedHero) {
heroArea.heroAActive = true
heroA.opacity = 1
heroB.opacity = 0
}
}
}
Image {
id: preloadPrevious
visible: false
asynchronous: true
cache: true
}
Image {
id: preloadNext
visible: false
asynchronous: true
cache: true
}
function showLogo(newSource) {
requestedLogo = newSource
transitionLogoToA.stop()
transitionLogoToB.stop()
if (logoAActive) {
logoB.source = requestedLogo
if (logoB.status === Image.Ready
&& logoB.source === requestedLogo) {
transitionLogoToB.restart()
}
} else {
logoA.source = requestedLogo
if (logoA.status === Image.Ready
&& logoA.source === requestedLogo) {
transitionLogoToA.restart()
}
}
}
Image {
id: logoA
z: 10
anchors.centerIn: parent
width: 400
height: 240
fillMode: Image.PreserveAspectFit
asynchronous: true
cache: true
opacity: 1
onStatusChanged: {
if (
status === Image.Ready
&& source === heroArea.requestedLogo
&& !heroArea.logoAActive
) {
transitionLogoToA.restart()
}
}
}
Image {
id: logoB
z: 10
anchors.centerIn: parent
width: 400
height: 240
fillMode: Image.PreserveAspectFit
asynchronous: true
cache: true
opacity: 0
onStatusChanged: {
if (
status === Image.Ready
&& source === heroArea.requestedLogo
&& heroArea.logoAActive
) {
transitionLogoToB.restart()
}
}
}
ParallelAnimation {
id: transitionLogoToB
NumberAnimation {
target: logoA
property: "opacity"
to: 0
duration: heroArea.fadeTime
easing.type: Easing.OutCubic
}
NumberAnimation {
target: logoB
property: "opacity"
to: 1
duration: heroArea.fadeTime
easing.type: Easing.OutCubic
}
onFinished: {
if (logoB.source === heroArea.requestedLogo) {
heroArea.logoAActive = false
logoA.opacity = 0
logoB.opacity = 1
}
}
}
ParallelAnimation {
id: transitionLogoToA
NumberAnimation {
target: logoB
property: "opacity"
to: 0
duration: heroArea.fadeTime
easing.type: Easing.OutCubic
}
NumberAnimation {
target: logoA
property: "opacity"
to: 1
duration: heroArea.fadeTime
easing.type: Easing.OutCubic
}
onFinished: {
if (logoA.source === heroArea.requestedLogo) {
heroArea.logoAActive = true
logoA.opacity = 1
logoB.opacity = 0
}
}
} }
} }
} }
@@ -80,17 +363,6 @@ QtObject {
anchors.fill: parent anchors.fill: parent
color: "#181818" color: "#181818"
// top text where a menu dock will probably be eventually
Text {
anchors.top: parent.top
anchors.topMargin: 30
anchors.horizontalCenter: parent.horizontalCenter
text: "top placeholder"
color: "white"
font.pixelSize: 24
}
// this is the game carousel. mostly self explanatory, but I wanna point out that the local index for this listView does probably still matter. // this is the game carousel. mostly self explanatory, but I wanna point out that the local index for this listView does probably still matter.
// I got rid of dedicated menu indexing for the dock, but I think it still matters here, for the sake of auto-scrolling // I got rid of dedicated menu indexing for the dock, but I think it still matters here, for the sake of auto-scrolling
ListView { ListView {
@@ -117,17 +389,47 @@ QtObject {
leftMargin: 16 leftMargin: 16
rightMargin: 16 rightMargin: 16
model: 20 // this is a placeholder for now, I don't know how I really want to handle max game carousel size yet. another time. model: gameLibraryModel
// provides the smoothing animation for game selection // provides the smoothing animation for game selection
// TODO: tune this to make it as nice as possible !!!
NumberAnimation { NumberAnimation {
id: scrollAnimation id: scrollAnimation
target: gamesList target: gamesList
property: "contentX" property: "contentX"
duration: 200 duration: 300
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
function selectCentermostGame() {
let viewCenter = gamesList.width / 2
let bestItem = null
let bestDistance = Infinity
for (let i = 0; i < gamesList.count; i++) {
let item = gamesList.itemAtIndex(i)
// I think offscreen items do not exist
if (!item) {
continue
}
let itemCenter = item.x - gamesList.contentX + item.width / 2
let distance = Math.abs(itemCenter - viewCenter)
if (distance < bestDistance) {
bestDistance = distance
bestItem = item
}
}
if (bestItem) {
navigationManager.focusItem(bestItem)
}
}
// used to scroll to a new game on the game list // used to scroll to a new game on the game list
function scrollTo(targetX) { function scrollTo(targetX) {
if (Math.abs(targetX - gamesList.contentX) < 0.5) { if (Math.abs(targetX - gamesList.contentX) < 0.5) {
@@ -220,18 +522,20 @@ QtObject {
// after flicking, we re-snap the carousel wherever it landed // after flicking, we re-snap the carousel wherever it landed
onMovementEnded: { onMovementEnded: {
gamesList.scrollTo(gamesList.snappedContentX(gamesList.contentX)) gamesList.scrollTo(gamesList.snappedContentX(gamesList.contentX))
gamesList.selectCentermostGame()
} }
delegate: Rectangle { // TODO: design a real game card QML to replace this placeholder delegate: GameCard {
id: gameTile id: gameTile
width: 120 title: model.title
height: 180 artwork: model.gridArtwork
color: navigationManager.currentItem === gameTile ? "blue" : "white" highlighted: navigationManager.currentItem === gameTile && !gamesList.moving
function onNavigationFocused() { function onNavigationFocused() {
gamesList.currentIndex = index gamesList.currentIndex = index
root.selectedGame = gameLibraryModel.gameById(model.gameId)
} }
Component.onCompleted: { Component.onCompleted: {
@@ -295,6 +599,54 @@ QtObject {
} }
} }
} }
Text {
id: selectedGameTitle
text: gamesList.currentItem ? gamesList.currentItem.title : ""
opacity: (navigationManager.currentItem === gamesList.currentItem && !gamesList.moving) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
color: "white"
font.pixelSize: 18
elide: Text.ElideRight
horizontalAlignment: Text. AlignHCenter
width: Math.min(implicitWidth, bottomRoot.width - 32)
x: { // we wanna make sure that it centers above the game, except when it's at rest and it's close enough to the edge of the screen that it wont fit
if (!gamesList.currentItem) {
return 16
}
let cardCenter = gamesList.x + gamesList.currentItem.x - gamesList.contentX + gamesList.currentItem.width / 2
let desiredX = cardCenter - width / 2
let minimumX = 16
let maximumX = bottomRoot.width - width - 16
return Math.max(minimumX, Math.min(desiredX, maximumX))
}
Behavior on x {
enabled: !gamesList.moving && !scrollAnimation.running
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
anchors.top: gamesList.bottom
anchors.topMargin: 10
}
} }
} }
} }
+137
View File
@@ -0,0 +1,137 @@
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include "GameLibraryModel.h"
GameLibraryModel::GameLibraryModel(GameRepository &repository, QObject *parent) : QAbstractListModel(parent), m_repository(repository) {
refresh();
}
int GameLibraryModel::rowCount(const QModelIndex &parent) const {
// our database is flat, so return 0 if it's asking for children
if (parent.isValid()) {
return 0;
}
return m_games.size();
}
// fetches artwork based on the game's UUID
QUrl GameLibraryModel::getArtworkUrl(const QString &gameId, const QString &artworkType) const {
QString dataDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir artworkDirectory(QDir(dataDirectory).filePath("artwork/" + gameId));
const QStringList extensions = {
"png",
"jpg",
"jpeg",
"webp",
"gif"
};
for (const QString &extension : extensions) {
QString artworkPath = artworkDirectory.filePath(artworkType + "." + extension);
if (QFileInfo::exists(artworkPath)) {
return QUrl::fromLocalFile(artworkPath);
}
}
return {};
}
QVariantMap GameLibraryModel::gameById(const QString &gameId) const {
for (const GameRecord &game : m_games) {
if (game.id == gameId) {
return {
{ "gameId", game.id },
{ "title", game.title },
{ "platform", game.platform },
{ "runner", game.runner },
{ "source", game.source },
{ "gridArtwork", getArtworkUrl(game.id, "grid") },
{ "heroArtwork", getArtworkUrl(game.id, "hero") },
{ "logoArtwork", getArtworkUrl(game.id, "logo") }
};
}
}
return {};
}
// returns whatever data is requested
QVariant GameLibraryModel::data(const QModelIndex &index, int role) const {
if (!index.isValid()) {
return {};
}
if (index.row() < 0 || index.row() >= m_games.size()) {
return {};
}
const GameRecord &game = m_games.at(index.row());
switch (role) {
case GameIdRole:
return game.id;
case TitleRole:
return game.title;
case PlatformRole:
return game.platform;
case RunnerRole:
return game.runner;
case SourceRole:
return game.source;
case GridArtworkRole:
return getArtworkUrl(game.id, "grid");
case HeroArtworkRole:
return getArtworkUrl(game.id, "hero");
case LogoArtworkRole:
return getArtworkUrl(game.id, "logo");
default:
return {};
}
}
QHash<int, QByteArray> GameLibraryModel::roleNames() const {
return {
{ GameIdRole, "gameId" },
{ TitleRole, "title" },
{ PlatformRole, "platform" },
{ RunnerRole, "runner" },
{ SourceRole, "source" },
{ GridArtworkRole, "gridArtwork" },
{ HeroArtworkRole, "heroArtwork" },
{ LogoArtworkRole, "logoArtwork" }
};
}
// Reload all games from the repository safely
bool GameLibraryModel::refresh() {
QString error;
QList<GameRecord> games = m_repository.allGames(&error);
if (!error.isEmpty()) {
qWarning() << "could not load game library:" << error;
return false;
}
beginResetModel();
m_games = games;
endResetModel();
return true;
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <QAbstractListModel>
#include <QList>
#include <QUrl>
#include "GameRecord.h"
#include "GameRepository.h"
class GameLibraryModel : public QAbstractListModel {
Q_OBJECT
public:
// lays out the individual bits of data that this can expose
enum Role {
GameIdRole = Qt::UserRole + 1,
TitleRole,
PlatformRole,
RunnerRole,
SourceRole,
GridArtworkRole,
HeroArtworkRole,
LogoArtworkRole
};
// make sure we get a GameRepository
explicit GameLibraryModel(GameRepository &repository, QObject *parent = nullptr);
Q_INVOKABLE QVariantMap gameById(const QString &gameId) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
bool refresh();
private:
GameRepository &m_repository;
QList<GameRecord> m_games;
QUrl getArtworkUrl(const QString &gameId, const QString &artworkType) const;
};
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <QString>
struct GameRecord
{
QString id;
QString title;
QString platform;
QString runner;
QString source;
};
+52
View File
@@ -0,0 +1,52 @@
#include <QSqlError>
#include <QSqlQuery>
#include "GameRepository.h"
// GameRepository is here to handle queries against the games stored in nomad's library database
// We don't wanna let QML touch our database directly, so database logic goes here
// store a handle to the database connection we got
GameRepository::GameRepository(QSqlDatabase database) : m_database(database) {}
QList<GameRecord> GameRepository::allGames(QString *error) const {
// this list will be populated with a bunch of GameRecords for each row of the query
QList<GameRecord> games;
QSqlQuery query(m_database);
// fetches every game in the library. for now, it's sorted by title's alphabetically.
// I'll probably keep it this way and do all UI sorting and organization in-house
if (!query.exec(R"(
SELECT
id,
title,
platform,
runner,
source
FROM games
ORDER BY title COLLATE NOCASE;
)")) {
if (error) {
*error = query.lastError().text();
}
// return empty list if it fails
return games;
}
// step through each row and convert it to a GameRecord, adding it to the list
while (query.next()) {
GameRecord game;
// Columns are numbered in the same order as the SELECT statement above, starting at 0
game.id = query.value(0).toString();
game.title = query.value(1).toString();
game.platform = query.value(2).toString();
game.runner = query.value(3).toString();
game.source = query.value(4).toString();
games.append(game);
}
return games;
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <QList>
#include <QSqlDatabase>
#include <QString>
#include "GameRecord.h"
class GameRepository {
public:
explicit GameRepository(QSqlDatabase database);
QList<GameRecord> allGames(QString *error = nullptr) const;
private:
QSqlDatabase m_database;
};
+158
View File
@@ -0,0 +1,158 @@
#include <QDir>
#include <QSqlError>
#include <QSqlQuery>
#include <QStandardPaths>
#include <QVariant>
#include "LibraryDatabase.h"
LibraryDatabase::~LibraryDatabase() {
// close our database when LibraryDatabase is destroyed
if (m_database.isOpen()) {
m_database.close();
}
}
bool LibraryDatabase::open() {
// ask Qt for whatever platform-specific appdata directory we've got access to
QString dataDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (dataDirectory.isEmpty()) {
m_lastError = "could not find application directory";
return false;
}
QDir directory;
// ensure the directory exists before trying to open the database
if (!directory.mkpath(dataDirectory)) {
m_lastError = "could not create application data directory: " + dataDirectory;
return false;
}
// just put the database at the root of our appdata directory
m_databasePath = dataDirectory + "/library.db";
// create a named Qt SQL connection with SQLITE
m_database = QSqlDatabase::addDatabase("QSQLITE", "nomad-library");
// tell the SQLite connection where it's database is
m_database.setDatabaseName(m_databasePath);
if (!m_database.open()) {
m_lastError = "could not open library database: " + m_database.lastError().text();
return false;
}
// make sure we've got a schema that this version of nomad understands
if (!initializeSchema()) {
m_database.close();
return false;
}
return true;
}
bool LibraryDatabase::initializeSchema() {
// newest database schema this version of nomad knows how to use
constexpr int CurrentSchemaVersion = 1;
// run queries against nomad's database connection
QSqlQuery query(m_database);
// SQLite's user_version is an application-controlled integer. we'll use it to
// keep track of which version of our schema the database is currently on
if (!query.exec("PRAGMA user_version;")) {
m_lastError = "could not read database schema version: " + query.lastError().text();
return false;
}
// move to the first row returned by the query to read it's value, and set it if it's
// read correctly
if (!query.next()) {
m_lastError = "database didn't return a schema version.";
return false;
}
int schemaVersion =
query.value(0).toInt();
// SQLite inits user_version to 0, so version 0 means this database hasn't had the
// nomad initial schema created
if (schemaVersion == 0) {
// make schema creation atomic; if any of it fails, make sure the database is unchanged.
if (!m_database.transaction()) {
m_lastError = "could not begin database initialization transaction.";
return false;
}
// initial games table
if (!query.exec(R"(
CREATE TABLE games (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
platform TEXT NOT NULL,
runner TEXT,
source TEXT NOT NULL
);
)")) {
m_lastError = "couldn't create games table: " + query.lastError().text();
m_database.rollback();
return false;
}
// schema creation successful, attempt to mark this database as version 1
if (!query.exec("PRAGMA user_version = 1;")) {
m_lastError = "could not set database schema version: " + query.lastError().text();
m_database.rollback();
return false;
}
// permanently apply this transaction, or attempt to
if (!m_database.commit()) {
m_lastError = "could not commit database initialization.";
m_database.rollback();
return false;
}
schemaVersion = 1;
}
// don't attempt to use a database if it's created with a newer version.
if (schemaVersion > CurrentSchemaVersion) {
m_lastError = "library database schema is newer than this version of nomad supports.";
return false;
}
return true;
}
QSqlDatabase LibraryDatabase::database() const {
return m_database;
}
bool LibraryDatabase::isOpen() const {
return m_database.isOpen();
}
QString LibraryDatabase::databasePath() const {
return m_databasePath;
}
QString LibraryDatabase::lastError() const {
return m_lastError;
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <QSqlDatabase>
#include <QString>
class LibraryDatabase {
public:
LibraryDatabase() = default;
~LibraryDatabase();
bool open();
bool isOpen() const;
QSqlDatabase database() const;
QString databasePath() const;
QString lastError() const;
private:
bool initializeSchema();
QSqlDatabase m_database;
QString m_databasePath;
QString m_lastError;
};
+23
View File
@@ -2,13 +2,35 @@
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include <QtQml> #include <QtQml>
#include <QDebug>
#include "input/InputManager.h" #include "input/InputManager.h"
#include "library/LibraryDatabase.h"
#include "library/GameRepository.h"
#include "library/GameLibraryModel.h"
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
// creates our Qt GUI app // creates our Qt GUI app
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
// opens nomad's persistent library database, ensures ready schema, etc
LibraryDatabase libraryDatabase;
if (!libraryDatabase.open()) {
qCritical() << "Failed to open nomad library:" << libraryDatabase.lastError();
return -1;
}
// GameRepository hadnles reading our game records from the database, while
// GameLibraryModel exposes those records in a QML-friendly list model
GameRepository gameRepository(libraryDatabase.database());
GameLibraryModel gameLibraryModel(gameRepository);
// database diagnostics
qDebug() << "Library database:" << libraryDatabase.databasePath();
qDebug() << "Games loaded:" << gameLibraryModel.rowCount();
// creats InputManager and installs it as an application-wide event filter so it can observe input events before normal delivery // creats InputManager and installs it as an application-wide event filter so it can observe input events before normal delivery
InputManager inputManager; InputManager inputManager;
app.installEventFilter(&inputManager); app.installEventFilter(&inputManager);
@@ -22,6 +44,7 @@ int main(int argc, char *argv[])
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
// make our InputManager instance available to QML for signals // make our InputManager instance available to QML for signals
engine.rootContext()->setContextProperty("inputManager", &inputManager); engine.rootContext()->setContextProperty("inputManager", &inputManager);
engine.rootContext()->setContextProperty("gameLibraryModel", &gameLibraryModel);
// load our Main.qml into the engine to start things off // load our Main.qml into the engine to start things off
engine.loadFromModule("nomad", "Main"); engine.loadFromModule("nomad", "Main");