diff --git a/CMakeLists.txt b/CMakeLists.txt index 2812592..67d11a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ find_package(Qt5 COMPONENTS Gui Widgets Xml + Network REQUIRED) add_subdirectory(minizip-ng) @@ -21,15 +22,17 @@ qt5_add_resources(RESOURCE_ADDED resources.qrc) add_executable(SnakeQT ${RESOURCE_ADDED} src/main.cpp src/jeu.cpp - src/inmemoryzip.cpp src/map.cpp src/constants.hpp + src/inmemoryzip.cpp + src/network_manager.cpp src/screens/snakewindow.cpp src/screens/mainmenu.hpp src/screens/gamescreen.cpp src/screens/editorscreen.cpp src/screens/snakewindow.cpp src/screens/gamearea.cpp + src/screens/browsemapscreen.cpp ) target_link_libraries(SnakeQT @@ -37,5 +40,6 @@ target_link_libraries(SnakeQT Qt5::Gui Qt5::Widgets Qt5::Xml + Qt5::Network minizip ) diff --git a/images/cloud.png b/images/cloud.png new file mode 100644 index 0000000..31e80b3 Binary files /dev/null and b/images/cloud.png differ diff --git a/resources.qrc b/resources.qrc index 2470760..d981965 100644 --- a/resources.qrc +++ b/resources.qrc @@ -5,5 +5,6 @@ images/ground.png images/wall.png images/apple.png + images/cloud.png \ No newline at end of file diff --git a/src/network_manager.cpp b/src/network_manager.cpp new file mode 100644 index 0000000..e727e33 --- /dev/null +++ b/src/network_manager.cpp @@ -0,0 +1,25 @@ +#include "network_manager.hpp" + +FileDownloader::FileDownloader(const QUrl &url, QObject *parent) : QObject(parent), m_WebCtrl(this) { + connect(&m_WebCtrl, SIGNAL(finished(QNetworkReply*)), this, SLOT(fileDownloaded(QNetworkReply*))); + + const QNetworkRequest request(url); + m_WebCtrl.get(request); +} + +FileDownloader::~FileDownloader() { +} + +void FileDownloader::fileDownloaded(QNetworkReply *pReply) { + if (pReply->error() != QNetworkReply::NoError) { + qDebug() << "Network error: " << pReply->errorString(); + } else { + m_DownloadedData = pReply->readAll(); + emit downloaded(); + } + pReply->deleteLater(); +} + +QByteArray FileDownloader::downloadedData() const { + return m_DownloadedData; +} diff --git a/src/network_manager.hpp b/src/network_manager.hpp new file mode 100644 index 0000000..c0ddfb2 --- /dev/null +++ b/src/network_manager.hpp @@ -0,0 +1,31 @@ +#ifndef NETWORK_MANAGER_H +#define NETWORK_MANAGER_H + +#include +#include +#include +#include +#include + +class FileDownloader final : public QObject { + Q_OBJECT + +public: + explicit FileDownloader(const QUrl &url, QObject *parent = nullptr); + + ~FileDownloader() override; + + QByteArray downloadedData() const; + +signals: + void downloaded(); + +private slots: + void fileDownloaded(QNetworkReply *pReply); + +private: + QNetworkAccessManager m_WebCtrl; + QByteArray m_DownloadedData; +}; + +#endif // NETWORK_MANAGER_H diff --git a/src/screens/browsemapscreen.cpp b/src/screens/browsemapscreen.cpp new file mode 100644 index 0000000..b376087 --- /dev/null +++ b/src/screens/browsemapscreen.cpp @@ -0,0 +1,94 @@ +#include "browsemapscreen.hpp" + + +/** + * Construct map browser screen. + */ +BrowseMapScreen::BrowseMapScreen(QWidget *parent): QWidget(parent) { + const auto layout = new QVBoxLayout(this); + + const auto backButton = new QPushButton("Back", this); + mapList = new QListWidget(this); + loadButton = new QPushButton("Play", this); + + layout->addWidget(backButton); + layout->addWidget(mapList); + layout->addWidget(loadButton); + + connect(backButton, &QPushButton::clicked, this, &BrowseMapScreen::backClicked); + connect(loadButton, &QPushButton::clicked, this, &BrowseMapScreen::loadClicked); + connect(mapList, &QListWidget::itemSelectionChanged, this, &BrowseMapScreen::mapSelectionChanged); + + // Load maps + QDirIterator it("maps", QDir::Files); + QPixmap transparentPixmap(24, 24); + transparentPixmap.fill(Qt::transparent); + const QIcon spacerIcon(transparentPixmap); + while (it.hasNext()) { + it.next(); + const auto item = new QListWidgetItem(it.fileName()); + item->setIcon(spacerIcon); + item->setData(Qt::UserRole, true); + mapList->addItem(item); + } + + loadOnlineMaps(); +} + +void BrowseMapScreen::loadOnlineMaps() { + downloader = new FileDownloader(QUrl("https://snakeqt.denisd3d.fr/maps"), this); + connect(downloader, &FileDownloader::downloaded, this, &BrowseMapScreen::onlineMapListDownloaded); +} + +void BrowseMapScreen::backClicked() { + emit back(); +} + +void BrowseMapScreen::loadClicked() { + if (mapList->currentItem() != nullptr) { + if (!mapList->currentItem()->data(Qt::UserRole).toBool()) { + auto mapName = mapList->currentItem()->text().replace(".skm", ""); + downloader = new FileDownloader(QUrl("https://snakeqt.denisd3d.fr/maps/" + mapName), + this); + connect(downloader, &FileDownloader::downloaded, [this, mapName]() { + QFile file("maps/" + mapName + ".skm"); + file.open(QIODevice::WriteOnly); + file.write(downloader->downloadedData()); + file.close(); + emit load("maps/" + mapName + ".skm"); + }); + } else { + emit load("maps/" + mapList->currentItem()->text()); + } + } +} + +void BrowseMapScreen::mapSelectionChanged() { + if (mapList->currentItem() != nullptr) { + loadButton->setText(mapList->currentItem()->data(Qt::UserRole).toBool() ? "Play" : "Download and Play"); + } +} + +void BrowseMapScreen::onlineMapListDownloaded() { + const QByteArray data = downloader->downloadedData(); + const QJsonDocument doc = QJsonDocument::fromJson(data); + const QIcon cloudIcon(":/images/cloud.png"); + QJsonArray array = doc.array(); + for (const auto &value: array) { + QString mapName = value.toObject().value("id").toString(); + bool found = false; + for (int i = 0; i < mapList->count(); i++) { + if (mapList->item(i)->text() == mapName) { + found = true; + break; + } + } + + if (!found) { + const auto item = new QListWidgetItem(mapName); + item->setData(Qt::UserRole, false); + item->setIcon(cloudIcon); + mapList->addItem(item); + } + } +} diff --git a/src/screens/browsemapscreen.hpp b/src/screens/browsemapscreen.hpp new file mode 100644 index 0000000..59bd1aa --- /dev/null +++ b/src/screens/browsemapscreen.hpp @@ -0,0 +1,37 @@ +#ifndef BROWSEMAPSCREEN_HPP +#define BROWSEMAPSCREEN_HPP + +#include +#include + +#include "../network_manager.hpp" + +class BrowseMapScreen final : public QWidget { + Q_OBJECT + QListWidget *mapList; + QPushButton *loadButton; + FileDownloader *downloader; + + void loadOnlineMaps(); + + void updateMapList(); + +public: + explicit BrowseMapScreen(QWidget *parent = nullptr); + +public slots: + void backClicked(); + + void loadClicked(); + + void onlineMapListDownloaded(); + + void mapSelectionChanged(); + +signals: + void back(); + + void load(const QString &mapName); +}; + +#endif //BROWSEMAPSCREEN_HPP diff --git a/src/screens/mainmenu.hpp b/src/screens/mainmenu.hpp index 15ace48..0498614 100644 --- a/src/screens/mainmenu.hpp +++ b/src/screens/mainmenu.hpp @@ -12,12 +12,15 @@ class MainMenu final : public QWidget { const auto startButton = new QPushButton("Start Game", this); startButton->setDisabled(true); + const auto browseMapButton = new QPushButton("Browse Map", this); const auto exitButton = new QPushButton("Exit", this); layout->addWidget(startButton); + layout->addWidget(browseMapButton); layout->addWidget(exitButton); connect(startButton, &QPushButton::clicked, this, &MainMenu::startGameClicked); + connect(browseMapButton, &QPushButton::clicked, this, &MainMenu::browseMapClicked); connect(exitButton, &QPushButton::clicked, this, &MainMenu::exitClicked); } @@ -25,6 +28,8 @@ class MainMenu final : public QWidget { void startGameClicked(); void exitClicked(); + + void browseMapClicked(); }; diff --git a/src/screens/snakewindow.cpp b/src/screens/snakewindow.cpp index 19bf4a1..9125f0a 100644 --- a/src/screens/snakewindow.cpp +++ b/src/screens/snakewindow.cpp @@ -14,6 +14,7 @@ SnakeWindow::SnakeWindow(QWidget *pParent, Qt::WindowFlags flags) mainMenu = new MainMenu(this); connect(mainMenu, &MainMenu::startGameClicked, this, &SnakeWindow::handleStartGameClicked); + connect(mainMenu, &MainMenu::browseMapClicked, this, &SnakeWindow::handleBrowseMapClicked); connect(mainMenu, &MainMenu::exitClicked, this, &SnakeWindow::handleExitClicked); stackedWidget->addWidget(mainMenu); @@ -91,6 +92,22 @@ void SnakeWindow::handleStartGameClicked() { stackedWidget->setCurrentWidget(gameScreen); } +void SnakeWindow::handleBrowseMapClicked() { + browseMapScreen = new BrowseMapScreen(this); + stackedWidget->addWidget(browseMapScreen); + stackedWidget->setCurrentWidget(browseMapScreen); + + connect(browseMapScreen, &BrowseMapScreen::back, this, [this] { + stackedWidget->setCurrentWidget(mainMenu); + }); + + connect(browseMapScreen, &BrowseMapScreen::load, this, [this](const QString &fileName) { + gameScreen = new GameScreen(this, fileName); + stackedWidget->addWidget(gameScreen); + stackedWidget->setCurrentWidget(gameScreen); + }); +} + void SnakeWindow::handleExitClicked() { // Exit the application when the "Exit" button is clicked QApplication::quit(); diff --git a/src/screens/snakewindow.hpp b/src/screens/snakewindow.hpp index bedd59f..ddf9b0c 100644 --- a/src/screens/snakewindow.hpp +++ b/src/screens/snakewindow.hpp @@ -4,6 +4,7 @@ #include #include +#include "browsemapscreen.hpp" #include "editorscreen.hpp" #include "mainmenu.hpp" #include "gamescreen.hpp" @@ -15,6 +16,7 @@ class SnakeWindow final : public QMainWindow { QStackedWidget *stackedWidget; MainMenu *mainMenu; GameScreen *gameScreen{}; + BrowseMapScreen *browseMapScreen{}; EditorScreen *editorScreen{}; public: @@ -23,6 +25,8 @@ class SnakeWindow final : public QMainWindow { public slots: void handleStartGameClicked(); + void handleBrowseMapClicked(); + static void handleExitClicked(); void toggleFullScreen() {