Initial commit. First draft v0.1
This commit is contained in:
212
README.md
212
README.md
@@ -1,2 +1,214 @@
|
|||||||
# yourls-ui
|
# 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=<signature>
|
||||||
|
&url=<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
|
||||||
14
main.cpp
Normal file
14
main.cpp
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#include "mainwindow.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
375
mainwindow.cpp
Normal file
375
mainwindow.cpp
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
#include "mainwindow.h"
|
||||||
|
#include "ui_mainwindow.h"
|
||||||
|
#include "yourlsclient.h"
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QCloseEvent>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QStatusBar>
|
||||||
|
#include <QStyle>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
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 ("<a href=\"%1\">%2</a>")
|
||||||
|
.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:<br>%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);
|
||||||
|
}
|
||||||
153
mainwindow.h
Normal file
153
mainwindow.h
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
#ifndef MAINWINDOW_H
|
||||||
|
#define MAINWINDOW_H
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include <QSystemTrayIcon>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
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
|
||||||
109
mainwindow.ui
Normal file
109
mainwindow.ui
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>420</width>
|
||||||
|
<height>204</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>yourls-ui settings</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="apiUrlLoabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>API URL</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1" colspan="2">
|
||||||
|
<widget class="QLineEdit" name="urlLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="apiKeyLobal">
|
||||||
|
<property name="text">
|
||||||
|
<string>API KEY</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1" colspan="2">
|
||||||
|
<widget class="QLineEdit" name="apiKeyLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>258</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QPushButton" name="saveBtn">
|
||||||
|
<property name="text">
|
||||||
|
<string>Save</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
|
<widget class="QMenuBar" name="menubar">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>420</width>
|
||||||
|
<height>22</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<widget class="QMenu" name="menuFile">
|
||||||
|
<property name="title">
|
||||||
|
<string>File</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionHelp"/>
|
||||||
|
<addaction name="actionAbout"/>
|
||||||
|
<addaction name="actionExit"/>
|
||||||
|
</widget>
|
||||||
|
<addaction name="menuFile"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QToolBar" name="toolBar">
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>toolBar</string>
|
||||||
|
</property>
|
||||||
|
<attribute name="toolBarArea">
|
||||||
|
<enum>TopToolBarArea</enum>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="toolBarBreak">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
|
<action name="actionHelp">
|
||||||
|
<property name="text">
|
||||||
|
<string>Help</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionAbout">
|
||||||
|
<property name="text">
|
||||||
|
<string>About</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionExit">
|
||||||
|
<property name="text">
|
||||||
|
<string>Exit</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
23
yourls-ui.pro
Normal file
23
yourls-ui.pro
Normal file
@@ -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
|
||||||
108
yourlsclient.cpp
Normal file
108
yourlsclient.cpp
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#include "yourlsclient.h"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
});
|
||||||
|
}
|
||||||
58
yourlsclient.h
Normal file
58
yourlsclient.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#ifndef YOURLSCLIENT_H
|
||||||
|
#define YOURLSCLIENT_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user