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
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.
// I'll burn that bridge when I get to it
property Window topWindow: Window {
@@ -12,35 +16,314 @@ QtObject {
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.
Connections {
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
Rectangle {
anchors.fill: parent
color: "#181818"
color: "black"
Column {
anchors.centerIn: parent
spacing: 20
Item {
id: heroArea
Text {
text: "nomad top screen"
color: "white"
font.pixelSize: 50
anchors.fill: parent
property bool heroAActive: true
property url requestedHero: ""
property bool logoAActive: true
property url requestedLogo: ""
readonly property int fadeTime: 200
function showHero(newSource) {
requestedHero = newSource
heroUpdateTimer.restart()
}
Text {
text: "last input: " + topWindow.lastInputText
color: "white"
font.pixelSize: 30
function loadRequestedHero() {
transitionToA.stop()
transitionToB.stop()
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() {
gamesList.currentIndex = index
root.selectedGame = gameLibraryModel.gameById(model.gameId)
}
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
QUrl GameLibraryModel::gridArtworkUrl(const QString &gameId) const {
QUrl GameLibraryModel::getArtworkUrl(const QString &gameId, const QString &artworkType) const {
QString dataDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir artworkDirectory(QDir(dataDirectory).filePath("artwork/" + gameId));
@@ -27,11 +27,12 @@ QUrl GameLibraryModel::gridArtworkUrl(const QString &gameId) const {
"png",
"jpg",
"jpeg",
"webp"
"webp",
"gif"
};
for (const QString &extension : extensions) {
QString artworkPath = artworkDirectory.filePath("grid." + extension);
QString artworkPath = artworkDirectory.filePath(artworkType + "." + extension);
if (QFileInfo::exists(artworkPath)) {
return QUrl::fromLocalFile(artworkPath);
@@ -41,6 +42,25 @@ QUrl GameLibraryModel::gridArtworkUrl(const QString &gameId) const {
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()) {
@@ -70,7 +90,13 @@ QVariant GameLibraryModel::data(const QModelIndex &index, int role) const {
return game.source;
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:
return {};
@@ -84,7 +110,9 @@ QHash<int, QByteArray> GameLibraryModel::roleNames() const {
{ PlatformRole, "platform" },
{ RunnerRole, "runner" },
{ SourceRole, "source" },
{ GridArtworkRole, "gridArtwork" }
{ GridArtworkRole, "gridArtwork" },
{ HeroArtworkRole, "heroArtwork" },
{ LogoArtworkRole, "logoArtwork" }
};
}
+6 -2
View File
@@ -17,12 +17,16 @@ public:
PlatformRole,
RunnerRole,
SourceRole,
GridArtworkRole
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;
@@ -34,5 +38,5 @@ public:
private:
GameRepository &m_repository;
QList<GameRecord> m_games;
QUrl gridArtworkUrl(const QString &gameId) const;
QUrl getArtworkUrl(const QString &gameId, const QString &artworkType) const;
};