applied clang format
This commit is contained in:
1 parent
39ab7acc08
commit
73d82e9562
10 files changed
+583
-609
No files matched your search
+32
-34
@@ -15,48 +15,46 @@
|
||||
// Keep the result window focused while taking ownership of the clipboard.
|
||||
class ClipboardResultDialog : public QDialog {
|
||||
public:
|
||||
ClipboardResultDialog (const QUrl &url, bool requiresFocus, QWidget *parent = nullptr)
|
||||
: QDialog (parent), m_text (url.toString (QUrl::FullyEncoded)),
|
||||
m_requiresFocus (requiresFocus), m_status (new QLabel (this)) {
|
||||
m_status->setText (QStringLiteral ("Activate this window to copy the short URL."));
|
||||
m_status->setWordWrap (true);
|
||||
}
|
||||
ClipboardResultDialog(const QUrl &url, bool requiresFocus, QWidget *parent = nullptr)
|
||||
: QDialog(parent), m_text(url.toString(QUrl::FullyEncoded)),
|
||||
m_requiresFocus(requiresFocus), m_status(new QLabel(this)) {
|
||||
m_status->setText(QStringLiteral("Activate this window to copy the short URL."));
|
||||
m_status->setWordWrap(true);
|
||||
}
|
||||
|
||||
QLabel *copyStatusLabel() const { return m_status; }
|
||||
QLabel *copyStatusLabel() const { return m_status; }
|
||||
|
||||
bool copyShortUrl() {
|
||||
if (m_requiresFocus && !isActiveWindow())
|
||||
return false;
|
||||
auto *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText (m_text, QClipboard::Clipboard);
|
||||
const bool copied = clipboard->text (QClipboard::Clipboard) == m_text
|
||||
&& (!QGuiApplication::platformName().startsWith (QStringLiteral ("wayland"))
|
||||
|| clipboard->ownsClipboard());
|
||||
m_status->setText (copied ? QStringLiteral ("Short URL copied to clipboard.")
|
||||
: QStringLiteral ("Could not copy the short URL. Click OK to retry."));
|
||||
bool copyShortUrl() {
|
||||
if (m_requiresFocus && !isActiveWindow())
|
||||
return false;
|
||||
auto *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText(m_text, QClipboard::Clipboard);
|
||||
const bool copied = clipboard->text(QClipboard::Clipboard) == m_text && (!QGuiApplication::platformName().startsWith(QStringLiteral("wayland")) || clipboard->ownsClipboard());
|
||||
m_status->setText(copied ? QStringLiteral("Short URL copied to clipboard.")
|
||||
: QStringLiteral("Could not copy the short URL. Click OK to retry."));
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] short URL write, active:" << isActiveWindow()
|
||||
<< "owns clipboard:" << clipboard->ownsClipboard() << "verified:" << copied;
|
||||
qDebug() << "[clipboard] short URL write, active:" << isActiveWindow()
|
||||
<< "owns clipboard:" << clipboard->ownsClipboard() << "verified:" << copied;
|
||||
#endif
|
||||
return copied;
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
protected:
|
||||
void changeEvent (QEvent *event) override {
|
||||
QDialog::changeEvent (event);
|
||||
if (event->type() == QEvent::ActivationChange && isActiveWindow()) {
|
||||
// Let Qt finish processing the focus event and its Wayland input serial.
|
||||
QTimer::singleShot (0, this, [this]() {
|
||||
if (isActiveWindow())
|
||||
copyShortUrl();
|
||||
});
|
||||
void changeEvent(QEvent *event) override {
|
||||
QDialog::changeEvent(event);
|
||||
if (event->type() == QEvent::ActivationChange && isActiveWindow()) {
|
||||
// Let Qt finish processing the focus event and its Wayland input serial.
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
if (isActiveWindow())
|
||||
copyShortUrl();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_text;
|
||||
bool m_requiresFocus;
|
||||
QLabel *m_status;
|
||||
QString m_text;
|
||||
bool m_requiresFocus;
|
||||
QLabel *m_status;
|
||||
};
|
||||
|
||||
#endif // CLIPBOARDRESULTDIALOG_H
|
||||
#endif// CLIPBOARDRESULTDIALOG_H
|
||||
+51
-52
@@ -10,64 +10,63 @@
|
||||
|
||||
namespace ClipboardUrl {
|
||||
|
||||
inline QUrl parseText (const QString &text) {
|
||||
QString urlText = text.trimmed();
|
||||
inline QUrl parseText(const QString &text) {
|
||||
QString urlText = text.trimmed();
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] raw text:" << text
|
||||
<< "UTF-8 hex:" << text.toUtf8().toHex();
|
||||
qDebug() << "[clipboard] raw text:" << text
|
||||
<< "UTF-8 hex:" << text.toUtf8().toHex();
|
||||
#endif
|
||||
if (urlText.size() >= 2) {
|
||||
const QChar first = urlText.front();
|
||||
const QChar last = urlText.back();
|
||||
if ((first == '\'' && last == '\'') || (first == '"' && last == '"')
|
||||
|| (first == '<' && last == '>'))
|
||||
urlText = urlText.mid (1, urlText.size() - 2).trimmed();
|
||||
}
|
||||
if (urlText.size() >= 2) {
|
||||
const QChar first = urlText.front();
|
||||
const QChar last = urlText.back();
|
||||
if ((first == '\'' && last == '\'') || (first == '"' && last == '"') || (first == '<' && last == '>'))
|
||||
urlText = urlText.mid(1, urlText.size() - 2).trimmed();
|
||||
}
|
||||
|
||||
static const QRegularExpression markdownLink (
|
||||
QStringLiteral ("\\A\\[[^\\r\\n]*\\]\\((https?://[^\\r\\n]+)\\)\\z"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
const auto match = markdownLink.match (urlText);
|
||||
if (match.hasMatch())
|
||||
urlText = match.captured (1);
|
||||
static const QRegularExpression markdownLink(
|
||||
QStringLiteral("\\A\\[[^\\r\\n]*\\]\\((https?://[^\\r\\n]+)\\)\\z"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
const auto match = markdownLink.match(urlText);
|
||||
if (match.hasMatch())
|
||||
urlText = match.captured(1);
|
||||
|
||||
const QUrl url = QUrl::fromUserInput (urlText);
|
||||
const QUrl url = QUrl::fromUserInput(urlText);
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] normalized text:" << urlText
|
||||
<< "parsed URL:" << url << "valid:" << url.isValid()
|
||||
<< "scheme:" << url.scheme() << "host:" << url.host()
|
||||
<< "parse error:" << url.errorString();
|
||||
qDebug() << "[clipboard] normalized text:" << urlText
|
||||
<< "parsed URL:" << url << "valid:" << url.isValid()
|
||||
<< "scheme:" << url.scheme() << "host:" << url.host()
|
||||
<< "parse error:" << url.errorString();
|
||||
#endif
|
||||
if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty())
|
||||
return QUrl();
|
||||
return url;
|
||||
}
|
||||
|
||||
inline QUrl fromMimeData (const QMimeData *mimeData) {
|
||||
if (mimeData == nullptr) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] MIME data unavailable";
|
||||
#endif
|
||||
return QUrl();
|
||||
}
|
||||
|
||||
const auto urls = mimeData->urls();
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] MIME formats:" << mimeData->formats()
|
||||
<< "URL entries:" << urls;
|
||||
#endif
|
||||
if (urls.size() == 1) {
|
||||
const QUrl url = urls.first();
|
||||
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] using MIME URL:" << url;
|
||||
#endif
|
||||
return url;
|
||||
if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty())
|
||||
return QUrl();
|
||||
return url;
|
||||
}
|
||||
}
|
||||
return parseText (mimeData->text());
|
||||
}
|
||||
|
||||
} // namespace ClipboardUrl
|
||||
inline QUrl fromMimeData(const QMimeData *mimeData) {
|
||||
if (mimeData == nullptr) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] MIME data unavailable";
|
||||
#endif
|
||||
return QUrl();
|
||||
}
|
||||
|
||||
#endif // CLIPBOARDURL_H
|
||||
const auto urls = mimeData->urls();
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] MIME formats:" << mimeData->formats()
|
||||
<< "URL entries:" << urls;
|
||||
#endif
|
||||
if (urls.size() == 1) {
|
||||
const QUrl url = urls.first();
|
||||
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] using MIME URL:" << url;
|
||||
#endif
|
||||
return url;
|
||||
}
|
||||
}
|
||||
return parseText(mimeData->text());
|
||||
}
|
||||
|
||||
}// namespace ClipboardUrl
|
||||
|
||||
#endif// CLIPBOARDURL_H
|
||||
@@ -3,12 +3,12 @@
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
|
||||
int main (int argc, char *argv[]) {
|
||||
QApplication application (argc, argv);
|
||||
QCoreApplication::setOrganizationName (QStringLiteral ("deeaitch"));
|
||||
QCoreApplication::setApplicationName (QStringLiteral ("yourls-ui"));
|
||||
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();
|
||||
MainWindow window;
|
||||
window.show();
|
||||
return application.exec();
|
||||
}
|
||||
+228
-241
@@ -1,6 +1,6 @@
|
||||
#include "mainwindow.h"
|
||||
#include "clipboardurl.h"
|
||||
#include "clipboardresultdialog.h"
|
||||
#include "clipboardurl.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include "yourlsclient.h"
|
||||
|
||||
@@ -24,139 +24,130 @@
|
||||
|
||||
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.toDisplayString();
|
||||
return QStringLiteral ("<a href=\"%1\">%2</a>")
|
||||
.arg (url.toString (QUrl::FullyEncoded), urlText.toHtmlEscaped());
|
||||
}
|
||||
QString makeLinkHtml(const QUrl &url) {
|
||||
const QString urlText = url.toDisplayString();
|
||||
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");
|
||||
}
|
||||
QString settingsOrganization() {
|
||||
return QStringLiteral("deeaitch");
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @brief Returns the application name used by QSettings.
|
||||
* @return Application string.
|
||||
*/
|
||||
QString settingsApplication() {
|
||||
return QStringLiteral ("yourls-ui");
|
||||
}
|
||||
QString settingsApplication() {
|
||||
return QStringLiteral("yourls-ui");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
}// 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);
|
||||
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);
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls-ui] debug build, executable:" << QCoreApplication::applicationFilePath()
|
||||
<< "Qt:" << qVersion() << "platform:" << QGuiApplication::platformName();
|
||||
qDebug() << "[yourls-ui] debug build, executable:" << QCoreApplication::applicationFilePath()
|
||||
<< "Qt:" << qVersion() << "platform:" << QGuiApplication::platformName();
|
||||
#endif
|
||||
setWindowTitle (QStringLiteral ("yourls-ui settings"));
|
||||
ui->apiKeyLineEdit->setEchoMode (QLineEdit::Password);
|
||||
statusBar()->showMessage (QStringLiteral ("Configure YOURLS API settings and save."));
|
||||
setWindowTitle(QStringLiteral("yourls-ui settings"));
|
||||
ui->apiKeyLineEdit->setEchoMode(QLineEdit::Password);
|
||||
statusBar()->showMessage(QStringLiteral("Configure YOURLS API settings and save."));
|
||||
|
||||
setupMenuActions();
|
||||
setupTray();
|
||||
setupConnections();
|
||||
loadSettings();
|
||||
setupMenuActions();
|
||||
setupTray();
|
||||
setupConnections();
|
||||
loadSettings();
|
||||
|
||||
connect (m_yourlsClient, &YourlsClient::shortenSucceeded, this, &MainWindow::onShortenSucceeded);
|
||||
connect (m_yourlsClient, &YourlsClient::shortenFailed, this, &MainWindow::onShortenFailed);
|
||||
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);
|
||||
}
|
||||
if (hasValidSettings()) {
|
||||
if (m_trayIcon != nullptr)
|
||||
m_trayIcon->show();
|
||||
QTimer::singleShot(0, this, &MainWindow::hideToTray);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destroys the main window.
|
||||
*/
|
||||
MainWindow::~MainWindow() {
|
||||
delete ui;
|
||||
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;
|
||||
}
|
||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
if (hasValidSettings() && m_trayIcon != nullptr && m_trayIcon->isVisible()) {
|
||||
hideToTray();
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
QMainWindow::closeEvent (event);
|
||||
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();
|
||||
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 (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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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();
|
||||
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();
|
||||
if (m_trayIcon != nullptr)
|
||||
m_trayIcon->show();
|
||||
|
||||
statusBar()->showMessage (QStringLiteral ("Settings saved."), 2000);
|
||||
hideToTray();
|
||||
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"
|
||||
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"));
|
||||
@@ -166,210 +157,207 @@ void MainWindow::showAbout() {
|
||||
* @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."));
|
||||
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();
|
||||
showNormal();
|
||||
raise();
|
||||
activateWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Hides the settings window to tray when possible.
|
||||
*/
|
||||
void MainWindow::hideToTray() {
|
||||
if (!hasValidSettings() || m_trayIcon == nullptr
|
||||
|| !QSystemTrayIcon::isSystemTrayAvailable() || !m_trayIcon->isVisible()) {
|
||||
showSettingsWindow();
|
||||
return;
|
||||
}
|
||||
if (!hasValidSettings() || m_trayIcon == nullptr || !QSystemTrayIcon::isSystemTrayAvailable() || !m_trayIcon->isVisible()) {
|
||||
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);
|
||||
}
|
||||
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();
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
QUrl longUrl;
|
||||
if (QGuiApplication::platformName().startsWith (QStringLiteral ("wayland"))
|
||||
&& !isActiveWindow()) {
|
||||
// Wayland delivers clipboard offers to clients with keyboard focus.
|
||||
// A tray menu belongs to the shell and does not focus our application.
|
||||
QDialog clipboardDialog (this);
|
||||
clipboardDialog.setWindowTitle (QStringLiteral ("Read clipboard URL"));
|
||||
auto *layout = new QVBoxLayout (&clipboardDialog);
|
||||
layout->addWidget (new QLabel (
|
||||
QStringLiteral ("Activate this window to read the URL from the clipboard."), &clipboardDialog));
|
||||
auto *buttons = new QDialogButtonBox (QDialogButtonBox::Cancel, &clipboardDialog);
|
||||
auto *readButton = buttons->addButton (QStringLiteral ("Read clipboard"), QDialogButtonBox::AcceptRole);
|
||||
layout->addWidget (buttons);
|
||||
connect (buttons, &QDialogButtonBox::rejected, &clipboardDialog, &QDialog::reject);
|
||||
|
||||
const auto readClipboard = [&]() {
|
||||
longUrl = ClipboardUrl::fromMimeData (QGuiApplication::clipboard()->mimeData());
|
||||
clipboardDialog.accept();
|
||||
};
|
||||
connect (readButton, &QPushButton::clicked, &clipboardDialog, readClipboard);
|
||||
|
||||
// Wait for focus and the clipboard offer, without blocking the event loop.
|
||||
QTimer pollTimer (&clipboardDialog);
|
||||
int activePolls = 0;
|
||||
connect (&pollTimer, &QTimer::timeout, &clipboardDialog, [&]() {
|
||||
if (!clipboardDialog.isActiveWindow())
|
||||
if (!hasValidSettings()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Settings required"),
|
||||
QStringLiteral("Please configure the API URL and signature first."));
|
||||
showSettingsWindow();
|
||||
return;
|
||||
const auto *mimeData = QGuiApplication::clipboard()->mimeData();
|
||||
if ((mimeData != nullptr && !mimeData->formats().isEmpty())
|
||||
|| ++activePolls >= 20)
|
||||
readClipboard();
|
||||
});
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] Wayland: waiting for focused window and clipboard offer";
|
||||
#endif
|
||||
pollTimer.start (100);
|
||||
if (clipboardDialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
pollTimer.stop();
|
||||
} else {
|
||||
longUrl = ClipboardUrl::fromMimeData (QGuiApplication::clipboard()->mimeData());
|
||||
}
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] final URL:" << longUrl << "valid:" << longUrl.isValid();
|
||||
#endif
|
||||
if (!longUrl.isValid()) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] rejected locally; no YOURLS request sent";
|
||||
#endif
|
||||
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();
|
||||
QUrl longUrl;
|
||||
if (QGuiApplication::platformName().startsWith(QStringLiteral("wayland")) && !isActiveWindow()) {
|
||||
// Wayland delivers clipboard offers to clients with keyboard focus.
|
||||
// A tray menu belongs to the shell and does not focus our application.
|
||||
QDialog clipboardDialog(this);
|
||||
clipboardDialog.setWindowTitle(QStringLiteral("Read clipboard URL"));
|
||||
auto *layout = new QVBoxLayout(&clipboardDialog);
|
||||
layout->addWidget(new QLabel(
|
||||
QStringLiteral("Activate this window to read the URL from the clipboard."), &clipboardDialog));
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, &clipboardDialog);
|
||||
auto *readButton = buttons->addButton(QStringLiteral("Read clipboard"), QDialogButtonBox::AcceptRole);
|
||||
layout->addWidget(buttons);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &clipboardDialog, &QDialog::reject);
|
||||
|
||||
if (m_trayIcon != nullptr) {
|
||||
m_trayIcon->showMessage (QStringLiteral ("yourls-ui"),
|
||||
QStringLiteral ("Requesting short URL..."),
|
||||
QSystemTrayIcon::Information,
|
||||
1200);
|
||||
}
|
||||
const auto readClipboard = [&]() {
|
||||
longUrl = ClipboardUrl::fromMimeData(QGuiApplication::clipboard()->mimeData());
|
||||
clipboardDialog.accept();
|
||||
};
|
||||
connect(readButton, &QPushButton::clicked, &clipboardDialog, readClipboard);
|
||||
|
||||
m_yourlsClient->shortenUrl (settings, longUrl);
|
||||
// Wait for focus and the clipboard offer, without blocking the event loop.
|
||||
QTimer pollTimer(&clipboardDialog);
|
||||
int activePolls = 0;
|
||||
connect(&pollTimer, &QTimer::timeout, &clipboardDialog, [&]() {
|
||||
if (!clipboardDialog.isActiveWindow())
|
||||
return;
|
||||
const auto *mimeData = QGuiApplication::clipboard()->mimeData();
|
||||
if ((mimeData != nullptr && !mimeData->formats().isEmpty()) || ++activePolls >= 20)
|
||||
readClipboard();
|
||||
});
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] Wayland: waiting for focused window and clipboard offer";
|
||||
#endif
|
||||
pollTimer.start(100);
|
||||
if (clipboardDialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
pollTimer.stop();
|
||||
} else {
|
||||
longUrl = ClipboardUrl::fromMimeData(QGuiApplication::clipboard()->mimeData());
|
||||
}
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] final URL:" << longUrl << "valid:" << longUrl.isValid();
|
||||
#endif
|
||||
if (!longUrl.isValid()) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[clipboard] rejected locally; no YOURLS request sent";
|
||||
#endif
|
||||
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) {
|
||||
showShortUrlPopup (shortUrl);
|
||||
void MainWindow::onShortenSucceeded(const QUrl &shortUrl) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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_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_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);
|
||||
m_trayIcon->setContextMenu(m_trayMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Connects UI signals to slots.
|
||||
*/
|
||||
void MainWindow::setupConnections() {
|
||||
connect (ui->saveBtn, &QPushButton::clicked, this, &MainWindow::saveSettings);
|
||||
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(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);
|
||||
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);
|
||||
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());
|
||||
QSettings settings(settingsOrganization(), settingsApplication());
|
||||
ui->urlLineEdit->setText(settings.value(QStringLiteral("api/url")).toString());
|
||||
ui->apiKeyLineEdit->setText(settings.value(QStringLiteral("api/signature")).toString());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -377,47 +365,46 @@ void MainWindow::loadSettings() {
|
||||
* @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();
|
||||
return !ui->urlLineEdit->text().trimmed().isEmpty() && !ui->apiKeyLineEdit->text().trimmed().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shows a popup with the generated short URL.
|
||||
* @param shortUrl Generated short URL.
|
||||
*/
|
||||
void MainWindow::showShortUrlPopup (const QUrl &shortUrl) {
|
||||
const bool wayland = QGuiApplication::platformName().startsWith (QStringLiteral ("wayland"));
|
||||
ClipboardResultDialog dialog (shortUrl, wayland, this);
|
||||
dialog.setWindowTitle (QStringLiteral ("Short URL created"));
|
||||
void MainWindow::showShortUrlPopup(const QUrl &shortUrl) {
|
||||
const bool wayland = QGuiApplication::platformName().startsWith(QStringLiteral("wayland"));
|
||||
ClipboardResultDialog dialog(shortUrl, wayland, 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);
|
||||
layout->addWidget (dialog.copyStatusLabel());
|
||||
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);
|
||||
layout->addWidget(dialog.copyStatusLabel());
|
||||
|
||||
auto *buttons = new QDialogButtonBox (QDialogButtonBox::Ok, &dialog);
|
||||
QObject::connect (buttons, &QDialogButtonBox::accepted, &dialog, [&dialog]() {
|
||||
// An OK click supplies focus/input even when automatic activation was denied.
|
||||
if (dialog.copyShortUrl())
|
||||
dialog.accept();
|
||||
});
|
||||
layout->addWidget (buttons);
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok, &dialog);
|
||||
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, [&dialog]() {
|
||||
// An OK click supplies focus/input even when automatic activation was denied.
|
||||
if (dialog.copyShortUrl())
|
||||
dialog.accept();
|
||||
});
|
||||
layout->addWidget(buttons);
|
||||
|
||||
if (!wayland)
|
||||
dialog.copyShortUrl();
|
||||
dialog.exec();
|
||||
if (!wayland)
|
||||
dialog.copyShortUrl();
|
||||
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);
|
||||
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);
|
||||
}
|
||||
+12
-12
@@ -7,7 +7,7 @@
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
class MainWindow;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
@@ -26,26 +26,26 @@ class YourlsClient;
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs the main window.
|
||||
* @param parent Parent widget.
|
||||
*/
|
||||
explicit MainWindow (QWidget *parent = nullptr);
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Destroys the main window.
|
||||
*/
|
||||
~MainWindow() override;
|
||||
|
||||
protected:
|
||||
protected:
|
||||
/**
|
||||
* @brief Handles close requests.
|
||||
* @param event Close event.
|
||||
*/
|
||||
void closeEvent (QCloseEvent *event) override;
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
private slots:
|
||||
/**
|
||||
* @brief Saves settings from the UI.
|
||||
*/
|
||||
@@ -80,7 +80,7 @@ class MainWindow : public QMainWindow {
|
||||
* @brief Handles tray icon activation.
|
||||
* @param reason Activation reason.
|
||||
*/
|
||||
void onTrayIconActivated (QSystemTrayIcon::ActivationReason reason);
|
||||
void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason);
|
||||
|
||||
/**
|
||||
* @brief Reads a URL from the clipboard and shortens it.
|
||||
@@ -91,15 +91,15 @@ class MainWindow : public QMainWindow {
|
||||
* @brief Handles successful shortening.
|
||||
* @param shortUrl Generated short URL.
|
||||
*/
|
||||
void onShortenSucceeded (const QUrl &shortUrl);
|
||||
void onShortenSucceeded(const QUrl &shortUrl);
|
||||
|
||||
/**
|
||||
* @brief Handles shortening failure.
|
||||
* @param details Detailed error text.
|
||||
*/
|
||||
void onShortenFailed (const QString &details);
|
||||
void onShortenFailed(const QString &details);
|
||||
|
||||
private:
|
||||
private:
|
||||
/**
|
||||
* @brief Creates tray icon and tray menu.
|
||||
*/
|
||||
@@ -125,7 +125,7 @@ class MainWindow : public QMainWindow {
|
||||
* @brief Shows a popup with the generated short URL.
|
||||
* @param shortUrl Generated short URL.
|
||||
*/
|
||||
void showShortUrlPopup (const QUrl &shortUrl);
|
||||
void showShortUrlPopup(const QUrl &shortUrl);
|
||||
|
||||
/**
|
||||
* @brief Creates tray/menu actions.
|
||||
@@ -143,4 +143,4 @@ class MainWindow : public QMainWindow {
|
||||
YourlsClient *m_yourlsClient;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
#endif// MAINWINDOW_H
|
||||
@@ -2,39 +2,39 @@
|
||||
#include <QTest>
|
||||
|
||||
class ClipboardResultDialogTest : public QObject {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void inactiveWindowDoesNotOverwriteClipboard() {
|
||||
auto *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText (QStringLiteral ("original"));
|
||||
ClipboardResultDialog dialog (QUrl (QStringLiteral ("https://cut.himro.ca/h")), true);
|
||||
QVERIFY (!dialog.copyShortUrl());
|
||||
QCOMPARE (clipboard->text(), QStringLiteral ("original"));
|
||||
}
|
||||
void inactiveWindowDoesNotOverwriteClipboard() {
|
||||
auto *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText(QStringLiteral("original"));
|
||||
ClipboardResultDialog dialog(QUrl(QStringLiteral("https://cut.himro.ca/h")), true);
|
||||
QVERIFY(!dialog.copyShortUrl());
|
||||
QCOMPARE(clipboard->text(), QStringLiteral("original"));
|
||||
}
|
||||
|
||||
void focusCopiesAndRetryRestoresShortUrl() {
|
||||
const QUrl shortUrl (QStringLiteral ("https://cut.himro.ca/h?value=a%26b#section"));
|
||||
auto *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText (QStringLiteral ("original"));
|
||||
ClipboardResultDialog dialog (shortUrl, true);
|
||||
dialog.show();
|
||||
dialog.activateWindow();
|
||||
QTRY_VERIFY (dialog.isActiveWindow());
|
||||
QTRY_COMPARE (clipboard->text(), shortUrl.toString (QUrl::FullyEncoded));
|
||||
QVERIFY (dialog.copyStatusLabel()->text().contains (QStringLiteral ("copied")));
|
||||
clipboard->setText (QStringLiteral ("other text"));
|
||||
QVERIFY (dialog.copyShortUrl());
|
||||
QCOMPARE (clipboard->text(), shortUrl.toString (QUrl::FullyEncoded));
|
||||
dialog.accept();
|
||||
QCOMPARE (clipboard->text(), shortUrl.toString (QUrl::FullyEncoded));
|
||||
}
|
||||
void focusCopiesAndRetryRestoresShortUrl() {
|
||||
const QUrl shortUrl(QStringLiteral("https://cut.himro.ca/h?value=a%26b#section"));
|
||||
auto *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText(QStringLiteral("original"));
|
||||
ClipboardResultDialog dialog(shortUrl, true);
|
||||
dialog.show();
|
||||
dialog.activateWindow();
|
||||
QTRY_VERIFY(dialog.isActiveWindow());
|
||||
QTRY_COMPARE(clipboard->text(), shortUrl.toString(QUrl::FullyEncoded));
|
||||
QVERIFY(dialog.copyStatusLabel()->text().contains(QStringLiteral("copied")));
|
||||
clipboard->setText(QStringLiteral("other text"));
|
||||
QVERIFY(dialog.copyShortUrl());
|
||||
QCOMPARE(clipboard->text(), shortUrl.toString(QUrl::FullyEncoded));
|
||||
dialog.accept();
|
||||
QCOMPARE(clipboard->text(), shortUrl.toString(QUrl::FullyEncoded));
|
||||
}
|
||||
|
||||
void platformsWithoutFocusRequirementCopyImmediately() {
|
||||
ClipboardResultDialog dialog (QUrl (QStringLiteral ("https://cut.himro.ca/h")), false);
|
||||
QVERIFY (dialog.copyShortUrl());
|
||||
QCOMPARE (QGuiApplication::clipboard()->text(), QStringLiteral ("https://cut.himro.ca/h"));
|
||||
}
|
||||
void platformsWithoutFocusRequirementCopyImmediately() {
|
||||
ClipboardResultDialog dialog(QUrl(QStringLiteral("https://cut.himro.ca/h")), false);
|
||||
QVERIFY(dialog.copyShortUrl());
|
||||
QCOMPARE(QGuiApplication::clipboard()->text(), QStringLiteral("https://cut.himro.ca/h"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN (ClipboardResultDialogTest)
|
||||
QTEST_MAIN(ClipboardResultDialogTest)
|
||||
#include "clipboardresultdialog_test.moc"
|
||||
+41
-43
@@ -3,50 +3,48 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
|
||||
int main (int argc, char **argv) {
|
||||
QCoreApplication application (argc, argv);
|
||||
const QUrl expected (QStringLiteral ("https://git.himro.ca/explore/repos"));
|
||||
int failures = 0;
|
||||
const auto check = [&failures] (bool condition, const char *description) {
|
||||
if (!condition) {
|
||||
qCritical() << description;
|
||||
++failures;
|
||||
int main(int argc, char **argv) {
|
||||
QCoreApplication application(argc, argv);
|
||||
const QUrl expected(QStringLiteral("https://git.himro.ca/explore/repos"));
|
||||
int failures = 0;
|
||||
const auto check = [&failures](bool condition, const char *description) {
|
||||
if (!condition) {
|
||||
qCritical() << description;
|
||||
++failures;
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto &text: {
|
||||
"https://git.himro.ca/explore/repos",
|
||||
" \nhttps://git.himro.ca/explore/repos\n ",
|
||||
"'https://git.himro.ca/explore/repos'",
|
||||
"\"https://git.himro.ca/explore/repos\"",
|
||||
"<https://git.himro.ca/explore/repos>",
|
||||
"[https://git.himro.ca/explore/repos](https://git.himro.ca/explore/repos)",
|
||||
"'[Repositories](https://git.himro.ca/explore/repos)'"}) {
|
||||
check(ClipboardUrl::parseText(QString::fromUtf8(text)) == expected, text);
|
||||
}
|
||||
};
|
||||
check(ClipboardUrl::parseText(QStringLiteral(
|
||||
"[Other label](https://git.himro.ca/explore/repos?filter=a%26b#section)")) == QUrl(QStringLiteral("https://git.himro.ca/explore/repos?filter=a%26b#section")),
|
||||
"Markdown must use the destination and preserve query and fragment");
|
||||
check(ClipboardUrl::parseText(QStringLiteral("[Link](https://example.com/a_(b))")) == QUrl(QStringLiteral("https://example.com/a_(b)")),
|
||||
"Parentheses in URL paths must be preserved");
|
||||
for (const auto &text: {"", "not a URL", "https://", "[Link](https://)"})
|
||||
check(!ClipboardUrl::parseText(QString::fromUtf8(text)).isValid(), text);
|
||||
|
||||
for (const auto &text : {
|
||||
"https://git.himro.ca/explore/repos",
|
||||
" \nhttps://git.himro.ca/explore/repos\n ",
|
||||
"'https://git.himro.ca/explore/repos'",
|
||||
"\"https://git.himro.ca/explore/repos\"",
|
||||
"<https://git.himro.ca/explore/repos>",
|
||||
"[https://git.himro.ca/explore/repos](https://git.himro.ca/explore/repos)",
|
||||
"'[Repositories](https://git.himro.ca/explore/repos)'"}) {
|
||||
check (ClipboardUrl::parseText (QString::fromUtf8 (text)) == expected, text);
|
||||
}
|
||||
check (ClipboardUrl::parseText (QStringLiteral (
|
||||
"[Other label](https://git.himro.ca/explore/repos?filter=a%26b#section)"))
|
||||
== QUrl (QStringLiteral ("https://git.himro.ca/explore/repos?filter=a%26b#section")),
|
||||
"Markdown must use the destination and preserve query and fragment");
|
||||
check (ClipboardUrl::parseText (QStringLiteral ("[Link](https://example.com/a_(b))"))
|
||||
== QUrl (QStringLiteral ("https://example.com/a_(b)")),
|
||||
"Parentheses in URL paths must be preserved");
|
||||
for (const auto &text : {"", "not a URL", "https://", "[Link](https://)"})
|
||||
check (!ClipboardUrl::parseText (QString::fromUtf8 (text)).isValid(), text);
|
||||
QMimeData mimeData;
|
||||
mimeData.setText(QStringLiteral("Repositories"));
|
||||
mimeData.setUrls({expected});
|
||||
check(ClipboardUrl::fromMimeData(&mimeData) == expected,
|
||||
"Browser URL MIME data must take precedence over a link label");
|
||||
mimeData.clear();
|
||||
mimeData.setText(expected.toString());
|
||||
check(ClipboardUrl::fromMimeData(&mimeData) == expected, "Plain text fallback");
|
||||
mimeData.setUrls({QUrl(QStringLiteral("file:///tmp/example"))});
|
||||
check(ClipboardUrl::fromMimeData(&mimeData) == expected, "Unusable MIME URL fallback");
|
||||
mimeData.clear();
|
||||
check(!ClipboardUrl::fromMimeData(&mimeData).isValid(), "Empty clipboard");
|
||||
check(!ClipboardUrl::fromMimeData(nullptr).isValid(), "Unavailable clipboard");
|
||||
|
||||
QMimeData mimeData;
|
||||
mimeData.setText (QStringLiteral ("Repositories"));
|
||||
mimeData.setUrls ({expected});
|
||||
check (ClipboardUrl::fromMimeData (&mimeData) == expected,
|
||||
"Browser URL MIME data must take precedence over a link label");
|
||||
mimeData.clear();
|
||||
mimeData.setText (expected.toString());
|
||||
check (ClipboardUrl::fromMimeData (&mimeData) == expected, "Plain text fallback");
|
||||
mimeData.setUrls ({QUrl (QStringLiteral ("file:///tmp/example"))});
|
||||
check (ClipboardUrl::fromMimeData (&mimeData) == expected, "Unusable MIME URL fallback");
|
||||
mimeData.clear();
|
||||
check (!ClipboardUrl::fromMimeData (&mimeData).isValid(), "Empty clipboard");
|
||||
check (!ClipboardUrl::fromMimeData (nullptr).isValid(), "Unavailable clipboard");
|
||||
|
||||
return failures == 0 ? 0 : 1;
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
+62
-61
@@ -9,69 +9,70 @@
|
||||
#include <QUrlQuery>
|
||||
|
||||
class YourlsClientTest : public QObject {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void responses_data() {
|
||||
QTest::addColumn<int> ("httpStatus");
|
||||
QTest::addColumn<QString> ("status");
|
||||
QTest::addColumn<QString> ("code");
|
||||
QTest::addColumn<QString> ("originalUrl");
|
||||
QTest::addColumn<QString> ("shortUrl");
|
||||
QTest::addColumn<bool> ("success");
|
||||
const QString source = QStringLiteral ("https://git.himro.ca/explore/repos?q=a%26b&next=x+y#section");
|
||||
const QString shortened = QStringLiteral ("https://cut.himro.ca/h");
|
||||
QTest::newRow ("created") << 200 << "success" << "" << source << shortened << true;
|
||||
QTest::newRow ("duplicate") << 400 << "fail" << "error:url" << source << shortened << true;
|
||||
QTest::newRow ("different-url") << 400 << "fail" << "error:url" << "https://example.com" << shortened << false;
|
||||
QTest::newRow ("missing-short-url") << 400 << "fail" << "error:url" << source << "" << false;
|
||||
QTest::newRow ("relative-short-url") << 400 << "fail" << "error:url" << source << "/h" << false;
|
||||
QTest::newRow ("other-error") << 400 << "fail" << "error:auth" << source << shortened << false;
|
||||
QTest::newRow ("server-error") << 500 << "fail" << "error:url" << source << shortened << false;
|
||||
}
|
||||
void responses_data() {
|
||||
QTest::addColumn<int>("httpStatus");
|
||||
QTest::addColumn<QString>("status");
|
||||
QTest::addColumn<QString>("code");
|
||||
QTest::addColumn<QString>("originalUrl");
|
||||
QTest::addColumn<QString>("shortUrl");
|
||||
QTest::addColumn<bool>("success");
|
||||
const QString source = QStringLiteral("https://git.himro.ca/explore/repos?q=a%26b&next=x+y#section");
|
||||
const QString shortened = QStringLiteral("https://cut.himro.ca/h");
|
||||
QTest::newRow("created") << 200 << "success" << "" << source << shortened << true;
|
||||
QTest::newRow("duplicate") << 400 << "fail" << "error:url" << source << shortened << true;
|
||||
QTest::newRow("different-url") << 400 << "fail" << "error:url" << "https://example.com" << shortened << false;
|
||||
QTest::newRow("missing-short-url") << 400 << "fail" << "error:url" << source << "" << false;
|
||||
QTest::newRow("relative-short-url") << 400 << "fail" << "error:url" << source << "/h" << false;
|
||||
QTest::newRow("other-error") << 400 << "fail" << "error:auth" << source << shortened << false;
|
||||
QTest::newRow("server-error") << 500 << "fail" << "error:url" << source << shortened << false;
|
||||
}
|
||||
|
||||
void responses() {
|
||||
QFETCH (int, httpStatus);
|
||||
QFETCH (QString, status);
|
||||
QFETCH (QString, code);
|
||||
QFETCH (QString, originalUrl);
|
||||
QFETCH (QString, shortUrl);
|
||||
QFETCH (bool, success);
|
||||
QTcpServer server;
|
||||
QVERIFY (server.listen (QHostAddress::LocalHost));
|
||||
QByteArray requestData;
|
||||
const QByteArray body = QJsonDocument (QJsonObject {
|
||||
{"status", status}, {"code", code}, {"shorturl", shortUrl},
|
||||
{"url", QJsonObject {{"url", originalUrl}}}
|
||||
}).toJson (QJsonDocument::Compact);
|
||||
connect (&server, &QTcpServer::newConnection, this, [&]() {
|
||||
auto *socket = server.nextPendingConnection();
|
||||
connect (socket, &QTcpSocket::readyRead, &server, [&, socket]() {
|
||||
requestData += socket->readAll();
|
||||
if (!requestData.contains ("\r\n\r\n"))
|
||||
return;
|
||||
socket->write ("HTTP/1.1 " + QByteArray::number (httpStatus)
|
||||
+ " Result\r\nContent-Type: application/json\r\nContent-Length: "
|
||||
+ QByteArray::number (body.size()) + "\r\nConnection: close\r\n\r\n" + body);
|
||||
socket->disconnectFromHost();
|
||||
});
|
||||
});
|
||||
YourlsClient client;
|
||||
QSignalSpy succeeded (&client, &YourlsClient::shortenSucceeded);
|
||||
QSignalSpy failed (&client, &YourlsClient::shortenFailed);
|
||||
const QUrl source (QStringLiteral ("https://git.himro.ca/explore/repos?q=a%26b&next=x+y#section"));
|
||||
client.shortenUrl ({QStringLiteral ("http://127.0.0.1:%1/api").arg (server.serverPort()),
|
||||
QStringLiteral ("test-signature")}, source);
|
||||
QTRY_COMPARE (succeeded.count() + failed.count(), 1);
|
||||
QCOMPARE (succeeded.count(), success ? 1 : 0);
|
||||
if (success)
|
||||
QCOMPARE (succeeded.first().first().toUrl(), QUrl (shortUrl));
|
||||
const QByteArray target = requestData.split (' ').value (1);
|
||||
const QUrlQuery query (QUrl::fromEncoded (target));
|
||||
// Decode the HTTP query once, as the server does, preserving URL escapes.
|
||||
const QString encodedValue = query.queryItemValue (QStringLiteral ("url"), QUrl::FullyEncoded);
|
||||
QCOMPARE (QUrl::fromPercentEncoding (encodedValue.toLatin1()), source.toString (QUrl::FullyEncoded));
|
||||
}
|
||||
void responses() {
|
||||
QFETCH(int, httpStatus);
|
||||
QFETCH(QString, status);
|
||||
QFETCH(QString, code);
|
||||
QFETCH(QString, originalUrl);
|
||||
QFETCH(QString, shortUrl);
|
||||
QFETCH(bool, success);
|
||||
QTcpServer server;
|
||||
QVERIFY(server.listen(QHostAddress::LocalHost));
|
||||
QByteArray requestData;
|
||||
const QByteArray body = QJsonDocument(QJsonObject{
|
||||
{"status", status},
|
||||
{"code", code},
|
||||
{"shorturl", shortUrl},
|
||||
{"url", QJsonObject{{"url", originalUrl}}}})
|
||||
.toJson(QJsonDocument::Compact);
|
||||
connect(&server, &QTcpServer::newConnection, this, [&]() {
|
||||
auto *socket = server.nextPendingConnection();
|
||||
connect(socket, &QTcpSocket::readyRead, &server, [&, socket]() {
|
||||
requestData += socket->readAll();
|
||||
if (!requestData.contains("\r\n\r\n"))
|
||||
return;
|
||||
socket->write("HTTP/1.1 " + QByteArray::number(httpStatus) + " Result\r\nContent-Type: application/json\r\nContent-Length: " + QByteArray::number(body.size()) + "\r\nConnection: close\r\n\r\n" + body);
|
||||
socket->disconnectFromHost();
|
||||
});
|
||||
});
|
||||
YourlsClient client;
|
||||
QSignalSpy succeeded(&client, &YourlsClient::shortenSucceeded);
|
||||
QSignalSpy failed(&client, &YourlsClient::shortenFailed);
|
||||
const QUrl source(QStringLiteral("https://git.himro.ca/explore/repos?q=a%26b&next=x+y#section"));
|
||||
client.shortenUrl({QStringLiteral("http://127.0.0.1:%1/api").arg(server.serverPort()),
|
||||
QStringLiteral("test-signature")},
|
||||
source);
|
||||
QTRY_COMPARE(succeeded.count() + failed.count(), 1);
|
||||
QCOMPARE(succeeded.count(), success ? 1 : 0);
|
||||
if (success)
|
||||
QCOMPARE(succeeded.first().first().toUrl(), QUrl(shortUrl));
|
||||
const QByteArray target = requestData.split(' ').value(1);
|
||||
const QUrlQuery query(QUrl::fromEncoded(target));
|
||||
// Decode the HTTP query once, as the server does, preserving URL escapes.
|
||||
const QString encodedValue = query.queryItemValue(QStringLiteral("url"), QUrl::FullyEncoded);
|
||||
QCOMPARE(QUrl::fromPercentEncoding(encodedValue.toLatin1()), source.toString(QUrl::FullyEncoded));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN (YourlsClientTest)
|
||||
QTEST_GUILESS_MAIN(YourlsClientTest)
|
||||
#include "yourlsclient_test.moc"
|
||||
+110
-119
@@ -14,9 +14,8 @@
|
||||
* @brief Constructs the client.
|
||||
* @param parent QObject parent.
|
||||
*/
|
||||
YourlsClient::YourlsClient (QObject *parent)
|
||||
: QObject (parent)
|
||||
, m_networkAccessManager (new QNetworkAccessManager (this)) {
|
||||
YourlsClient::YourlsClient(QObject *parent)
|
||||
: QObject(parent), m_networkAccessManager(new QNetworkAccessManager(this)) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,134 +23,126 @@ YourlsClient::YourlsClient (QObject *parent)
|
||||
* @param settings API endpoint and signature.
|
||||
* @param longUrl Source URL to shorten.
|
||||
*/
|
||||
void YourlsClient::shortenUrl (const YourlsSettings &settings, const QUrl &longUrl) {
|
||||
void YourlsClient::shortenUrl(const YourlsSettings &settings, const QUrl &longUrl) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls] source URL:" << longUrl << "valid:" << longUrl.isValid()
|
||||
<< "scheme:" << longUrl.scheme() << "host:" << longUrl.host();
|
||||
qDebug() << "[yourls] source URL:" << longUrl << "valid:" << longUrl.isValid()
|
||||
<< "scheme:" << longUrl.scheme() << "host:" << longUrl.host();
|
||||
#endif
|
||||
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"),
|
||||
QString::fromLatin1 (QUrl::toPercentEncoding (longUrl.toString (QUrl::FullyEncoded))));
|
||||
requestUrl.setQuery (query);
|
||||
|
||||
QNetworkRequest request (requestUrl);
|
||||
request.setHeader (QNetworkRequest::UserAgentHeader, QStringLiteral ("yourls-ui/1.0"));
|
||||
|
||||
#ifndef QT_NO_DEBUG
|
||||
// Never log the request query: it contains the API signature.
|
||||
qDebug() << "[yourls] sending GET, endpoint:"
|
||||
<< requestUrl.adjusted (QUrl::RemoveQuery | QUrl::RemoveFragment | QUrl::RemoveUserInfo)
|
||||
<< "source URL:" << longUrl;
|
||||
#endif
|
||||
QNetworkReply *reply = m_networkAccessManager->get (request);
|
||||
|
||||
connect (reply, &QNetworkReply::finished, this, [this, reply, longUrl]() {
|
||||
const QByteArray body = reply->readAll();
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls] response HTTP status:"
|
||||
<< reply->attribute (QNetworkRequest::HttpStatusCodeAttribute)
|
||||
<< "network error code:" << reply->error()
|
||||
<< "response bytes:" << body.size();
|
||||
#endif
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument jsonDocument = QJsonDocument::fromJson (body, &parseError);
|
||||
const QJsonObject object = jsonDocument.object();
|
||||
const QUrl existingShortUrl (object.value (QStringLiteral ("shorturl")).toString());
|
||||
const QUrl existingLongUrl (object.value (QStringLiteral ("url")).toObject()
|
||||
.value (QStringLiteral ("url")).toString());
|
||||
// YOURLS reports duplicate URLs as HTTP 400, but returns the existing link.
|
||||
if (reply->attribute (QNetworkRequest::HttpStatusCodeAttribute).toInt() == 400
|
||||
&& object.value (QStringLiteral ("status")).toString() == QStringLiteral ("fail")
|
||||
&& object.value (QStringLiteral ("code")).toString() == QStringLiteral ("error:url")
|
||||
&& existingLongUrl == longUrl && existingShortUrl.isValid()
|
||||
&& (existingShortUrl.scheme() == QStringLiteral ("https")
|
||||
|| existingShortUrl.scheme() == QStringLiteral ("http"))
|
||||
&& !existingShortUrl.host().isEmpty()) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls] reusing existing short URL:" << existingShortUrl;
|
||||
#endif
|
||||
reply->deleteLater();
|
||||
emit shortenSucceeded (existingShortUrl);
|
||||
return;
|
||||
if (settings.apiUrl.trimmed().isEmpty()) {
|
||||
emit shortenFailed(QStringLiteral("API URL is empty."));
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
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"),
|
||||
QString::fromLatin1(QUrl::toPercentEncoding(longUrl.toString(QUrl::FullyEncoded))));
|
||||
requestUrl.setQuery(query);
|
||||
|
||||
QNetworkRequest request(requestUrl);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("yourls-ui/1.0"));
|
||||
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls] JSON parse error:" << parseError.errorString()
|
||||
<< "offset:" << parseError.offset << "is object:" << jsonDocument.isObject();
|
||||
// Never log the request query: it contains the API signature.
|
||||
qDebug() << "[yourls] sending GET, endpoint:"
|
||||
<< requestUrl.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment | QUrl::RemoveUserInfo)
|
||||
<< "source URL:" << longUrl;
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
QNetworkReply *reply = m_networkAccessManager->get(request);
|
||||
|
||||
const QString status = object.value (QStringLiteral ("status")).toString();
|
||||
const QString shortUrlText = object.value (QStringLiteral ("shorturl")).toString();
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, longUrl]() {
|
||||
const QByteArray body = reply->readAll();
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls] API status:" << status
|
||||
<< "code:" << object.value (QStringLiteral ("code")).toVariant()
|
||||
<< "short URL:" << shortUrlText;
|
||||
qDebug() << "[yourls] response HTTP status:"
|
||||
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)
|
||||
<< "network error code:" << reply->error()
|
||||
<< "response bytes:" << body.size();
|
||||
#endif
|
||||
|
||||
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;
|
||||
}
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument jsonDocument = QJsonDocument::fromJson(body, &parseError);
|
||||
const QJsonObject object = jsonDocument.object();
|
||||
const QUrl existingShortUrl(object.value(QStringLiteral("shorturl")).toString());
|
||||
const QUrl existingLongUrl(object.value(QStringLiteral("url")).toObject().value(QStringLiteral("url")).toString());
|
||||
// YOURLS reports duplicate URLs as HTTP 400, but returns the existing link.
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 400 && object.value(QStringLiteral("status")).toString() == QStringLiteral("fail") && object.value(QStringLiteral("code")).toString() == QStringLiteral("error:url") && existingLongUrl == longUrl && existingShortUrl.isValid() && (existingShortUrl.scheme() == QStringLiteral("https") || existingShortUrl.scheme() == QStringLiteral("http")) && !existingShortUrl.host().isEmpty()) {
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls] reusing existing short URL:" << existingShortUrl;
|
||||
#endif
|
||||
reply->deleteLater();
|
||||
emit shortenSucceeded(existingShortUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const QUrl shortUrl (shortUrlText);
|
||||
if (!shortUrl.isValid() || shortUrl.host().isEmpty()
|
||||
|| (shortUrl.scheme() != QStringLiteral ("https") && shortUrl.scheme() != QStringLiteral ("http"))) {
|
||||
reply->deleteLater();
|
||||
emit shortenFailed (QStringLiteral ("YOURLS returned an invalid short URL: %1").arg (shortUrlText));
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
emit shortenSucceeded (shortUrl);
|
||||
});
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls] JSON parse error:" << parseError.errorString()
|
||||
<< "offset:" << parseError.offset << "is object:" << jsonDocument.isObject();
|
||||
#endif
|
||||
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 QString status = object.value(QStringLiteral("status")).toString();
|
||||
const QString shortUrlText = object.value(QStringLiteral("shorturl")).toString();
|
||||
#ifndef QT_NO_DEBUG
|
||||
qDebug() << "[yourls] API status:" << status
|
||||
<< "code:" << object.value(QStringLiteral("code")).toVariant()
|
||||
<< "short URL:" << shortUrlText;
|
||||
#endif
|
||||
|
||||
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() || shortUrl.host().isEmpty() || (shortUrl.scheme() != QStringLiteral("https") && shortUrl.scheme() != QStringLiteral("http"))) {
|
||||
reply->deleteLater();
|
||||
emit shortenFailed(QStringLiteral("YOURLS returned an invalid short URL: %1").arg(shortUrlText));
|
||||
return;
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
emit shortenSucceeded(shortUrl);
|
||||
});
|
||||
}
|
||||
+10
-10
@@ -10,8 +10,8 @@ class QNetworkAccessManager;
|
||||
* @brief Small value object describing YOURLS API settings.
|
||||
*/
|
||||
struct YourlsSettings {
|
||||
QString apiUrl;
|
||||
QString signature;
|
||||
QString apiUrl;
|
||||
QString signature;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -24,35 +24,35 @@ struct YourlsSettings {
|
||||
class YourlsClient : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs the client.
|
||||
* @param parent QObject parent.
|
||||
*/
|
||||
explicit YourlsClient (QObject *parent = nullptr);
|
||||
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);
|
||||
void shortenUrl(const YourlsSettings &settings, const QUrl &longUrl);
|
||||
|
||||
signals:
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted when the short URL has been created successfully.
|
||||
* @param shortUrl Generated short URL.
|
||||
*/
|
||||
void shortenSucceeded (const QUrl &shortUrl);
|
||||
void shortenSucceeded(const QUrl &shortUrl);
|
||||
|
||||
/**
|
||||
* @brief Emitted when the request fails.
|
||||
* @param details Human-readable error details.
|
||||
*/
|
||||
void shortenFailed (const QString &details);
|
||||
void shortenFailed(const QString &details);
|
||||
|
||||
private:
|
||||
private:
|
||||
QNetworkAccessManager *m_networkAccessManager;
|
||||
};
|
||||
|
||||
#endif // YOURLSCLIENT_H
|
||||
#endif// YOURLSCLIENT_H
|
||||
Reference in new issue
Block a user