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