Compare commits

..

1 Commits

Author SHA1 Message Date
cybrneko 6a152edfb4 top screen hero and logo art 2026-08-15 03:46:53 -05:00
3 changed files with 347 additions and 31 deletions
+308 -24
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
}
}
} }
} }
} }
@@ -252,6 +535,7 @@ QtObject {
function onNavigationFocused() { function onNavigationFocused() {
gamesList.currentIndex = index gamesList.currentIndex = index
root.selectedGame = gameLibraryModel.gameById(model.gameId)
} }
Component.onCompleted: { Component.onCompleted: {
+33 -5
View File
@@ -18,7 +18,7 @@ int GameLibraryModel::rowCount(const QModelIndex &parent) const {
} }
// fetches artwork based on the game's UUID // fetches artwork based on the game's UUID
QUrl GameLibraryModel::gridArtworkUrl(const QString &gameId) const { QUrl GameLibraryModel::getArtworkUrl(const QString &gameId, const QString &artworkType) const {
QString dataDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QString dataDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir artworkDirectory(QDir(dataDirectory).filePath("artwork/" + gameId)); QDir artworkDirectory(QDir(dataDirectory).filePath("artwork/" + gameId));
@@ -27,11 +27,12 @@ QUrl GameLibraryModel::gridArtworkUrl(const QString &gameId) const {
"png", "png",
"jpg", "jpg",
"jpeg", "jpeg",
"webp" "webp",
"gif"
}; };
for (const QString &extension : extensions) { for (const QString &extension : extensions) {
QString artworkPath = artworkDirectory.filePath("grid." + extension); QString artworkPath = artworkDirectory.filePath(artworkType + "." + extension);
if (QFileInfo::exists(artworkPath)) { if (QFileInfo::exists(artworkPath)) {
return QUrl::fromLocalFile(artworkPath); return QUrl::fromLocalFile(artworkPath);
@@ -41,6 +42,25 @@ QUrl GameLibraryModel::gridArtworkUrl(const QString &gameId) const {
return {}; 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 // returns whatever data is requested
QVariant GameLibraryModel::data(const QModelIndex &index, int role) const { QVariant GameLibraryModel::data(const QModelIndex &index, int role) const {
if (!index.isValid()) { if (!index.isValid()) {
@@ -70,7 +90,13 @@ QVariant GameLibraryModel::data(const QModelIndex &index, int role) const {
return game.source; return game.source;
case GridArtworkRole: case GridArtworkRole:
return gridArtworkUrl(game.id); return getArtworkUrl(game.id, "grid");
case HeroArtworkRole:
return getArtworkUrl(game.id, "hero");
case LogoArtworkRole:
return getArtworkUrl(game.id, "logo");
default: default:
return {}; return {};
@@ -84,7 +110,9 @@ QHash<int, QByteArray> GameLibraryModel::roleNames() const {
{ PlatformRole, "platform" }, { PlatformRole, "platform" },
{ RunnerRole, "runner" }, { RunnerRole, "runner" },
{ SourceRole, "source" }, { SourceRole, "source" },
{ GridArtworkRole, "gridArtwork" } { GridArtworkRole, "gridArtwork" },
{ HeroArtworkRole, "heroArtwork" },
{ LogoArtworkRole, "logoArtwork" }
}; };
} }
+6 -2
View File
@@ -17,12 +17,16 @@ public:
PlatformRole, PlatformRole,
RunnerRole, RunnerRole,
SourceRole, SourceRole,
GridArtworkRole GridArtworkRole,
HeroArtworkRole,
LogoArtworkRole
}; };
// make sure we get a GameRepository // make sure we get a GameRepository
explicit GameLibraryModel(GameRepository &repository, QObject *parent = nullptr); explicit GameLibraryModel(GameRepository &repository, QObject *parent = nullptr);
Q_INVOKABLE QVariantMap gameById(const QString &gameId) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@@ -34,5 +38,5 @@ public:
private: private:
GameRepository &m_repository; GameRepository &m_repository;
QList<GameRecord> m_games; QList<GameRecord> m_games;
QUrl gridArtworkUrl(const QString &gameId) const; QUrl getArtworkUrl(const QString &gameId, const QString &artworkType) const;
}; };