LCOV - code coverage report
Current view: top level - src/utilities - UpdateChecker.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 94.0 % 83 78
Test Date: 2026-04-19 00:35:54 Functions: 85.7 % 7 6

            Line data    Source code
       1              : #include "UpdateChecker.h"
       2              : #include "FangLogging.h"
       3              : #include"FeedFetcher.h"
       4              : 
       5              : #include <QRegularExpression>
       6              : #include <QVersionNumber>
       7              : 
       8              : // Only include FangApp.h when instance() is needed (not in tests)
       9              : #ifndef UPDATECHECKER_NO_FANGAPP
      10              : #include "../FangApp.h"
      11              : #endif
      12              : 
      13              : const QUrl UpdateChecker::UPDATE_FEED_URL = QUrl("https://getfang.com/feed.xml");
      14              : const int UpdateChecker::CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
      15              : 
      16           22 : UpdateChecker::UpdateChecker(QObject *parent, FeedSource* injectedParser, SettingsInterface* injectedSettings) :
      17              :     QObject(parent),
      18           22 :     parser(injectedParser ? injectedParser : new FeedFetcher(this)),
      19           22 :     settingsInterface(injectedSettings),
      20           22 :     timer(this),
      21           22 :     _latestVersion(""),
      22           22 :     _updateAvailable(false)
      23              : {
      24           22 :     connect(parser, &FeedSource::done, this, &UpdateChecker::onParserDone);
      25           22 :     connect(&timer, &QTimer::timeout, this, &UpdateChecker::checkNow);
      26              : 
      27           22 :     timer.setInterval(CHECK_INTERVAL_MS);
      28           22 : }
      29              : 
      30            0 : void UpdateChecker::start()
      31              : {
      32              :     // Perform immediate check
      33            0 :     checkNow();
      34              : 
      35              :     // Start periodic checking
      36            0 :     timer.start();
      37            0 : }
      38              : 
      39            7 : void UpdateChecker::checkNow()
      40              : {
      41           14 :     qCDebug(logUtility) << "UpdateChecker: Checking for updates...";
      42            7 :     parser->parse(UPDATE_FEED_URL);
      43            7 : }
      44              : 
      45            7 : void UpdateChecker::onParserDone()
      46              : {
      47            7 :     if (parser->getResult() != FeedFetchResult::OK) {
      48            2 :         qCWarning(logUtility) << "UpdateChecker: Failed to fetch update feed:"
      49            1 :                               << static_cast<int>(parser->getResult());
      50            4 :         return;
      51              :     }
      52              : 
      53            6 :     auto feed = parser->getFeed();
      54            6 :     if (!feed || feed->items.isEmpty()) {
      55            2 :         qCWarning(logUtility) << "UpdateChecker: No items in update feed";
      56            1 :         return;
      57              :     }
      58              : 
      59              :     // Get the latest release (first item in feed)
      60            5 :     const auto& latestRelease = feed->items.first();
      61            5 :     QString version = extractVersion(latestRelease->title);
      62              : 
      63            5 :     if (version.isEmpty()) {
      64            2 :         qCWarning(logUtility) << "UpdateChecker: Could not extract version from title:"
      65            1 :                               << latestRelease->title;
      66            1 :         return;
      67              :     }
      68              : 
      69            4 :     _latestVersion = version;
      70            8 :     qCDebug(logUtility) << "UpdateChecker: Latest version:" << _latestVersion
      71            4 :                         << "Current version:" << APP_VERSION;
      72              : 
      73              :     // Compare versions
      74            4 :     if (isNewerVersion(APP_VERSION, _latestVersion)) {
      75            2 :         _updateAvailable = true;
      76              : 
      77              :         // Check if we've already shown the dialog for this version
      78            2 :         SettingsInterface* settings = settingsInterface;
      79              : #ifndef UPDATECHECKER_NO_FANGAPP
      80              :         if (!settings) {
      81              :             settings = FangApp::instance()->getSettings();
      82              :         }
      83              : #endif
      84            2 :         if (!settings) {
      85            0 :             qCWarning(logUtility) << "UpdateChecker: No settings available";
      86            1 :             return;
      87              :         }
      88            2 :         QString lastShown = settings->getLastSeenVersion();
      89            2 :         if (lastShown == _latestVersion) {
      90            2 :             qCDebug(logUtility) << "UpdateChecker: Update available but already shown for version:"
      91            1 :                                 << _latestVersion;
      92            1 :             return;
      93              :         }
      94              : 
      95            2 :         qCInfo(logUtility) << "UpdateChecker: Update available! New version:" << _latestVersion;
      96            1 :         emit updateAvailable(_latestVersion);
      97            2 :     } else {
      98            4 :         qCDebug(logUtility) << "UpdateChecker: No update available";
      99            2 :         _updateAvailable = false;
     100              :     }
     101            8 : }
     102              : 
     103           11 : QString UpdateChecker::extractVersion(const QString& title)
     104              : {
     105              :     // Expected format: "Fang X.Y.Z" or "Fang X.Y.Z-beta" or "Fang X.Y.Z-beta.N"
     106              :     // Match version pattern after "Fang "
     107           11 :     static QRegularExpression versionPattern(R"(Fang\s+(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?))");
     108              : 
     109           11 :     QRegularExpressionMatch match = versionPattern.match(title);
     110           11 :     if (match.hasMatch()) {
     111            8 :         return match.captured(1);
     112              :     }
     113              : 
     114            3 :     return QString();
     115           11 : }
     116              : 
     117           13 : bool UpdateChecker::isNewerVersion(const QString& currentVersion, const QString& newVersion)
     118              : {
     119              :     // Parse version strings of format: X.Y.Z or X.Y.Z-suffix
     120           26 :     auto parseVersion = [](const QString& version) -> QPair<QVersionNumber, QString> {
     121           26 :         QString numericPart = version;
     122           26 :         QString suffix;
     123              : 
     124           26 :         int dashIndex = version.indexOf('-');
     125           26 :         if (dashIndex != -1) {
     126           11 :             numericPart = version.left(dashIndex);
     127           11 :             suffix = version.mid(dashIndex + 1);
     128              :         }
     129              : 
     130           52 :         return qMakePair(QVersionNumber::fromString(numericPart), suffix);
     131           26 :     };
     132              : 
     133           13 :     auto [currentNum, currentSuffix] = parseVersion(currentVersion);
     134           13 :     auto [newNum, newSuffix] = parseVersion(newVersion);
     135              : 
     136              :     // Compare numeric part of version string.
     137           13 :     int comparison = QVersionNumber::compare(newNum, currentNum);
     138           13 :     if (comparison > 0) {
     139              :         // New version has higher number.
     140            5 :         return true;
     141            8 :     } else if (comparison < 0) {
     142              :         // New version has lower number.
     143            2 :         return false;
     144              :     }
     145              : 
     146              :     // No suffix > any suffix (beta, alpha, etc.)
     147            6 :     if (newSuffix.isEmpty() && !currentSuffix.isEmpty()) {
     148              :         // New is release, current is pre-release.
     149            1 :         return true;
     150            5 :     } else if (!newSuffix.isEmpty() && currentSuffix.isEmpty()) {
     151              :         // New is pre-release, current is release.
     152            1 :         return false;
     153              :     }
     154              : 
     155              :     // Both have suffixes or both don't, so just compare strings at this point.
     156            4 :     return newSuffix > currentSuffix;
     157           13 : }
        

Generated by: LCOV version 2.0-1