diff --git a/CMakeLists.txt b/CMakeLists.txt index 608346e7536e3749fc28c8a3154ff2cf289beb34..7c44c6b42eafe9b6578a31ef0dd284ddf2f9bb88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,10 +19,12 @@ find_package(Qt6 REQUIRED COMPONENTS LinguistTools) qt_standard_project_setup(REQUIRES 6.5) qt_add_executable(mobile-datovka + src/app_version_info.cpp src/auxiliaries/email_helper.cpp src/auxiliaries/email_helper.h src/auxiliaries/ios_helper.cpp src/auxiliaries/ios_helper.h src/backup_zip.cpp src/backup_zip.h src/crypto/crypto.c src/crypto/crypto.h + src/datovka_shared/app_version_info.cpp src/datovka_shared/app_version_info.h src/datovka_shared/compat/compiler.h src/datovka_shared/compat_qt/misc.h src/datovka_shared/compat_qt/random.cpp src/datovka_shared/compat_qt/random.h @@ -232,6 +234,7 @@ qt_add_qml_module(mobile-datovka res/../qml/components/AccessibleTextInfoSmall.qml res/../qml/components/AccessibleToolButton.qml res/../qml/components/AccountList.qml + res/../qml/components/ChangeLogBox.qml res/../qml/components/ControlGroupItem.qml res/../qml/components/DataboxList.qml res/../qml/components/FileDialogue.qml diff --git a/mobile-datovka.pro b/mobile-datovka.pro index b6c4c4a72bce217553a26d2e9be184670c10d145..734d849a8c160c751c4d6355cef3f349fc812a31 100644 --- a/mobile-datovka.pro +++ b/mobile-datovka.pro @@ -164,9 +164,11 @@ TRANSLATIONS_FILES += \ res/locale/datovka_uk.qm SOURCES += \ + src/app_version_info.cpp \ src/auxiliaries/email_helper.cpp \ src/auxiliaries/ios_helper.cpp \ src/backup_zip.cpp \ + src/datovka_shared/app_version_info.cpp \ src/datovka_shared/compat_qt/random.cpp \ src/datovka_shared/graphics/colour.cpp \ src/datovka_shared/gov_services/helper.cpp \ @@ -343,6 +345,7 @@ HEADERS += \ src/auxiliaries/email_helper.h \ src/auxiliaries/ios_helper.h \ src/backup_zip.h \ + src/datovka_shared/app_version_info.h \ src/datovka_shared/compat/compiler.h \ src/datovka_shared/compat_qt/misc.h \ src/datovka_shared/compat_qt/random.h \ diff --git a/qml/components/ChangeLogBox.qml b/qml/components/ChangeLogBox.qml new file mode 100644 index 0000000000000000000000000000000000000000..9b580a3770fe456aa5d58846ac9266497a1c2de5 --- /dev/null +++ b/qml/components/ChangeLogBox.qml @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2014-2025 CZ.NIC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 + +/* + * Provides a simple change log dialog. + */ +Popup { + id: root + anchors.centerIn: parent + modal: true + + property int preferredMinWidth: 280 + + /* Public interface. */ + function showChangeLog() { + changeLogText.text = settings.loadChangeLogText() + root.open() + } + + background: Rectangle { + color: datovkaPalette.base + border.color: datovkaPalette.text + radius: defaultMargin + } + + ColumnLayout { + spacing: defaultMargin * 2 + AccessibleText { + font.bold: true + Layout.alignment: Qt.AlignCenter + color: datovkaPalette.highlightedText + text: qsTr("Version") + ": " + Qt.application.version + } + AccessibleText { + Layout.preferredWidth: root.preferredMinWidth + horizontalAlignment : Text.Center + wrapMode: Text.Wrap + font.bold: true + text: qsTr("What's new?") + } + Rectangle { + Layout.preferredWidth: root.preferredMinWidth + implicitHeight: preferredMinWidth + color: "transparent" + border.color: datovkaPalette.line + border.width: 1 + Flickable { + anchors.fill: parent + anchors.margins: 1 + contentHeight: flickContent.implicitHeight + clip: true + Pane { + id: flickContent + anchors.fill: parent + AccessibleText { + id: changeLogText + width: parent.width + wrapMode: Text.WordWrap + textFormat: TextEdit.RichText + } + } + ScrollIndicator.vertical: ScrollIndicator {} + } + } + AccessibleButton { + Layout.alignment: Qt.AlignCenter + text: "OK" + onClicked: root.close() + } + } +} diff --git a/qml/main.qml b/qml/main.qml index 7a3c4b7f95b25f873af190b4bfb464ef0d4821dd..3abb7b1ec6a6b18ca0fed9ea2a4065f8b2d6a924 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -262,6 +262,10 @@ ApplicationWindow { id: okMsgBox } + ChangeLogBox { + id: changeLogBox + } + DrawerMenuDatovka { id: drawerMenuDatovka } @@ -480,6 +484,10 @@ ApplicationWindow { target: settings function onRunOnAppStartUpSig() { console.log("Running actions after application start-up.") + var isNewVersion = settings.isNewVersion() + if (isNewVersion) { + changeLogBox.showChangeLog() + } var areNews = messages.checkNewDatabasesFormat() if (!areNews) { pageView.push(pageConvertDatabase, { diff --git a/qml/pages/PageAboutApp.qml b/qml/pages/PageAboutApp.qml index 7d661a8c9ed5d8a39d31dadbea4c029c22bf5200..8d47d48eeb97b6cbb2123f912d365dd864f4b78c 100644 --- a/qml/pages/PageAboutApp.qml +++ b/qml/pages/PageAboutApp.qml @@ -151,6 +151,12 @@ Page { + "<br/>" + "<a href=\"mailto:datovka@labs.nic.cz?Subject=[Mobile Datovka%20" + settings.appVersion() + "]\">datovka@labs.nic.cz</a>" } + AccessibleButton { + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Show News") + accessibleName: qsTr("What's new?") + onClicked: changeLogBox.showChangeLog() + } } // Column } // Pane } // Flickable diff --git a/res/qml.qrc b/res/qml.qrc index c44523fa6cb76dff68f5305314196933e8888260..8f36184fd3f08f2bd6c8ab534b7bf0ee7fea8816 100644 --- a/res/qml.qrc +++ b/res/qml.qrc @@ -18,6 +18,7 @@ <file>../qml/components/AccessibleTextInfoSmall.qml</file> <file>../qml/components/AccessibleToolButton.qml</file> <file>../qml/components/AccountList.qml</file> + <file>../qml/components/ChangeLogBox.qml</file> <file>../qml/components/ControlGroupItem.qml</file> <file>../qml/components/DataboxList.qml</file> <file>../qml/components/FileDialogue.qml</file> diff --git a/src/app_version_info.cpp b/src/app_version_info.cpp new file mode 100644 index 0000000000000000000000000000000000000000..685d3310454fb634a7a40706840c045d73122409 --- /dev/null +++ b/src/app_version_info.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014-2025 CZ.NIC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + */ + +#include <QStringBuilder> + +#include "src/datovka_shared/app_version_info.h" + +#define textLineNL(text) \ + (QLatin1String("<style>li{margin-left:-30px;}</style><ul><li>") % (text) % QLatin1String("</li></ul>")) + +QString AppVersionInfo::releaseNewsText(void) +{ + QString content; + + content.append(textLineNL(tr("Showing news. :)"))); + + return content; +} diff --git a/src/datovka_shared/app_version_info.cpp b/src/datovka_shared/app_version_info.cpp new file mode 100644 index 0000000000000000000000000000000000000000..66ee35067c7ba3ce1f9877715c1728db1c217d88 --- /dev/null +++ b/src/datovka_shared/app_version_info.cpp @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2014-2025 CZ.NIC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + */ + +#include <QRegularExpression> +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) +# include <QVersionNumber> +#else /* < Qt-5.6 */ +# include <QVector> +#endif /* >= Qt-5.6 */ + +#include "src/datovka_shared/app_version_info.h" +#include "src/datovka_shared/log/log.h" + +/* Release version string. */ +static const QRegularExpression releaseVerExpr( + QLatin1String("[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*")); +/* Test build version string. */ +static const QRegularExpression gitAchiveVerExr( + QLatin1String("[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9a-f][0-9a-f]*")); + +/*! + * @brief Check whether trimmed \a str contains exactly a string matching \a re. + * + * @param[in] str String to be checked for match. + * @param[in] re Regular expression. + * @return True if trimmed \a str matches \a re. + */ +static inline +bool trimmedMatchesRegExpr(const QString &str, const QRegularExpression &re) +{ + const QString trimmedStr = str.trimmed(); + + QRegularExpressionMatch match(re.match(trimmedStr)); + + return match.hasMatch() && (match.capturedLength() == trimmedStr.length()); +} + +bool AppVersionInfo::isReleaseVersionString(const QString &vStr) +{ + return trimmedMatchesRegExpr(vStr, releaseVerExpr); +} + +bool AppVersionInfo::isGitArchiveString(const QString &vStr) +{ + return trimmedMatchesRegExpr(vStr, gitAchiveVerExr); +} + +/*! + * @brief Strip unwanted data from version string. + * + * @param[in,out] vStr Version string. Must contain a substring in format + * '[0-9]+.[0-9]+.[0-9]+' . + * @return True if such substring is found. + */ +static +bool stripVersionString(QString &vStr) +{ + vStr.remove(QRegularExpression(QLatin1String("^[^0-9.]*"))); + vStr.remove(QRegularExpression(QLatin1String("[^0-9.].*$"))); + vStr.remove(QRegularExpression(QLatin1String("^[.]*"))); + vStr.remove(QRegularExpression(QLatin1String("[.]*$"))); + + QRegularExpressionMatch match(releaseVerExpr.match(vStr)); + + return match.hasMatch() && (match.capturedLength() == vStr.length()); +} + +#if (QT_VERSION < QT_VERSION_CHECK(5, 6, 0)) +# warning "Compiling against version < Qt-5.6 which does not have QVersionNumber." + +/*! + * @brief Replacement for QVersionNumber which is not present in Qt before 5.6. + */ +class QVersionNumber { +public: + QVersionNumber(void) : m_major(-1), m_micro(-1), m_minor(-1) + { + } + + bool isNull(void) const + { + return (m_major < 0) || (m_micro < 0) || (m_minor < 0); + } + + static + int compare(const QVersionNumber &v1, const QVersionNumber &v2) + { + if (v1.m_major < v2.m_major) { + return -1; + } else if (v1.m_major > v2.m_major) { + return 1; + } else if (v1.m_micro < v2.m_micro) { + return -1; + } else if (v1.m_micro > v2.m_micro) { + return 1; + } else if (v1.m_minor < v2.m_minor) { + return -1; + } else if (v1.m_minor > v2.m_minor) { + return 1; + } else { + return 0; + } + } + + static + QVersionNumber fromString(const QString &str) + { + QVersionNumber verNum; + + const int elemNum = 3; + QStringList elemList(str.split(QChar('.'))); + if (elemList.size() != elemNum) { + return verNum; + } + + QVector<int> elemVect(3, -1); + for (int i = 0; i < elemNum; ++i) { + bool ok = false; + elemVect[i] = elemList[i].toInt(&ok); + if (!ok) { + elemVect[i] = -1; + } + } + + verNum.m_major = elemVect[0]; + verNum.m_micro = elemVect[1]; + verNum.m_minor = elemVect[2]; + return verNum; + } + +private: + int m_major; + int m_micro; + int m_minor; +}; +#endif + +int AppVersionInfo::compareVersionStrings(const QString &vStr1, const QString &vStr2) +{ + QString vs1(vStr1), vs2(vStr2); + + if (Q_UNLIKELY(!stripVersionString(vs1))) { + logErrorNL("Cannot strip version string '%s'.", + vs1.toUtf8().constData()); + return -2; + } + if (Q_UNLIKELY(!stripVersionString(vs2))) { + logErrorNL("Cannot strip version string '%s'.", + vs2.toUtf8().constData()); + return 2; + } + + QVersionNumber v1 = QVersionNumber::fromString(vs1); + if (Q_UNLIKELY(v1.isNull())) { + logErrorNL( + "Version string '%s' doesn't match required format.", + vs1.toUtf8().constData()); + return -2; + } + QVersionNumber v2 = QVersionNumber::fromString(vs2); + if (Q_UNLIKELY(v2.isNull())) { + logErrorNL( + "Version string '%s' doesn't match required format.", + vs2.toUtf8().constData()); + return 2; + } + + /* + * Documentation of QVersionNumber::compare() only mentions negative + * or positive values. It doesn't mention -1 or 1. + */ + int cmp = QVersionNumber::compare(v1, v2); + if (cmp < 0) { + return -1; + } else if (cmp == 0) { + return 0; + } else { + return 1; + } +} diff --git a/src/datovka_shared/app_version_info.h b/src/datovka_shared/app_version_info.h new file mode 100644 index 0000000000000000000000000000000000000000..e7b24ebc47c1079c2289bc4e693a4a0188ad7f48 --- /dev/null +++ b/src/datovka_shared/app_version_info.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2014-2025 CZ.NIC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + */ + +#pragma once + +#include <QCoreApplication> /* Q_DECLARE_TR_FUNCTIONS */ +#include <QString> + +class AppVersionInfo { + Q_DECLARE_TR_FUNCTIONS(AppVersionInfo) + +public: + /*! + * @brief Check whether string contains only release version + * (eg. 4.25.0). + * + * @note The string may contain some leading and trailing white-space characters. + * + * @param[in] vStr Version string. + * @return If \a vStr is a release version. + */ + static + bool isReleaseVersionString(const QString &vStr); + + /*! + * @brief Check whether string contains git development build version + * (eg. 4.25.0.9999.20250106.151005.ee2327675e2f1a9a). + * + * @note The string may contain some leading and trailing white-space characters. + * + * @param[in] vStr Version string. + * @return If \a vStr is a git development build version. + */ + static + bool isGitArchiveString(const QString &vStr); + + /*! + * @brief Compare newest available version and application version. + * + * @param[in] vStr1 Version string. + * @param[in] vStr2 Version string. + * @retval -2 if vStr1 doesn't contain a suitable version string + * @retval -1 if vStr1 is less than vStr2 + * @retval 0 if vStr1 is equal to vStr2 + * @retval 1 if vStr1 is greater than vStr2 + * @retval 2 if vStr2 doesn't contain a suitable version string + */ + static + int compareVersionStrings(const QString &vStr1, const QString &vStr2); + + /*! + * @brief Release news. + * + * @return Release news text. + */ + static + QString releaseNewsText(void); +}; diff --git a/src/main.cpp b/src/main.cpp index 44d9d51a51cb7e671d82d67d9b52d028ca200f7b..a4dbab8792d520d440593a910ffb22b18127ae78 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -194,6 +194,7 @@ const struct QmlTypeEntry qmlComponents[] = { { "FilterBar", 1, 0 }, { "GovFormList", 1, 0 }, { "GovServiceList", 1, 0 }, + { "ChangeLogBox", 1, 0 }, { "MessageBox", 1, 0 }, { "MessageList", 1, 0 }, { "PageHeader", 1, 0 }, diff --git a/src/setwrapper.cpp b/src/setwrapper.cpp index 3759a6803ecb80e5b2fb2eeba1e269f632f70ba0..f731a0c62eccce24431c060033787109114203f8 100644 --- a/src/setwrapper.cpp +++ b/src/setwrapper.cpp @@ -24,17 +24,18 @@ #include <QtGlobal> /* QT_VERSION, qVersion() */ #if defined (Q_OS_ANDROID) -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - #include "android_qt6/src/android_io.h" -#else - #include "android/src/android_io.h" -#endif +# if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +# include "android_qt6/src/android_io.h" +# else +# include "android/src/android_io.h" +# endif #endif #if defined (Q_OS_IOS) -#include "ios/src/url_opener.h" +# include "ios/src/url_opener.h" #endif +#include "src/datovka_shared/app_version_info.h" #include "src/datovka_shared/compat_qt/random.h" #include "src/datovka_shared/localisation/localisation.h" #include "src/datovka_shared/log/log.h" @@ -782,3 +783,42 @@ bool GlobalSettingsQmlWrapper::useIosDocumentPicker(void) return false; } } + +bool GlobalSettingsQmlWrapper::isNewVersion(void) +{ + if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::prefsPtr)) { + Q_ASSERT(0); + return false; + } + + bool isNew = true; + QString storedVersion; + + /* + * If running app doesn't have release version then don't check. + */ + if (Q_UNLIKELY(!AppVersionInfo::isReleaseVersionString(VERSION))) { + return false; + } + + GlobInstcs::prefsPtr->strVal("application.notification_shown.last_version", storedVersion); + + /* + * If stored version is empty or non-release version string then behave + * as having a new version. + */ + isNew = storedVersion.isEmpty() + || (!AppVersionInfo::isReleaseVersionString(storedVersion)) + || (1 == AppVersionInfo::compareVersionStrings(VERSION, storedVersion)); + + if (isNew) { + GlobInstcs::prefsPtr->setStrVal("application.notification_shown.last_version", VERSION); + } + + return isNew; +} + +QString GlobalSettingsQmlWrapper::loadChangeLogText(void) +{ + return AppVersionInfo::releaseNewsText(); +} diff --git a/src/setwrapper.h b/src/setwrapper.h index 718863aa0129557b1eebcb66f68aede209398e0e..aa1a08e25dacebcb9d2c57d29081266acbf5a7c3 100644 --- a/src/setwrapper.h +++ b/src/setwrapper.h @@ -491,6 +491,25 @@ public: Q_INVOKABLE static bool useIosDocumentPicker(void); + /*! + * @brief Check if new version after first startup. + * + * @note This check isn't performed if running app doesn't have a valid + * release version. + * + * @return True if it is new version after first startup. + */ + Q_INVOKABLE static + bool isNewVersion(void); + + /*! + * @brief Load changelog content. + * + * @return Changelog text. + */ + Q_INVOKABLE static + QString loadChangeLogText(void); + signals: /*! * @brief Send PIN verification result to QML.