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 : }
|