From 2098cb4ba9d27b374da3ff71905c4053629292ae Mon Sep 17 00:00:00 2001 From: deeaitch Date: Mon, 16 Mar 2026 21:38:19 -0400 Subject: [PATCH] Initial commit. First draft v0.1 --- README.md | 212 +++++++++++++++++++++++++++ main.cpp | 14 ++ mainwindow.cpp | 375 +++++++++++++++++++++++++++++++++++++++++++++++ mainwindow.h | 153 +++++++++++++++++++ mainwindow.ui | 109 ++++++++++++++ yourls-ui.pro | 23 +++ yourlsclient.cpp | 108 ++++++++++++++ yourlsclient.h | 58 ++++++++ 8 files changed, 1052 insertions(+) create mode 100644 main.cpp create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h create mode 100644 mainwindow.ui create mode 100644 yourls-ui.pro create mode 100644 yourlsclient.cpp create mode 100644 yourlsclient.h diff --git a/README.md b/README.md index d6ba4fb..3a36180 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,214 @@ # yourls-ui +Small Qt tray utility for shortening URLs using the **YOURLS API**. + +The application sits in the system tray and allows quickly shortening the URL currently stored in the clipboard. + +It was designed as a lightweight helper for everyday use with a self-hosted YOURLS instance. + +--- + +# Features (v0.1) + +- System tray application +- Simple configuration window +- Uses YOURLS API with **signature authentication** +- Reads URL from clipboard +- Generates a short URL +- Copies the short URL back to clipboard +- Shows popup with clickable link +- Error reporting with API response + +--- + +# How it works + +1. Application starts +2. If configuration exists → it goes directly to the **system tray** +3. If configuration is missing → **settings window opens** + +From the tray menu you can run: + +``` +Cut URL +``` + +The application will: + +1. Read the current clipboard +2. Check if the content is a valid URL +3. Call the YOURLS API +4. Receive a shortened link +5. Copy the shortened link back to clipboard +6. Show a popup window with the result + +--- + +# Configuration + +The settings window contains: + +``` +API URL +API signature +``` + +Example: + +``` +API URL: +https://example.com/yourls-api.php + +Signature: +e484ea32c0 +``` + +⚠️ Important: + +The signature field must contain **only the signature value**, not: + +``` +signature=e484ea32c0 +``` + +Correct: + +``` +e484ea32c0 +``` + +--- + +# Tray menu + +Right click the tray icon to access the menu: + +``` +Cut URL +Help +About +Exit +``` + +Double clicking the tray icon opens the settings window. + +--- + +# Building + +Requirements: + +``` +Qt 5 +Qt Widgets +Qt Network +qmake +``` + +Build: + +```bash +qmake +make +``` + +Or open `yourls-ui.pro` in **Qt Creator** and build normally. + +--- + +# Project structure + +``` +main.cpp + Application entry point + +mainwindow.h / mainwindow.cpp + Main application window + Tray integration + UI logic + +mainwindow.ui + Qt Designer UI for settings window + +yourlsclient.h / yourlsclient.cpp + YOURLS API client + Handles HTTP requests and responses +``` + +The YOURLS API communication is encapsulated in `YourlsClient`. + +--- + +# API call + +The application performs a request similar to: + +``` +GET /yourls-api.php +?action=shorturl +&format=json +&signature= +&url= +``` + +Example: + +``` +https://example.com/yourls-api.php?action=shorturl&format=json&signature=ABC123&url=https://example.com +``` + +--- + +# Future ideas + +Possible improvements for future versions: + +### Hotkey support +Global hotkey to trigger URL shortening. + +### Clipboard watcher +Automatically detect URLs copied to clipboard. + +### Tray notifications +Use system notifications instead of popup dialog. + +### Recent history +Store last shortened URLs. + +### Custom alias +Allow specifying custom short URL alias. + +### Open YOURLS admin +Add tray menu entry for opening YOURLS admin panel. + +### Packaging +Provide: + +``` +AppImage +.deb package +``` + +--- + +# Version + +``` +v0.1 +``` + +Initial working release. + +--- + +# Author + +``` +deeaitch +``` + +--- + +# License + +GPL \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..2388e7f --- /dev/null +++ b/main.cpp @@ -0,0 +1,14 @@ +#include "mainwindow.h" + +#include +#include + +int main (int argc, char *argv[]) { + QApplication application (argc, argv); + QCoreApplication::setOrganizationName (QStringLiteral ("deeaitch")); + QCoreApplication::setApplicationName (QStringLiteral ("yourls-ui")); + + MainWindow window; + window.show(); + return application.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..8f5778b --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,375 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include "yourlsclient.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + + /** + * @brief Builds a clickable HTML link for dialogs. + * @param url URL to render. + * @return Rich text string. + */ + QString makeLinkHtml (const QUrl &url) { + const QString urlText = url.toString (QUrl::FullyDecoded); + return QStringLiteral ("%2") + .arg (url.toString (QUrl::FullyEncoded), urlText.toHtmlEscaped()); + } + + /** + * @brief Returns the organization name used by QSettings. + * @return Organization string. + */ + QString settingsOrganization() { + return QStringLiteral ("deeaitch"); + } + + /** + * @brief Returns the application name used by QSettings. + * @return Application string. + */ + QString settingsApplication() { + return QStringLiteral ("yourls-ui"); + } + +} // namespace + +/** + * @brief Constructs the main window. + * @param parent Parent widget. + */ +MainWindow::MainWindow (QWidget *parent) + : QMainWindow (parent) + , ui (new Ui::MainWindow) + , m_trayIcon (nullptr) + , m_trayMenu (nullptr) + , m_cutUrlAction (nullptr) + , m_settingsAction (nullptr) + , m_helpAction (nullptr) + , m_aboutAction (nullptr) + , m_exitAction (nullptr) + , m_yourlsClient (new YourlsClient (this)) { + ui->setupUi (this); + setWindowTitle (QStringLiteral ("yourls-ui settings")); + ui->apiKeyLineEdit->setEchoMode (QLineEdit::Password); + statusBar()->showMessage (QStringLiteral ("Configure YOURLS API settings and save.")); + + setupMenuActions(); + setupTray(); + setupConnections(); + loadSettings(); + + connect (m_yourlsClient, &YourlsClient::shortenSucceeded, this, &MainWindow::onShortenSucceeded); + connect (m_yourlsClient, &YourlsClient::shortenFailed, this, &MainWindow::onShortenFailed); + + if (hasValidSettings()) { + if (m_trayIcon != nullptr) + m_trayIcon->show(); + QTimer::singleShot (0, this, &MainWindow::hideToTray); + } +} + +/** + * @brief Destroys the main window. + */ +MainWindow::~MainWindow() { + delete ui; +} + +/** + * @brief Handles close requests. + * @param event Close event. + */ +void MainWindow::closeEvent (QCloseEvent *event) { + if (hasValidSettings() && m_trayIcon != nullptr && m_trayIcon->isVisible()) { + hideToTray(); + event->ignore(); + return; + } + + QMainWindow::closeEvent (event); +} + +/** + * @brief Saves settings from the UI. + */ +void MainWindow::saveSettings() { + const QString apiUrl = ui->urlLineEdit->text().trimmed(); + const QString apiKey = ui->apiKeyLineEdit->text().trimmed(); + + if (apiUrl.isEmpty()) { + QMessageBox::warning (this, QStringLiteral ("Missing API URL"), QStringLiteral ("Please enter the YOURLS API URL.")); + return; + } + + if (apiKey.isEmpty()) { + QMessageBox::warning (this, QStringLiteral ("Missing API key"), QStringLiteral ("Please enter the YOURLS API signature.")); + return; + } + + const QUrl parsedApiUrl (apiUrl); + if (!parsedApiUrl.isValid() || parsedApiUrl.scheme().isEmpty() || parsedApiUrl.host().isEmpty()) { + QMessageBox::warning (this, + QStringLiteral ("Invalid API URL"), + QStringLiteral ("The configured API URL does not look valid.")); + return; + } + + QSettings settings (settingsOrganization(), settingsApplication()); + settings.setValue (QStringLiteral ("api/url"), apiUrl); + settings.setValue (QStringLiteral ("api/signature"), apiKey); + settings.sync(); + + if (m_trayIcon != nullptr) + m_trayIcon->show(); + + statusBar()->showMessage (QStringLiteral ("Settings saved."), 2000); + hideToTray(); +} + +/** + * @brief Shows About text. + */ +void MainWindow::showAbout() { + QMessageBox::about (this, + QStringLiteral ("About yourls-ui"), + QStringLiteral ("yourls-ui\n\n" + "Small tray utility for shortening clipboard URLs with a YOURLS API.\n\n" + "Creator: deeaitch\n" + "Created: 2026-03-16")); +} + +/** + * @brief Shows Help text. + */ +void MainWindow::showHelp() { + QMessageBox::information (this, + QStringLiteral ("Help"), + QStringLiteral ("1. Open Settings and enter the YOURLS API URL and signature.\n" + "2. Save the settings. The application will stay in the tray.\n" + "3. Copy a full URL to the clipboard.\n" + "4. Right-click the tray icon and choose \"Cut URL\".\n" + "5. The short URL will be copied back to the clipboard and shown in a popup.\n\n" + "If something fails, the program shows detailed error information.")); +} + +/** + * @brief Shows the settings window. + */ +void MainWindow::showSettingsWindow() { + showNormal(); + raise(); + activateWindow(); +} + +/** + * @brief Hides the settings window to tray when possible. + */ +void MainWindow::hideToTray() { + if (!hasValidSettings()) { + showSettingsWindow(); + return; + } + + hide(); + if (m_trayIcon != nullptr && m_trayIcon->isVisible()) { + m_trayIcon->showMessage (QStringLiteral ("yourls-ui"), + QStringLiteral ("The application is running in the system tray."), + QSystemTrayIcon::Information, + 2000); + } +} + +/** + * @brief Exits the application. + */ +void MainWindow::exitApplication() { + QApplication::quit(); +} + +/** + * @brief Handles tray icon activation. + * @param reason Activation reason. + */ +void MainWindow::onTrayIconActivated (QSystemTrayIcon::ActivationReason reason) { + if (reason == QSystemTrayIcon::DoubleClick || reason == QSystemTrayIcon::Trigger) + showSettingsWindow(); +} + +/** + * @brief Reads a URL from the clipboard and shortens it. + */ +void MainWindow::cutUrlFromClipboard() { + if (!hasValidSettings()) { + QMessageBox::warning (this, + QStringLiteral ("Settings required"), + QStringLiteral ("Please configure the API URL and signature first.")); + showSettingsWindow(); + return; + } + + const QString clipboardText = QGuiApplication::clipboard()->text().trimmed(); + const QUrl longUrl = parseClipboardUrl (clipboardText); + if (!longUrl.isValid()) { + QMessageBox::warning (this, + QStringLiteral ("Invalid clipboard content"), + QStringLiteral ("The clipboard does not contain a valid URL.")); + return; + } + + YourlsSettings settings; + settings.apiUrl = ui->urlLineEdit->text().trimmed(); + settings.signature = ui->apiKeyLineEdit->text().trimmed(); + + if (m_trayIcon != nullptr) { + m_trayIcon->showMessage (QStringLiteral ("yourls-ui"), + QStringLiteral ("Requesting short URL..."), + QSystemTrayIcon::Information, + 1200); + } + + m_yourlsClient->shortenUrl (settings, longUrl); +} + +/** + * @brief Handles successful shortening. + * @param shortUrl Generated short URL. + */ +void MainWindow::onShortenSucceeded (const QUrl &shortUrl) { + QGuiApplication::clipboard()->setText (shortUrl.toString (QUrl::FullyDecoded)); + showShortUrlPopup (shortUrl); +} + +/** + * @brief Handles shortening failure. + * @param details Detailed error text. + */ +void MainWindow::onShortenFailed (const QString &details) { + QMessageBox::critical (this, QStringLiteral ("Failed to create short URL"), details); +} + +/** + * @brief Creates tray icon and tray menu. + */ +void MainWindow::setupTray() { + if (!QSystemTrayIcon::isSystemTrayAvailable()) { + QMessageBox::warning (this, + QStringLiteral ("System tray unavailable"), + QStringLiteral ("No system tray was detected. The application will stay as a normal window.")); + return; + } + + m_trayIcon = new QSystemTrayIcon (this); + m_trayIcon->setIcon (style()->standardIcon (QStyle::SP_DialogApplyButton)); + + m_trayMenu = new QMenu (this); + m_trayMenu->addAction (m_cutUrlAction); + m_trayMenu->addSeparator(); + m_trayMenu->addAction (m_settingsAction); + m_trayMenu->addAction (m_helpAction); + m_trayMenu->addAction (m_aboutAction); + m_trayMenu->addSeparator(); + m_trayMenu->addAction (m_exitAction); + + m_trayIcon->setContextMenu (m_trayMenu); +} + +/** + * @brief Connects UI signals to slots. + */ +void MainWindow::setupConnections() { + connect (ui->saveBtn, &QPushButton::clicked, this, &MainWindow::saveSettings); + + connect (ui->actionHelp, &QAction::triggered, this, &MainWindow::showHelp); + connect (ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout); + connect (ui->actionExit, &QAction::triggered, this, &MainWindow::exitApplication); + + connect (m_cutUrlAction, &QAction::triggered, this, &MainWindow::cutUrlFromClipboard); + connect (m_settingsAction, &QAction::triggered, this, &MainWindow::showSettingsWindow); + connect (m_helpAction, &QAction::triggered, this, &MainWindow::showHelp); + connect (m_aboutAction, &QAction::triggered, this, &MainWindow::showAbout); + connect (m_exitAction, &QAction::triggered, this, &MainWindow::exitApplication); + + if (m_trayIcon != nullptr) + connect (m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::onTrayIconActivated); +} + +/** + * @brief Loads persisted settings into the UI. + */ +void MainWindow::loadSettings() { + QSettings settings (settingsOrganization(), settingsApplication()); + ui->urlLineEdit->setText (settings.value (QStringLiteral ("api/url")).toString()); + ui->apiKeyLineEdit->setText (settings.value (QStringLiteral ("api/signature")).toString()); +} + +/** + * @brief Returns true when all required settings are present. + * @return True when the application can work from the tray. + */ +bool MainWindow::hasValidSettings() const { + return !ui->urlLineEdit->text().trimmed().isEmpty() + && !ui->apiKeyLineEdit->text().trimmed().isEmpty(); +} + +/** + * @brief Validates and normalizes URL text. + * @param text Input text. + * @return Parsed URL. + */ +QUrl MainWindow::parseClipboardUrl (const QString &text) const { + const QUrl url = QUrl::fromUserInput (text.trimmed()); + if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty()) + return QUrl(); + return url; +} + +/** + * @brief Shows a popup with the generated short URL. + * @param shortUrl Generated short URL. + */ +void MainWindow::showShortUrlPopup (const QUrl &shortUrl) { + QDialog dialog (this); + dialog.setWindowTitle (QStringLiteral ("Short URL created")); + + auto *layout = new QVBoxLayout (&dialog); + auto *label = new QLabel (QStringLiteral ("Generated short URL:
%1").arg (makeLinkHtml (shortUrl)), &dialog); + label->setTextFormat (Qt::RichText); + label->setTextInteractionFlags (Qt::TextBrowserInteraction); + label->setOpenExternalLinks (true); + layout->addWidget (label); + + auto *buttons = new QDialogButtonBox (QDialogButtonBox::Ok, &dialog); + QObject::connect (buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + layout->addWidget (buttons); + + dialog.exec(); +} + +/** + * @brief Creates tray/menu actions. + */ +void MainWindow::setupMenuActions() { + m_cutUrlAction = new QAction (QStringLiteral ("Cut URL"), this); + m_settingsAction = new QAction (QStringLiteral ("Settings"), this); + m_helpAction = new QAction (QStringLiteral ("Help"), this); + m_aboutAction = new QAction (QStringLiteral ("About"), this); + m_exitAction = new QAction (QStringLiteral ("Exit"), this); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..07aba8a --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,153 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE +namespace Ui { + class MainWindow; +} +QT_END_NAMESPACE + +class QAction; +class QCloseEvent; +class QMenu; +class YourlsClient; + +/** + * @brief Main application window used as the settings page. + * + * When valid settings exist the application starts in the tray. + * The tray menu contains the main workflow action, "Cut URL", + * plus Help, About, Settings and Exit. + */ +class MainWindow : public QMainWindow { + Q_OBJECT + + public: + /** + * @brief Constructs the main window. + * @param parent Parent widget. + */ + explicit MainWindow (QWidget *parent = nullptr); + + /** + * @brief Destroys the main window. + */ + ~MainWindow() override; + + protected: + /** + * @brief Handles close requests. + * @param event Close event. + */ + void closeEvent (QCloseEvent *event) override; + + private slots: + /** + * @brief Saves settings from the UI. + */ + void saveSettings(); + + /** + * @brief Shows About text. + */ + void showAbout(); + + /** + * @brief Shows Help text. + */ + void showHelp(); + + /** + * @brief Shows the settings window. + */ + void showSettingsWindow(); + + /** + * @brief Hides the settings window to tray when possible. + */ + void hideToTray(); + + /** + * @brief Exits the application. + */ + void exitApplication(); + + /** + * @brief Handles tray icon activation. + * @param reason Activation reason. + */ + void onTrayIconActivated (QSystemTrayIcon::ActivationReason reason); + + /** + * @brief Reads a URL from the clipboard and shortens it. + */ + void cutUrlFromClipboard(); + + /** + * @brief Handles successful shortening. + * @param shortUrl Generated short URL. + */ + void onShortenSucceeded (const QUrl &shortUrl); + + /** + * @brief Handles shortening failure. + * @param details Detailed error text. + */ + void onShortenFailed (const QString &details); + + private: + /** + * @brief Creates tray icon and tray menu. + */ + void setupTray(); + + /** + * @brief Connects UI signals to slots. + */ + void setupConnections(); + + /** + * @brief Loads persisted settings into the UI. + */ + void loadSettings(); + + /** + * @brief Returns true when all required settings are present. + * @return True when the application can work from the tray. + */ + bool hasValidSettings() const; + + /** + * @brief Validates and normalizes URL text. + * @param text Input text. + * @return Parsed URL. + */ + QUrl parseClipboardUrl (const QString &text) const; + + /** + * @brief Shows a popup with the generated short URL. + * @param shortUrl Generated short URL. + */ + void showShortUrlPopup (const QUrl &shortUrl); + + /** + * @brief Creates tray/menu actions. + */ + void setupMenuActions(); + + Ui::MainWindow *ui; + QSystemTrayIcon *m_trayIcon; + QMenu *m_trayMenu; + QAction *m_cutUrlAction; + QAction *m_settingsAction; + QAction *m_helpAction; + QAction *m_aboutAction; + QAction *m_exitAction; + YourlsClient *m_yourlsClient; +}; + +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..d91e0a8 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,109 @@ + + + MainWindow + + + + 0 + 0 + 420 + 204 + + + + yourls-ui settings + + + + + + + API URL + + + + + + + + + + API KEY + + + + + + + + + + Qt::Horizontal + + + + 258 + 20 + + + + + + + + Save + + + + + + + + + + 0 + 0 + 420 + 22 + + + + + File + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + Help + + + + + About + + + + + Exit + + + + + + diff --git a/yourls-ui.pro b/yourls-ui.pro new file mode 100644 index 0000000..7788149 --- /dev/null +++ b/yourls-ui.pro @@ -0,0 +1,23 @@ +QT += core gui network + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++17 +TARGET = yourls-ui +TEMPLATE = app + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + yourlsclient.cpp + +HEADERS += \ + mainwindow.h \ + yourlsclient.h + +FORMS += \ + mainwindow.ui + +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/yourlsclient.cpp b/yourlsclient.cpp new file mode 100644 index 0000000..bf34dba --- /dev/null +++ b/yourlsclient.cpp @@ -0,0 +1,108 @@ +#include "yourlsclient.h" + +#include +#include +#include +#include +#include +#include + +/** + * @brief Constructs the client. + * @param parent QObject parent. + */ +YourlsClient::YourlsClient (QObject *parent) + : QObject (parent) + , m_networkAccessManager (new QNetworkAccessManager (this)) { +} + +/** + * @brief Requests a short URL from the YOURLS API. + * @param settings API endpoint and signature. + * @param longUrl Source URL to shorten. + */ +void YourlsClient::shortenUrl (const YourlsSettings &settings, const QUrl &longUrl) { + if (settings.apiUrl.trimmed().isEmpty()) { + emit shortenFailed (QStringLiteral ("API URL is empty.")); + return; + } + + if (settings.signature.trimmed().isEmpty()) { + emit shortenFailed (QStringLiteral ("API signature is empty.")); + return; + } + + if (!longUrl.isValid() || longUrl.scheme().isEmpty() || longUrl.host().isEmpty()) { + emit shortenFailed (QStringLiteral ("Clipboard does not contain a valid URL.")); + return; + } + + QUrl requestUrl (settings.apiUrl); + if (!requestUrl.isValid()) { + emit shortenFailed (QStringLiteral ("Configured API URL is invalid: %1").arg (settings.apiUrl)); + return; + } + + QUrlQuery query; + query.addQueryItem (QStringLiteral ("signature"), settings.signature); + query.addQueryItem (QStringLiteral ("action"), QStringLiteral ("shorturl")); + query.addQueryItem (QStringLiteral ("format"), QStringLiteral ("json")); + query.addQueryItem (QStringLiteral ("url"), longUrl.toString (QUrl::FullyDecoded)); + requestUrl.setQuery (query); + + QNetworkRequest request (requestUrl); + request.setHeader (QNetworkRequest::UserAgentHeader, QStringLiteral ("yourls-ui/1.0")); + + QNetworkReply *reply = m_networkAccessManager->get (request); + + connect (reply, &QNetworkReply::finished, this, [this, reply]() { + const QByteArray body = reply->readAll(); + + if (reply->error() != QNetworkReply::NoError) { + const QString details = QStringLiteral ("Network error: %1\nHTTP status: %2\nResponse body:\n%3") + .arg (reply->errorString(), + reply->attribute (QNetworkRequest::HttpStatusCodeAttribute).toString(), + QString::fromUtf8 (body)); + reply->deleteLater(); + emit shortenFailed (details); + return; + } + + QJsonParseError parseError; + const QJsonDocument jsonDocument = QJsonDocument::fromJson (body, &parseError); + if (parseError.error != QJsonParseError::NoError || !jsonDocument.isObject()) { + const QString details = QStringLiteral ("Failed to parse YOURLS response as JSON: %1\nResponse body:\n%2") + .arg (parseError.errorString(), QString::fromUtf8 (body)); + reply->deleteLater(); + emit shortenFailed (details); + return; + } + + const QJsonObject object = jsonDocument.object(); + const QString status = object.value (QStringLiteral ("status")).toString(); + const QString shortUrlText = object.value (QStringLiteral ("shorturl")).toString(); + + if (status.compare (QStringLiteral ("success"), Qt::CaseInsensitive) != 0 || shortUrlText.isEmpty()) { + QStringList details; + details << QStringLiteral ("YOURLS returned an error."); + if (object.contains (QStringLiteral ("message"))) + details << QStringLiteral ("Message: %1").arg (object.value (QStringLiteral ("message")).toString()); + if (object.contains (QStringLiteral ("code"))) + details << QStringLiteral ("Code: %1").arg (object.value (QStringLiteral ("code")).toVariant().toString()); + details << QStringLiteral ("Raw response:\n%1").arg (QString::fromUtf8 (body)); + reply->deleteLater(); + emit shortenFailed (details.join (QStringLiteral ("\n"))); + return; + } + + const QUrl shortUrl (shortUrlText); + if (!shortUrl.isValid()) { + reply->deleteLater(); + emit shortenFailed (QStringLiteral ("YOURLS returned an invalid short URL: %1").arg (shortUrlText)); + return; + } + + reply->deleteLater(); + emit shortenSucceeded (shortUrl); + }); +} diff --git a/yourlsclient.h b/yourlsclient.h new file mode 100644 index 0000000..68c4ad8 --- /dev/null +++ b/yourlsclient.h @@ -0,0 +1,58 @@ +#ifndef YOURLSCLIENT_H +#define YOURLSCLIENT_H + +#include +#include + +class QNetworkAccessManager; + +/** + * @brief Small value object describing YOURLS API settings. + */ +struct YourlsSettings { + QString apiUrl; + QString signature; +}; + +/** + * @brief Client responsible for communication with the YOURLS API. + * + * The class builds a signed YOURLS request, sends it over HTTP(S), + * parses the JSON response and returns either a short URL or a detailed + * error string. + */ +class YourlsClient : public QObject { + Q_OBJECT + + public: + /** + * @brief Constructs the client. + * @param parent QObject parent. + */ + explicit YourlsClient (QObject *parent = nullptr); + + /** + * @brief Requests a short URL from the YOURLS API. + * @param settings API endpoint and signature. + * @param longUrl Source URL to shorten. + */ + void shortenUrl (const YourlsSettings &settings, const QUrl &longUrl); + + signals: + /** + * @brief Emitted when the short URL has been created successfully. + * @param shortUrl Generated short URL. + */ + void shortenSucceeded (const QUrl &shortUrl); + + /** + * @brief Emitted when the request fails. + * @param details Human-readable error details. + */ + void shortenFailed (const QString &details); + + private: + QNetworkAccessManager *m_networkAccessManager; +}; + +#endif // YOURLSCLIENT_H