From 630a373ccacd907341da4e77d19c6c2dbf54e193 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 21 Feb 2024 17:51:17 -0500 Subject: [PATCH] Start adding character support, creation actions and more --- CMakeLists.txt | 33 +++++--- src/artconfigwindow.cpp | 1 - src/artdetailwindow.cpp | 25 +++++- src/artdetailwindow.h | 2 +- src/artmodel.cpp | 85 +++++++++++++------ src/artmodel.h | 13 ++- src/characterdetailwindow.cpp | 148 ++++++++++++++++++++++++++++++++++ src/characterdetailwindow.h | 33 ++++++++ src/charactermodel.cpp | 135 +++++++++++++++++++++++++++++++ src/charactermodel.h | 48 +++++++++++ src/featuredartmodel.cpp | 8 +- src/featuredartmodel.h | 2 +- src/imagelabel.cpp | 2 +- src/main.cpp | 8 +- src/mainwindow.cpp | 71 ++++++++++++++-- src/mainwindow.h | 6 +- 16 files changed, 558 insertions(+), 62 deletions(-) create mode 100644 src/characterdetailwindow.cpp create mode 100644 src/characterdetailwindow.h create mode 100644 src/charactermodel.cpp create mode 100644 src/charactermodel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 10498ad..d71508b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,8 +11,9 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) -set(QT_MIN_VERSION 5.15) -set(KF5_MIN_VERSION 5.83) +set(QT_MAJOR_VERSION 6) +set(QT_MIN_VERSION 6.5) +set(KF_MIN_VERSION 5.240) find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) @@ -27,12 +28,15 @@ include(ECMPoQmTools) include(KDEGitCommitHooks) include(KDEClangFormat) -find_package(Qt5 ${QT_MIN_VERSION} NO_MODULE REQUIRED COMPONENTS +find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE REQUIRED COMPONENTS Core Gui Widgets Concurrent) -find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS CoreAddons XmlGui I18n) +find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS CoreAddons XmlGui I18n) +find_package(QCoro6 REQUIRED COMPONENTS Core Network DBus) + +qcoro_enable_coroutines() add_executable(Redai) target_sources(Redai PRIVATE @@ -43,6 +47,10 @@ target_sources(Redai PRIVATE src/artdetailwindow.h src/artmodel.cpp src/artmodel.h + src/characterdetailwindow.cpp + src/characterdetailwindow.h + src/charactermodel.cpp + src/charactermodel.h src/featuredartmodel.cpp src/featuredartmodel.h src/imagelabel.cpp @@ -51,13 +59,16 @@ target_sources(Redai PRIVATE src/mainwindow.cpp src/mainwindow.h) target_link_libraries(Redai - Qt5::Core - Qt5::Gui - Qt5::Widgets - Qt5::Concurrent - KF5::I18n - KF5::CoreAddons - KF5::XmlGui) + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Concurrent + KF6::I18n + KF6::CoreAddons + KF6::XmlGui + QCoro::Core + QCoro::Network) +target_compile_options(Redai PRIVATE -fexceptions) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/artconfigwindow.cpp b/src/artconfigwindow.cpp index 3a04288..40b982e 100644 --- a/src/artconfigwindow.cpp +++ b/src/artconfigwindow.cpp @@ -13,7 +13,6 @@ #include #include #include -#include ArtConfigWindow::ArtConfigWindow(const QString &filename, const QString &definitionDirectory, const QString &assetDirectory, QWidget *parent) : QDialog(parent) diff --git a/src/artdetailwindow.cpp b/src/artdetailwindow.cpp index df5dd7e..44a6d3e 100644 --- a/src/artdetailwindow.cpp +++ b/src/artdetailwindow.cpp @@ -6,6 +6,8 @@ #include "imagelabel.h" #include +#include +#include #include #include #include @@ -15,10 +17,11 @@ #include #include #include +#include #include #include -ArtDetailWindow::ArtDetailWindow(const QString &filename, const QDir &assetDirectory, QWidget *parent) +ArtDetailWindow::ArtDetailWindow(const QString &filename, const QUrl &imagePath, QWidget *parent) : QDialog(parent) { setMinimumWidth(800); @@ -38,8 +41,24 @@ ArtDetailWindow::ArtDetailWindow(const QString &filename, const QDir &assetDirec formLayoutWidget->setLayout(formLayout); mainLayout->addWidget(formLayoutWidget); - QImage image; - image.load(assetDirectory.absoluteFilePath(QStringLiteral("%1.webp").arg(withoutExtension))); + // load thumbnail + const auto load_image = [imagePath]() -> QCoro::Task { + QImage image; + if (!imagePath.isLocalFile()) { + QNetworkAccessManager nam; + auto reply = co_await nam.get(QNetworkRequest(imagePath)); + const auto data = reply->readAll(); + reply->deleteLater(); + + image.loadFromData(data, "image/jpeg"); + } else { + image.load(imagePath.toLocalFile()); + } + + co_return image; + }; + + auto image = QCoro::waitFor(load_image()); auto previewBox = new QGroupBox(i18nc("@title:group", "Preview")); mainLayout->addWidget(previewBox); diff --git a/src/artdetailwindow.h b/src/artdetailwindow.h index 711ea8c..37be0d9 100644 --- a/src/artdetailwindow.h +++ b/src/artdetailwindow.h @@ -18,7 +18,7 @@ class ArtDetailWindow : public QDialog { Q_OBJECT public: - ArtDetailWindow(const QString &filename, const QDir &assetDirectory, QWidget *parent = nullptr); + ArtDetailWindow(const QString &filename, const QUrl &imagePath, QWidget *parent = nullptr); private: void loadData(const QString &filename); diff --git a/src/artmodel.cpp b/src/artmodel.cpp index 9da21fc..99ac1a0 100644 --- a/src/artmodel.cpp +++ b/src/artmodel.cpp @@ -5,21 +5,24 @@ #include "artmodel.h" #include +#include +#include #include #include #include #include #include #include +#include #include #include ArtModel::ArtModel(const QDir &definitionDirectory, const QDir &assetDirectory, QObject *parent) : QAbstractTableModel(parent) { - piecesFuture = new QFutureWatcher(this); - connect(piecesFuture, &QFutureWatcher::resultReadyAt, this, &ArtModel::pieceFinished); - connect(piecesFuture, &QFutureWatcher::finished, this, &ArtModel::finished); + piecesFuture = new QFutureWatcher(this); + connect(piecesFuture, &QFutureWatcher::resultReadyAt, this, &ArtModel::pieceFinished); + connect(piecesFuture, &QFutureWatcher::finished, this, &ArtModel::finished); struct PieceInformation { QString definition; @@ -36,34 +39,46 @@ ArtModel::ArtModel(const QDir &definitionDirectory, const QDir &assetDirectory, pieceList.push_back(PieceInformation{definitionDirectory.absoluteFilePath(info.baseName()), assetDirectory.absoluteFilePath(info.baseName())}); - beginInsertRows(QModelIndex(), m_artPieces.size(), m_artPieces.size() + 1); + beginInsertRows(QModelIndex(), static_cast(m_artPieces.size()), static_cast(m_artPieces.size() + 1)); m_artPieces.push_back({}); endInsertRows(); } - const std::function loadPiece = [](const PieceInformation &info) -> ArtPiece { - ArtPiece p(info.definition, info.asset); + const std::function(const PieceInformation &info)> loadPiece = [](const PieceInformation &info) -> QCoro::Task { + auto p = new ArtPiece(info.definition, info.asset); - p.image.load(p.filename); - p.thumbnail = QPixmap::fromImage(p.image).scaled(100, 100, Qt::AspectRatioMode::KeepAspectRatio).toImage(); + // load thumbnail + if (!p->getThumbnailPath().isLocalFile()) { + QNetworkAccessManager nam; + auto reply = co_await nam.get(QNetworkRequest(p->getThumbnailPath())); + const auto data = reply->readAll(); + reply->deleteLater(); - return p; + p->thumbnail.loadFromData(data, "image/jpeg"); + } else { + p->thumbnail.load(p->getThumbnailPath().toLocalFile()); + } + + p->thumbnail = p->thumbnail.scaled(100, 100, Qt::AspectRatioMode::KeepAspectRatio, Qt::SmoothTransformation); + co_return p; }; - piecesFuture->setFuture(QtConcurrent::mapped(pieceList, loadPiece)); + piecesFuture->setFuture(QtConcurrent::mapped(pieceList, [loadPiece](const PieceInformation &info) -> ArtPiece * { + return QCoro::waitFor(loadPiece(info)); + })); } int ArtModel::rowCount(const QModelIndex &parent) const { - Q_UNUSED(parent); - return m_artPieces.size(); + Q_UNUSED(parent) + return static_cast(m_artPieces.size()); } int ArtModel::columnCount(const QModelIndex &parent) const { - Q_UNUSED(parent); + Q_UNUSED(parent) return 4; } @@ -73,30 +88,32 @@ QVariant ArtModel::data(const QModelIndex &index, const int role) const return {}; } + if (m_artPieces[index.row()] == nullptr) { + return {}; + } + if (role == Qt::DisplayRole) { switch (index.column()) { case 0: { - const QString filename = m_artPieces[index.row()].filename; + const QString filename = m_artPieces[index.row()]->filename; return filename.split('/').last(); } case 1: return {}; case 2: - return m_artPieces[index.row()].title; + return m_artPieces[index.row()]->title; case 3: - return m_artPieces[index.row()].hasAltText; + return m_artPieces[index.row()]->hasAltText; } } else if (role == Qt::UserRole) { - return m_artPieces[index.row()].object; + return QVariant::fromValue(m_artPieces[index.row()]); } else if (role == Qt::DecorationRole) { switch (index.column()) { case 1: - return m_artPieces[index.row()].thumbnail; + return m_artPieces[index.row()]->thumbnail; case 3: - return m_artPieces[index.row()].hasAltText ? QIcon::fromTheme(QStringLiteral("emblem-checked")) : QIcon::fromTheme(QStringLiteral("emblem-error")); + return m_artPieces[index.row()]->hasAltText ? QIcon::fromTheme(QStringLiteral("emblem-checked")) : QIcon::fromTheme(QStringLiteral("emblem-error")); } - } else if (role == Qt::UserRole + 1) { - return m_artPieces[index.row()].jsonFilename; } return {}; @@ -131,11 +148,11 @@ void ArtModel::pieceFinished(const int row) void ArtModel::finished() { - std::sort(m_artPieces.begin(), m_artPieces.end(), [](const ArtPiece &a, const ArtPiece &b) { - return a.date > b.date; + std::sort(m_artPieces.begin(), m_artPieces.end(), [](const ArtPiece *a, const ArtPiece *b) { + return a->date > b->date; }); - Q_EMIT dataChanged(index(0, 0), index(m_artPieces.size(), 3)); + Q_EMIT dataChanged(index(0, 0), index(static_cast(m_artPieces.size()), 3)); Q_EMIT loadingFinished(); } @@ -164,3 +181,23 @@ ArtPiece::ArtPiece(const QString &filename, const QString &assetFilename) hasAltText = true; } } + +QUrl ArtPiece::getImagePath() const +{ + if (date.year() > 2022) { + QFileInfo info(filename); + return QUrl::fromUserInput(QStringLiteral("https://images.redstrate.com/art/%1.jpg").arg(info.fileName())); + } else { + return QUrl::fromLocalFile(QStringLiteral("%1.webp").arg(filename)); + } +} + +QUrl ArtPiece::getThumbnailPath() const +{ + if (date.year() > 2022) { + QFileInfo info(filename); + return QUrl::fromUserInput(QStringLiteral("https://images.redstrate.com/thumb/%1.jpg").arg(info.fileName())); + } else { + return QUrl::fromLocalFile(QStringLiteral("%1.webp").arg(filename)); + } +} diff --git a/src/artmodel.h b/src/artmodel.h index b3b69ca..61b4451 100644 --- a/src/artmodel.h +++ b/src/artmodel.h @@ -9,9 +9,11 @@ #include #include #include +#include -class ArtPiece +class ArtPiece : public QObject { + Q_OBJECT public: ArtPiece() = default; ArtPiece(const QString &filename, const QString &assetFilename); @@ -21,9 +23,12 @@ public: QJsonObject object; QDate date; - QImage image, thumbnail; + QImage thumbnail; bool hasAltText = false; + + QUrl getImagePath() const; + QUrl getThumbnailPath() const; }; class ArtModel : public QAbstractTableModel @@ -42,11 +47,11 @@ Q_SIGNALS: void loadingFinished(); protected: - QVector m_artPieces; + QVector m_artPieces; private: void pieceFinished(int index); void finished(); - QFutureWatcher *piecesFuture; + QFutureWatcher *piecesFuture; }; diff --git a/src/characterdetailwindow.cpp b/src/characterdetailwindow.cpp new file mode 100644 index 0000000..c49dc0b --- /dev/null +++ b/src/characterdetailwindow.cpp @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "characterdetailwindow.h" +#include "imagelabel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CharacterDetailWindow::CharacterDetailWindow(const QString &filename, QWidget *parent) + : QDialog(parent) +{ + setMinimumWidth(800); + setMinimumHeight(600); + setWindowModality(Qt::WindowModality::WindowModal); + setWindowTitle(filename); + + QFileInfo info(filename); + const QString withoutExtension = info.completeBaseName(); + + auto mainLayout = new QHBoxLayout(); + setLayout(mainLayout); + + auto formLayout = new QFormLayout(); + auto formLayoutWidget = new QWidget(); + formLayoutWidget->setMaximumWidth(450); + formLayoutWidget->setLayout(formLayout); + mainLayout->addWidget(formLayoutWidget); + + auto previewBox = new QGroupBox(i18nc("@title:group", "Preview")); + mainLayout->addWidget(previewBox); + + auto previewLayout = new QVBoxLayout(); + previewBox->setLayout(previewLayout); + + auto imageView = new ImageLabel(); + previewLayout->addWidget(imageView); + + m_nameEdit = new QLineEdit(); + formLayout->addRow(i18nc("@label:textbox", "Title"), m_nameEdit); + + m_pronounsEdit = new QLineEdit(); + formLayout->addRow(i18nc("@label:textbox", "Pronouns"), m_pronounsEdit); + + m_descriptionEdit = new QTextEdit(); + m_descriptionEdit->setAcceptRichText(false); + formLayout->addRow(i18nc("@label:textbox", "Description"), m_descriptionEdit); + + m_ageEdit = new QLineEdit(); + formLayout->addRow(i18nc("@label:textbox", "Age"), m_ageEdit); + + m_originEdit = new QLineEdit(); + formLayout->addRow(i18nc("@label:textbox", "Origin"), m_originEdit); + + auto bottomButtonLayout = new QHBoxLayout(); + formLayout->addRow(bottomButtonLayout); + + auto cancelButton = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-close")), i18nc("@action:button", "Cancel")); + connect(cancelButton, &QPushButton::clicked, this, &CharacterDetailWindow::close); + bottomButtonLayout->addWidget(cancelButton); + bottomButtonLayout->addStretch(1); + + auto saveButton = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18nc("@action:button", "Save")); + connect(saveButton, &QPushButton::clicked, this, [this, filename] { + saveData(filename); + }); + bottomButtonLayout->addWidget(saveButton); + + if (QFile::exists(filename)) { + loadData(filename); + } +} + +void CharacterDetailWindow::loadData(const QString &filename) +{ + qDebug() << "Loading data from" << filename; + + QFile artFile(filename); + artFile.open(QFile::ReadOnly); + + const QJsonDocument artJson = QJsonDocument::fromJson(artFile.readAll()); + + if (artJson.object().contains(QStringLiteral("name"))) { + m_nameEdit->setText(artJson[QStringLiteral("name")].toString()); + } + + if (artJson.object().contains(QStringLiteral("pronouns"))) { + m_pronounsEdit->setText(artJson[QStringLiteral("pronouns")].toString()); + } + + if (artJson.object().contains(QStringLiteral("age"))) { + m_ageEdit->setText(artJson[QStringLiteral("age")].toString()); + } + + if (artJson.object().contains(QStringLiteral("description"))) { + m_descriptionEdit->setText(artJson[QStringLiteral("description")].toString()); + } + + if (artJson.object().contains(QStringLiteral("origin"))) { + m_originEdit->setText(artJson[QStringLiteral("origin")].toString()); + } +} + +void CharacterDetailWindow::saveData(const QString &filename) +{ + qDebug() << "Saving data to" << filename; + + QJsonObject object; + + if (!m_nameEdit->text().isEmpty()) { + object[QStringLiteral("name")] = m_nameEdit->text(); + } + + if (!m_pronounsEdit->text().isEmpty()) { + object[QStringLiteral("pronouns")] = m_pronounsEdit->text(); + } + + if (!m_ageEdit->text().isEmpty()) { + object[QStringLiteral("age")] = m_ageEdit->text(); + } + + if (!m_descriptionEdit->document()->toPlainText().isEmpty()) { + object[QStringLiteral("description")] = m_descriptionEdit->document()->toPlainText(); + } + + if (!m_originEdit->text().isEmpty()) { + object[QStringLiteral("origin")] = m_originEdit->text(); + } + + const QJsonDocument jsonDoc(object); + + QFile file(filename); + file.open(QFile::WriteOnly); + file.write(jsonDoc.toJson()); + file.close(); + + close(); +} diff --git a/src/characterdetailwindow.h b/src/characterdetailwindow.h new file mode 100644 index 0000000..b0ef22d --- /dev/null +++ b/src/characterdetailwindow.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class CharacterDetailWindow : public QDialog +{ + Q_OBJECT +public: + explicit CharacterDetailWindow(const QString &filename, QWidget *parent = nullptr); + +private: + void loadData(const QString &filename); + + void saveData(const QString &filename); + + QLineEdit *m_nameEdit; + QLineEdit *m_pronounsEdit; + QTextEdit *m_descriptionEdit; + QLineEdit *m_ageEdit; + QLineEdit *m_originEdit; +}; \ No newline at end of file diff --git a/src/charactermodel.cpp b/src/charactermodel.cpp new file mode 100644 index 0000000..312ebd6 --- /dev/null +++ b/src/charactermodel.cpp @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "charactermodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +CharacterModel::CharacterModel(const QDir &definitionDirectory, QObject *parent) + : QAbstractTableModel(parent) +{ + charactersFuture = new QFutureWatcher(this); + connect(charactersFuture, &QFutureWatcher::resultReadyAt, this, &CharacterModel::characterFinished); + connect(charactersFuture, &QFutureWatcher::finished, this, &CharacterModel::finished); + + struct CharacterInformation { + QString definition; + }; + QVector characterList; + + QDirIterator it(definitionDirectory); + while (it.hasNext()) { + QFileInfo info(it.next()); + if (!info.isFile()) { + continue; + } + + characterList.push_back(CharacterInformation{definitionDirectory.absoluteFilePath(info.baseName())}); + + beginInsertRows(QModelIndex(), static_cast(m_characters.size()), static_cast(m_characters.size() + 1)); + + m_characters.push_back({}); + + endInsertRows(); + } + + const std::function(const CharacterInformation &info)> loadPiece = + [](const CharacterInformation &info) -> QCoro::Task { + auto p = new Character(info.definition); + + co_return p; + }; + + charactersFuture->setFuture(QtConcurrent::mapped(characterList, [loadPiece](const CharacterInformation &info) -> Character * { + return QCoro::waitFor(loadPiece(info)); + })); +} + +int CharacterModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return static_cast(m_characters.size()); +} + +int CharacterModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +QVariant CharacterModel::data(const QModelIndex &index, const int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (m_characters[index.row()] == nullptr) { + return {}; + } + + if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: { + return m_characters[index.row()]->name; + } + } + } else if (role == Qt::UserRole) { + return QVariant::fromValue(m_characters[index.row()]); + } + + return {}; +} + +QVariant CharacterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Orientation::Horizontal && role == Qt::DisplayRole) { + switch (section) { + case 0: + return i18nc("@title:column", "Name"); + default: + Q_UNREACHABLE(); + } + } + + return QAbstractTableModel::headerData(section, orientation, role); +} + +void CharacterModel::characterFinished(const int row) +{ + m_characters[row] = charactersFuture->resultAt(row); + + Q_EMIT dataChanged(index(row, 0), index(row + 1, 0)); +} + +void CharacterModel::finished() +{ + std::sort(m_characters.begin(), m_characters.end(), [](const Character *a, const Character *b) { + return a->name > b->name; + }); + + Q_EMIT dataChanged(index(0, 0), index(static_cast(m_characters.size()), 3)); + + Q_EMIT loadingFinished(); +} + +Character::Character(const QString &filename) +{ + this->filename = filename + ".json"; + + QFile artFile(this->filename); + artFile.open(QFile::ReadOnly); + + const QJsonDocument artJson = QJsonDocument::fromJson(artFile.readAll()); + + if (artJson.object().contains(QStringLiteral("name"))) { + name = artJson.object()[QStringLiteral("name")].toString(); + } +} diff --git a/src/charactermodel.h b/src/charactermodel.h new file mode 100644 index 0000000..c7ca823 --- /dev/null +++ b/src/charactermodel.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +class Character : public QObject +{ + Q_OBJECT +public: + Character() = default; + Character(const QString &filename); + + QString filename; + QString name; +}; + +class CharacterModel : public QAbstractTableModel +{ + Q_OBJECT +public: + explicit CharacterModel(const QDir &definitionDirectory, QObject *parent = nullptr); + + [[nodiscard]] int rowCount(const QModelIndex &parent) const override; + [[nodiscard]] int columnCount(const QModelIndex &parent) const override; + + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + +Q_SIGNALS: + void loadingFinished(); + +protected: + QVector m_characters; + +private: + void characterFinished(int index); + void finished(); + + QFutureWatcher *charactersFuture; +}; diff --git a/src/featuredartmodel.cpp b/src/featuredartmodel.cpp index e789ea4..82c6beb 100644 --- a/src/featuredartmodel.cpp +++ b/src/featuredartmodel.cpp @@ -14,7 +14,7 @@ FeaturedArtModel::FeaturedArtModel(const QString &definitionDirectory, const QSt QVariant FeaturedArtModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) - return QVariant(); + return {}; if (role == Qt::DisplayRole) { if (index.column() == 0) { @@ -57,12 +57,12 @@ bool FeaturedArtModel::setData(const QModelIndex &index, const QVariant &value, return true; } -void FeaturedArtModel::setFeaturedItems(QStringList featured) +void FeaturedArtModel::setFeaturedItems(const QStringList &featured) { for (const auto &id : featured) { for (int i = 0; i < m_artPieces.size(); i++) { - ArtPiece &piece = m_artPieces[i]; - QFileInfo fileInfo(piece.jsonFilename); + auto &piece = m_artPieces[i]; + QFileInfo fileInfo(piece->jsonFilename); if (fileInfo.baseName() == id) { setData(index(i, 0), Qt::Checked, Qt::CheckStateRole); } diff --git a/src/featuredartmodel.h b/src/featuredartmodel.h index 5bb0115..69c33d2 100644 --- a/src/featuredartmodel.h +++ b/src/featuredartmodel.h @@ -17,7 +17,7 @@ public: bool setData(const QModelIndex &index, const QVariant &value, int role) override; - void setFeaturedItems(QStringList featured); + void setFeaturedItems(const QStringList &featured); private: QSet checkedItems; diff --git a/src/imagelabel.cpp b/src/imagelabel.cpp index 67c8317..60ce485 100644 --- a/src/imagelabel.cpp +++ b/src/imagelabel.cpp @@ -35,7 +35,7 @@ QPixmap ImageLabel::scaledPixmap() const void ImageLabel::resizeEvent(QResizeEvent *e) { - Q_UNUSED(e); + Q_UNUSED(e) if (!pix.isNull()) { QLabel::setPixmap(scaledPixmap()); } diff --git a/src/main.cpp b/src/main.cpp index edc5a8e..75b710a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,9 +12,6 @@ int main(int argc, char *argv[]) { - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); - QApplication app(argc, argv); KLocalizedString::setApplicationDomain("redai"); @@ -24,7 +21,7 @@ int main(int argc, char *argv[]) QStringLiteral("1.0"), i18n("Website gallery manager"), KAboutLicense::GPL_V3, - i18n("© 2023 Joshua Goins")); + i18n("© 2024 Joshua Goins")); about.addAuthor(i18n("Joshua Goins"), i18n("Maintainer"), QStringLiteral("josh@redstrate.com"), QStringLiteral("https://redstrate.com/")); about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails")); @@ -45,8 +42,9 @@ int main(int argc, char *argv[]) const QDir assetPath = sitePath.absoluteFilePath(QStringLiteral("assets")); const QString artAssetPath = assetPath.absoluteFilePath(QStringLiteral("art")); const QString dataPath = sitePath.absoluteFilePath(QStringLiteral("data")); + const QString charaDefPath = sitePath.absoluteFilePath(QStringLiteral("characters")); - MainWindow window(defPath, artAssetPath, dataPath); + MainWindow window(defPath, artAssetPath, dataPath, charaDefPath); window.show(); return QApplication::exec(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 2f02aea..50ada7c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -10,24 +10,59 @@ #include #include #include +#include #include #include #include +#include #include "artconfigwindow.h" #include "artdetailwindow.h" #include "artmodel.h" +#include "characterdetailwindow.h" +#include "charactermodel.h" -MainWindow::MainWindow(const QDir &definitionDirectory, const QDir &assetDirectory, const QDir &dataDirectory, QWidget *parent) +MainWindow::MainWindow(const QDir &definitionDirectory, + const QDir &assetDirectory, + const QDir &dataDirectory, + const QDir &charactersDefinitionDirectory, + QWidget *parent) : QMainWindow(parent) { setWindowTitle(i18nc("@title:window", "Redai")); setMinimumSize(1280, 720); + auto toolbar = new QToolBar(); + toolbar->setToolButtonStyle(Qt::ToolButtonStyle::ToolButtonTextBesideIcon); + addToolBar(toolbar); + + auto newArtworkAction = toolbar->addAction(i18nc("@action", "Add Artwork")); + newArtworkAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); + connect(newArtworkAction, &QAction::triggered, this, [this, definitionDirectory] { + const QString text = QInputDialog::getText(this, i18n("Identifier"), i18n("Identifier:")); + if (!text.isEmpty()) { + auto window = new ArtDetailWindow(QStringLiteral("%1/%2.json").arg(definitionDirectory.absolutePath(), text), QStringLiteral(""), this); + window->show(); + } + }); + + auto newCharacterAction = toolbar->addAction(i18nc("@action", "Add Character")); + newCharacterAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); + connect(newCharacterAction, &QAction::triggered, this, [this, charactersDefinitionDirectory] { + const QString text = QInputDialog::getText(this, i18n("Identifier"), i18n("Identifier:")); + if (!text.isEmpty()) { + auto window = new CharacterDetailWindow(QStringLiteral("%1/%2.json").arg(charactersDefinitionDirectory.absolutePath(), text), this); + window->show(); + } + }); + auto menuBar = new QMenuBar(); setMenuBar(menuBar); auto fileMenu = menuBar->addMenu(i18nc("@title:menu", "File")); + fileMenu->addAction(newArtworkAction); + fileMenu->addAction(newCharacterAction); + fileMenu->addSeparator(); auto quitAction = fileMenu->addAction(i18nc("@action:inmenu", "Quit")); quitAction->setIcon(QIcon::fromTheme(QStringLiteral("gtk-quit"))); @@ -57,7 +92,7 @@ MainWindow::MainWindow(const QDir &definitionDirectory, const QDir &assetDirecto auto aboutNovusAction = helpMenu->addAction(i18nc("@action:inmenu", "About Redai")); aboutNovusAction->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); - connect(aboutNovusAction, &QAction::triggered, this, [this] { + connect(aboutNovusAction, &QAction::triggered, this, [] { static QPointer dialog; if (!dialog) { dialog = new KAboutApplicationDialog(KAboutData::applicationData(), nullptr); @@ -70,6 +105,9 @@ MainWindow::MainWindow(const QDir &definitionDirectory, const QDir &assetDirecto aboutQtAction->setIcon(QIcon(QStringLiteral(":/qt-project.org/qmessagebox/images/qtlogo-64.png"))); connect(aboutQtAction, &QAction::triggered, QApplication::instance(), &QApplication::aboutQt); + auto tabWidget = new QTabWidget(); + setCentralWidget(tabWidget); + auto model = new ArtModel(definitionDirectory, assetDirectory); auto pieceListView = new QTableView(); @@ -84,12 +122,33 @@ MainWindow::MainWindow(const QDir &definitionDirectory, const QDir &assetDirecto horizontalHeader->setSectionResizeMode(QHeaderView::Stretch); connect(pieceListView, &QListView::clicked, this, [this, assetDirectory](const QModelIndex index) { - const QString filename = index.data(Qt::UserRole + 1).toString(); - const QJsonObject object = index.data(Qt::UserRole).toJsonObject(); + const auto &piece = index.data(Qt::UserRole).value(); - auto window = new ArtDetailWindow(filename, assetDirectory, this); + auto window = new ArtDetailWindow(piece->jsonFilename, piece->getImagePath(), this); window->show(); }); - setCentralWidget(pieceListView); + tabWidget->addTab(pieceListView, QStringLiteral("Pieces")); + + auto charModel = new CharacterModel(charactersDefinitionDirectory); + + auto charView = new QTableView(); + charView->setModel(charModel); + charView->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows); + charView->setSelectionMode(QAbstractItemView::SingleSelection); + + verticalHeader = charView->verticalHeader(); + verticalHeader->setSectionResizeMode(QHeaderView::ResizeToContents); + + horizontalHeader = charView->horizontalHeader(); + horizontalHeader->setSectionResizeMode(QHeaderView::Stretch); + + connect(charView, &QListView::clicked, this, [this, assetDirectory](const QModelIndex index) { + const auto &piece = index.data(Qt::UserRole).value(); + + auto window = new CharacterDetailWindow(piece->filename, this); + window->show(); + }); + + tabWidget->addTab(charView, QStringLiteral("Characters")); } \ No newline at end of file diff --git a/src/mainwindow.h b/src/mainwindow.h index c127b98..0a9288e 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -11,5 +11,9 @@ class MainWindow : public QMainWindow { Q_OBJECT public: - explicit MainWindow(const QDir &definitionDirectory, const QDir &assetDirectory, const QDir &dataDirectory, QWidget *parent = nullptr); + explicit MainWindow(const QDir &definitionDirectory, + const QDir &assetDirectory, + const QDir &dataDirectory, + const QDir &charactersDefinitionDirectory, + QWidget *parent = nullptr); };