#include "mainwindow.h" #include "ui_mainwindow.h" #include "yourlsclient.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { /** * @brief Builds a clickable HTML link for dialogs. * @param url URL to render. * @return Rich text string. */ QString makeLinkHtml (const QUrl &url) { const QString urlText = url.toString (QUrl::FullyDecoded); return QStringLiteral ("%2") .arg (url.toString (QUrl::FullyEncoded), urlText.toHtmlEscaped()); } /** * @brief Returns the organization name used by QSettings. * @return Organization string. */ QString settingsOrganization() { return QStringLiteral ("deeaitch"); } /** * @brief Returns the application name used by QSettings. * @return Application string. */ QString settingsApplication() { return QStringLiteral ("yourls-ui"); } } // namespace /** * @brief Constructs the main window. * @param parent Parent widget. */ MainWindow::MainWindow (QWidget *parent) : QMainWindow (parent) , ui (new Ui::MainWindow) , m_trayIcon (nullptr) , m_trayMenu (nullptr) , m_cutUrlAction (nullptr) , m_settingsAction (nullptr) , m_helpAction (nullptr) , m_aboutAction (nullptr) , m_exitAction (nullptr) , m_yourlsClient (new YourlsClient (this)) { ui->setupUi (this); setWindowTitle (QStringLiteral ("yourls-ui settings")); ui->apiKeyLineEdit->setEchoMode (QLineEdit::Password); statusBar()->showMessage (QStringLiteral ("Configure YOURLS API settings and save.")); setupMenuActions(); setupTray(); setupConnections(); loadSettings(); connect (m_yourlsClient, &YourlsClient::shortenSucceeded, this, &MainWindow::onShortenSucceeded); connect (m_yourlsClient, &YourlsClient::shortenFailed, this, &MainWindow::onShortenFailed); if (hasValidSettings()) { if (m_trayIcon != nullptr) m_trayIcon->show(); QTimer::singleShot (0, this, &MainWindow::hideToTray); } } /** * @brief Destroys the main window. */ MainWindow::~MainWindow() { delete ui; } /** * @brief Handles close requests. * @param event Close event. */ void MainWindow::closeEvent (QCloseEvent *event) { if (hasValidSettings() && m_trayIcon != nullptr && m_trayIcon->isVisible()) { hideToTray(); event->ignore(); return; } QMainWindow::closeEvent (event); } /** * @brief Saves settings from the UI. */ void MainWindow::saveSettings() { const QString apiUrl = ui->urlLineEdit->text().trimmed(); const QString apiKey = ui->apiKeyLineEdit->text().trimmed(); if (apiUrl.isEmpty()) { QMessageBox::warning (this, QStringLiteral ("Missing API URL"), QStringLiteral ("Please enter the YOURLS API URL.")); return; } if (apiKey.isEmpty()) { QMessageBox::warning (this, QStringLiteral ("Missing API key"), QStringLiteral ("Please enter the YOURLS API signature.")); return; } const QUrl parsedApiUrl (apiUrl); if (!parsedApiUrl.isValid() || parsedApiUrl.scheme().isEmpty() || parsedApiUrl.host().isEmpty()) { QMessageBox::warning (this, QStringLiteral ("Invalid API URL"), QStringLiteral ("The configured API URL does not look valid.")); return; } QSettings settings (settingsOrganization(), settingsApplication()); settings.setValue (QStringLiteral ("api/url"), apiUrl); settings.setValue (QStringLiteral ("api/signature"), apiKey); settings.sync(); if (m_trayIcon != nullptr) m_trayIcon->show(); statusBar()->showMessage (QStringLiteral ("Settings saved."), 2000); hideToTray(); } /** * @brief Shows About text. */ void MainWindow::showAbout() { QMessageBox::about (this, QStringLiteral ("About yourls-ui"), QStringLiteral ("yourls-ui\n\n" "Small tray utility for shortening clipboard URLs with a YOURLS API.\n\n" "Creator: deeaitch\n" "Created: 2026-03-16")); } /** * @brief Shows Help text. */ void MainWindow::showHelp() { QMessageBox::information (this, QStringLiteral ("Help"), QStringLiteral ("1. Open Settings and enter the YOURLS API URL and signature.\n" "2. Save the settings. The application will stay in the tray.\n" "3. Copy a full URL to the clipboard.\n" "4. Right-click the tray icon and choose \"Cut URL\".\n" "5. The short URL will be copied back to the clipboard and shown in a popup.\n\n" "If something fails, the program shows detailed error information.")); } /** * @brief Shows the settings window. */ void MainWindow::showSettingsWindow() { showNormal(); raise(); activateWindow(); } /** * @brief Hides the settings window to tray when possible. */ void MainWindow::hideToTray() { if (!hasValidSettings()) { showSettingsWindow(); return; } hide(); if (m_trayIcon != nullptr && m_trayIcon->isVisible()) { m_trayIcon->showMessage (QStringLiteral ("yourls-ui"), QStringLiteral ("The application is running in the system tray."), QSystemTrayIcon::Information, 2000); } } /** * @brief Exits the application. */ void MainWindow::exitApplication() { QApplication::quit(); } /** * @brief Handles tray icon activation. * @param reason Activation reason. */ void MainWindow::onTrayIconActivated (QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::DoubleClick || reason == QSystemTrayIcon::Trigger) showSettingsWindow(); } /** * @brief Reads a URL from the clipboard and shortens it. */ void MainWindow::cutUrlFromClipboard() { if (!hasValidSettings()) { QMessageBox::warning (this, QStringLiteral ("Settings required"), QStringLiteral ("Please configure the API URL and signature first.")); showSettingsWindow(); return; } const QString clipboardText = QGuiApplication::clipboard()->text().trimmed(); const QUrl longUrl = parseClipboardUrl (clipboardText); if (!longUrl.isValid()) { QMessageBox::warning (this, QStringLiteral ("Invalid clipboard content"), QStringLiteral ("The clipboard does not contain a valid URL.")); return; } YourlsSettings settings; settings.apiUrl = ui->urlLineEdit->text().trimmed(); settings.signature = ui->apiKeyLineEdit->text().trimmed(); if (m_trayIcon != nullptr) { m_trayIcon->showMessage (QStringLiteral ("yourls-ui"), QStringLiteral ("Requesting short URL..."), QSystemTrayIcon::Information, 1200); } m_yourlsClient->shortenUrl (settings, longUrl); } /** * @brief Handles successful shortening. * @param shortUrl Generated short URL. */ void MainWindow::onShortenSucceeded (const QUrl &shortUrl) { QGuiApplication::clipboard()->setText (shortUrl.toString (QUrl::FullyDecoded)); showShortUrlPopup (shortUrl); } /** * @brief Handles shortening failure. * @param details Detailed error text. */ void MainWindow::onShortenFailed (const QString &details) { QMessageBox::critical (this, QStringLiteral ("Failed to create short URL"), details); } /** * @brief Creates tray icon and tray menu. */ void MainWindow::setupTray() { if (!QSystemTrayIcon::isSystemTrayAvailable()) { QMessageBox::warning (this, QStringLiteral ("System tray unavailable"), QStringLiteral ("No system tray was detected. The application will stay as a normal window.")); return; } m_trayIcon = new QSystemTrayIcon (this); m_trayIcon->setIcon (style()->standardIcon (QStyle::SP_DialogApplyButton)); m_trayMenu = new QMenu (this); m_trayMenu->addAction (m_cutUrlAction); m_trayMenu->addSeparator(); m_trayMenu->addAction (m_settingsAction); m_trayMenu->addAction (m_helpAction); m_trayMenu->addAction (m_aboutAction); m_trayMenu->addSeparator(); m_trayMenu->addAction (m_exitAction); m_trayIcon->setContextMenu (m_trayMenu); } /** * @brief Connects UI signals to slots. */ void MainWindow::setupConnections() { connect (ui->saveBtn, &QPushButton::clicked, this, &MainWindow::saveSettings); connect (ui->actionHelp, &QAction::triggered, this, &MainWindow::showHelp); connect (ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout); connect (ui->actionExit, &QAction::triggered, this, &MainWindow::exitApplication); connect (m_cutUrlAction, &QAction::triggered, this, &MainWindow::cutUrlFromClipboard); connect (m_settingsAction, &QAction::triggered, this, &MainWindow::showSettingsWindow); connect (m_helpAction, &QAction::triggered, this, &MainWindow::showHelp); connect (m_aboutAction, &QAction::triggered, this, &MainWindow::showAbout); connect (m_exitAction, &QAction::triggered, this, &MainWindow::exitApplication); if (m_trayIcon != nullptr) connect (m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::onTrayIconActivated); } /** * @brief Loads persisted settings into the UI. */ void MainWindow::loadSettings() { QSettings settings (settingsOrganization(), settingsApplication()); ui->urlLineEdit->setText (settings.value (QStringLiteral ("api/url")).toString()); ui->apiKeyLineEdit->setText (settings.value (QStringLiteral ("api/signature")).toString()); } /** * @brief Returns true when all required settings are present. * @return True when the application can work from the tray. */ bool MainWindow::hasValidSettings() const { return !ui->urlLineEdit->text().trimmed().isEmpty() && !ui->apiKeyLineEdit->text().trimmed().isEmpty(); } /** * @brief Validates and normalizes URL text. * @param text Input text. * @return Parsed URL. */ QUrl MainWindow::parseClipboardUrl (const QString &text) const { const QUrl url = QUrl::fromUserInput (text.trimmed()); if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty()) return QUrl(); return url; } /** * @brief Shows a popup with the generated short URL. * @param shortUrl Generated short URL. */ void MainWindow::showShortUrlPopup (const QUrl &shortUrl) { QDialog dialog (this); dialog.setWindowTitle (QStringLiteral ("Short URL created")); auto *layout = new QVBoxLayout (&dialog); auto *label = new QLabel (QStringLiteral ("Generated short URL:
%1").arg (makeLinkHtml (shortUrl)), &dialog); label->setTextFormat (Qt::RichText); label->setTextInteractionFlags (Qt::TextBrowserInteraction); label->setOpenExternalLinks (true); layout->addWidget (label); auto *buttons = new QDialogButtonBox (QDialogButtonBox::Ok, &dialog); QObject::connect (buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); layout->addWidget (buttons); dialog.exec(); } /** * @brief Creates tray/menu actions. */ void MainWindow::setupMenuActions() { m_cutUrlAction = new QAction (QStringLiteral ("Cut URL"), this); m_settingsAction = new QAction (QStringLiteral ("Settings"), this); m_helpAction = new QAction (QStringLiteral ("Help"), this); m_aboutAction = new QAction (QStringLiteral ("About"), this); m_exitAction = new QAction (QStringLiteral ("Exit"), this); }