Start adding character support, creation actions and more
This commit is contained in:
parent
aaaa489116
commit
630a373cca
16 changed files with 558 additions and 62 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
#include <QJsonDocument>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
|
||||
ArtConfigWindow::ArtConfigWindow(const QString &filename, const QString &definitionDirectory, const QString &assetDirectory, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#include "imagelabel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QCoroNetwork>
|
||||
#include <QCoroTask>
|
||||
#include <QDateEdit>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
@ -15,10 +17,11 @@
|
|||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QLineEdit>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
|
||||
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> {
|
||||
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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -5,21 +5,24 @@
|
|||
#include "artmodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QCoroNetwork>
|
||||
#include <QCoroTask>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QIcon>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QPixmap>
|
||||
#include <QtConcurrent>
|
||||
|
||||
ArtModel::ArtModel(const QDir &definitionDirectory, const QDir &assetDirectory, QObject *parent)
|
||||
: QAbstractTableModel(parent)
|
||||
{
|
||||
piecesFuture = new QFutureWatcher<ArtPiece>(this);
|
||||
connect(piecesFuture, &QFutureWatcher<ArtPiece>::resultReadyAt, this, &ArtModel::pieceFinished);
|
||||
connect(piecesFuture, &QFutureWatcher<ArtPiece>::finished, this, &ArtModel::finished);
|
||||
piecesFuture = new QFutureWatcher<ArtPiece *>(this);
|
||||
connect(piecesFuture, &QFutureWatcher<ArtPiece *>::resultReadyAt, this, &ArtModel::pieceFinished);
|
||||
connect(piecesFuture, &QFutureWatcher<ArtPiece *>::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<int>(m_artPieces.size()), static_cast<int>(m_artPieces.size() + 1));
|
||||
|
||||
m_artPieces.push_back({});
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
const std::function<ArtPiece(const PieceInformation &info)> loadPiece = [](const PieceInformation &info) -> ArtPiece {
|
||||
ArtPiece p(info.definition, info.asset);
|
||||
const std::function<QCoro::Task<ArtPiece *>(const PieceInformation &info)> loadPiece = [](const PieceInformation &info) -> QCoro::Task<ArtPiece *> {
|
||||
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<int>(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<int>(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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,11 @@
|
|||
#include <QFutureWatcher>
|
||||
#include <QImage>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
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<ArtPiece> m_artPieces;
|
||||
QVector<ArtPiece *> m_artPieces;
|
||||
|
||||
private:
|
||||
void pieceFinished(int index);
|
||||
void finished();
|
||||
|
||||
QFutureWatcher<ArtPiece> *piecesFuture;
|
||||
QFutureWatcher<ArtPiece *> *piecesFuture;
|
||||
};
|
||||
|
|
148
src/characterdetailwindow.cpp
Normal file
148
src/characterdetailwindow.cpp
Normal file
|
@ -0,0 +1,148 @@
|
|||
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "characterdetailwindow.h"
|
||||
#include "imagelabel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
|
||||
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();
|
||||
}
|
33
src/characterdetailwindow.h
Normal file
33
src/characterdetailwindow.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDateEdit>
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QJsonObject>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QStringListModel>
|
||||
#include <QTextEdit>
|
||||
|
||||
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;
|
||||
};
|
135
src/charactermodel.cpp
Normal file
135
src/charactermodel.cpp
Normal file
|
@ -0,0 +1,135 @@
|
|||
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "charactermodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QCoroTask>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QPixmap>
|
||||
#include <QtConcurrent>
|
||||
|
||||
CharacterModel::CharacterModel(const QDir &definitionDirectory, QObject *parent)
|
||||
: QAbstractTableModel(parent)
|
||||
{
|
||||
charactersFuture = new QFutureWatcher<Character *>(this);
|
||||
connect(charactersFuture, &QFutureWatcher<Character *>::resultReadyAt, this, &CharacterModel::characterFinished);
|
||||
connect(charactersFuture, &QFutureWatcher<Character *>::finished, this, &CharacterModel::finished);
|
||||
|
||||
struct CharacterInformation {
|
||||
QString definition;
|
||||
};
|
||||
QVector<CharacterInformation> 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<int>(m_characters.size()), static_cast<int>(m_characters.size() + 1));
|
||||
|
||||
m_characters.push_back({});
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
const std::function<QCoro::Task<Character *>(const CharacterInformation &info)> loadPiece =
|
||||
[](const CharacterInformation &info) -> QCoro::Task<Character *> {
|
||||
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<int>(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<int>(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();
|
||||
}
|
||||
}
|
48
src/charactermodel.h
Normal file
48
src/charactermodel.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractTableModel>
|
||||
#include <QDir>
|
||||
#include <QFutureWatcher>
|
||||
#include <QImage>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
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<Character *> m_characters;
|
||||
|
||||
private:
|
||||
void characterFinished(int index);
|
||||
void finished();
|
||||
|
||||
QFutureWatcher<Character *> *charactersFuture;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<QPersistentModelIndex> checkedItems;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -10,24 +10,59 @@
|
|||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QMenuBar>
|
||||
#include <QPointer>
|
||||
#include <QTableView>
|
||||
#include <QToolBar>
|
||||
|
||||
#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<QDialog> 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<ArtPiece *>();
|
||||
|
||||
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<Character *>();
|
||||
|
||||
auto window = new CharacterDetailWindow(piece->filename, this);
|
||||
window->show();
|
||||
});
|
||||
|
||||
tabWidget->addTab(charView, QStringLiteral("Characters"));
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue