From bbfaf86bc8089a350c894d77b45ca045afb731a1 Mon Sep 17 00:00:00 2001 From: elecpower Date: Wed, 20 Jul 2022 23:07:24 +1000 Subject: [PATCH] Refactor firmware and companion updates plus add extra supporting components --- companion/src/CMakeLists.txt | 18 +- companion/src/apppreferencesdialog.cpp | 177 +- companion/src/apppreferencesdialog.h | 21 +- companion/src/apppreferencesdialog.ui | 602 ++++--- companion/src/companion.qrc | 40 +- companion/src/constants.h | 5 +- .../src/firmwares/opentx/opentxinterface.cpp | 16 - .../src/firmwares/opentx/opentxinterface.h | 6 +- companion/src/fwpreferencesdialog.cpp | 78 - companion/src/fwpreferencesdialog.ui | 180 -- .../originals/icons Classical/download.png | Bin 0 -> 82297 bytes .../originals/icons Monochrome/download.png | Bin 0 -> 482 bytes companion/src/mainwindow.cpp | 481 +---- companion/src/mainwindow.h | 39 +- companion/src/progresswidget.h | 9 +- companion/src/progresswidget.ui | 10 + companion/src/releasenotesfirmwaredialog.cpp | 52 - companion/src/storage/CMakeLists.txt | 1 + companion/src/storage/appdata.cpp | 82 +- companion/src/storage/appdata.h | 165 +- companion/src/storage/minizinterface.cpp | 292 ++++ companion/src/storage/minizinterface.h | 60 + .../16/{fwpreferences.png => download.png} | Bin .../24/{fwpreferences.png => download.png} | Bin .../32/{fwpreferences.png => download.png} | Bin .../48/{fwpreferences.png => download.png} | Bin .../16/{fwpreferences.png => download.png} | Bin .../24/{fwpreferences.png => download.png} | Bin .../32/{fwpreferences.png => download.png} | Bin .../48/{fwpreferences.png => download.png} | Bin .../16/{fwpreferences.png => download.png} | Bin .../24/{fwpreferences.png => download.png} | Bin .../32/{fwpreferences.png => download.png} | Bin .../48/{fwpreferences.png => download.png} | Bin .../16/{fwpreferences.png => download.png} | Bin .../24/{fwpreferences.png => download.png} | Bin .../32/{fwpreferences.png => download.png} | Bin .../48/{fwpreferences.png => download.png} | Bin .../16/{fwpreferences.png => download.png} | Bin .../24/{fwpreferences.png => download.png} | Bin .../32/{fwpreferences.png => download.png} | Bin .../48/{fwpreferences.png => download.png} | Bin companion/src/updates/CMakeLists.txt | 39 + .../src/{ => updates}/downloaddialog.cpp | 40 +- companion/src/{ => updates}/downloaddialog.h | 20 +- companion/src/{ => updates}/downloaddialog.ui | 0 companion/src/updates/repomodels.cpp | 456 +++++ companion/src/updates/repomodels.h | 300 ++++ companion/src/updates/updatecompanion.cpp | 143 ++ .../updatecompanion.h} | 39 +- companion/src/updates/updatefirmware.cpp | 99 ++ companion/src/updates/updatefirmware.h | 37 + companion/src/updates/updateinterface.cpp | 1550 +++++++++++++++++ companion/src/updates/updateinterface.h | 299 ++++ companion/src/updates/updatemultiprotocol.cpp | 46 + companion/src/updates/updatemultiprotocol.h | 37 + companion/src/updates/updateoptionsdialog.cpp | 259 +++ companion/src/updates/updateoptionsdialog.h | 72 + companion/src/updates/updateoptionsdialog.ui | 171 ++ companion/src/updates/updates.cpp | 94 + .../updates.h} | 27 +- companion/src/updates/updatesdcard.cpp | 102 ++ companion/src/updates/updatesdcard.h | 37 + companion/src/updates/updatesdialog.cpp | 218 +++ .../updatesdialog.h} | 39 +- companion/src/updates/updatesdialog.ui | 216 +++ companion/src/updates/updatesounds.cpp | 53 + companion/src/updates/updatesounds.h | 35 + .../updatethemes.cpp} | 39 +- companion/src/updates/updatethemes.h | 35 + 70 files changed, 5526 insertions(+), 1310 deletions(-) delete mode 100644 companion/src/fwpreferencesdialog.cpp delete mode 100644 companion/src/fwpreferencesdialog.ui create mode 100644 companion/src/images/originals/icons Classical/download.png create mode 100644 companion/src/images/originals/icons Monochrome/download.png delete mode 100644 companion/src/releasenotesfirmwaredialog.cpp create mode 100644 companion/src/storage/minizinterface.cpp create mode 100644 companion/src/storage/minizinterface.h rename companion/src/themes/classic/16/{fwpreferences.png => download.png} (100%) rename companion/src/themes/classic/24/{fwpreferences.png => download.png} (100%) rename companion/src/themes/classic/32/{fwpreferences.png => download.png} (100%) rename companion/src/themes/classic/48/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monoblue/16/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monoblue/24/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monoblue/32/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monoblue/48/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monochrome/16/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monochrome/24/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monochrome/32/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monochrome/48/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monowhite/16/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monowhite/24/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monowhite/32/{fwpreferences.png => download.png} (100%) rename companion/src/themes/monowhite/48/{fwpreferences.png => download.png} (100%) rename companion/src/themes/yerico/16/{fwpreferences.png => download.png} (100%) rename companion/src/themes/yerico/24/{fwpreferences.png => download.png} (100%) rename companion/src/themes/yerico/32/{fwpreferences.png => download.png} (100%) rename companion/src/themes/yerico/48/{fwpreferences.png => download.png} (100%) create mode 100644 companion/src/updates/CMakeLists.txt rename companion/src/{ => updates}/downloaddialog.cpp (75%) rename companion/src/{ => updates}/downloaddialog.h (87%) rename companion/src/{ => updates}/downloaddialog.ui (100%) create mode 100644 companion/src/updates/repomodels.cpp create mode 100644 companion/src/updates/repomodels.h create mode 100644 companion/src/updates/updatecompanion.cpp rename companion/src/{releasenotesdialog.cpp => updates/updatecompanion.h} (50%) create mode 100644 companion/src/updates/updatefirmware.cpp create mode 100644 companion/src/updates/updatefirmware.h create mode 100644 companion/src/updates/updateinterface.cpp create mode 100644 companion/src/updates/updateinterface.h create mode 100644 companion/src/updates/updatemultiprotocol.cpp create mode 100644 companion/src/updates/updatemultiprotocol.h create mode 100644 companion/src/updates/updateoptionsdialog.cpp create mode 100644 companion/src/updates/updateoptionsdialog.h create mode 100644 companion/src/updates/updateoptionsdialog.ui create mode 100644 companion/src/updates/updates.cpp rename companion/src/{releasenotesdialog.h => updates/updates.h} (66%) create mode 100644 companion/src/updates/updatesdcard.cpp create mode 100644 companion/src/updates/updatesdcard.h create mode 100644 companion/src/updates/updatesdialog.cpp rename companion/src/{fwpreferencesdialog.h => updates/updatesdialog.h} (57%) create mode 100644 companion/src/updates/updatesdialog.ui create mode 100644 companion/src/updates/updatesounds.cpp create mode 100644 companion/src/updates/updatesounds.h rename companion/src/{releasenotesfirmwaredialog.h => updates/updatethemes.cpp} (55%) create mode 100644 companion/src/updates/updatethemes.h diff --git a/companion/src/CMakeLists.txt b/companion/src/CMakeLists.txt index 7f5e2ef3b..3cbfff2da 100644 --- a/companion/src/CMakeLists.txt +++ b/companion/src/CMakeLists.txt @@ -25,11 +25,6 @@ set(COMPANION_OSX_APP_BUNDLE_NAME "EdgeTX Companion ${VERSION_MAJOR}.${VERSION_M # the binary (default homebrew path) set(DFU_UTIL_PATH "/usr/local/bin/dfu-util") -option(ALLOW_NIGHTLY_BUILDS "Allow nightly builds download / update") # Disabled by default -if(ALLOW_NIGHTLY_BUILDS) - add_definitions(-DALLOW_NIGHTLY_BUILDS) -endif(ALLOW_NIGHTLY_BUILDS) - option(DEBUG_STORAGE_IMPORT "Turn on debug output for storage import") # Disabled by default if(DEBUG_STORAGE_IMPORT) add_definitions(-DDEBUG_STORAGE_IMPORT) @@ -198,6 +193,7 @@ add_subdirectory(storage) add_subdirectory(thirdparty/qcustomplot) add_subdirectory(thirdparty/maxlibqt/src/widgets) add_subdirectory(thirdparty/miniz) +add_subdirectory(updates) set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "Enable testing" FORCE) set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "Enable parse tools" FORCE) @@ -213,12 +209,9 @@ set(companion_SRCS modelslist.cpp labels.cpp apppreferencesdialog.cpp - fwpreferencesdialog.cpp burnconfigdialog.cpp multimodelprinter.cpp comparedialog.cpp - releasenotesdialog.cpp - releasenotesfirmwaredialog.cpp customizesplashdialog.cpp radiointerface.cpp progresswidget.cpp @@ -231,7 +224,6 @@ set(companion_SRCS printdialog.cpp modelprinter.cpp logsdialog.cpp - downloaddialog.cpp splashlibrarydialog.cpp mainwindow.cpp companion.cpp @@ -245,13 +237,10 @@ set(companion_SRCS set(companion_MOC_HDRS apppreferencesdialog.h - fwpreferencesdialog.h burnconfigdialog.h comparedialog.h printdialog.h logsdialog.h - releasenotesdialog.h - releasenotesfirmwaredialog.h customizesplashdialog.h splashlibrarydialog.h splashlabel.h @@ -262,7 +251,6 @@ set(companion_MOC_HDRS process_sync.h flashfirmwaredialog.h flasheepromdialog.h - downloaddialog.h mdichild.h mainwindow.h radionotfound.h @@ -282,9 +270,7 @@ set(companion_UIS comparedialog.ui logsdialog.ui apppreferencesdialog.ui - fwpreferencesdialog.ui burnconfigdialog.ui - downloaddialog.ui printdialog.ui htmldialog.ui customizesplashdialog.ui @@ -306,7 +292,7 @@ qt5_wrap_ui(companion_SRCS ${companion_UIS}) qt5_wrap_cpp(companion_SRCS ${companion_MOC_HDRS}) add_executable(${COMPANION_NAME} MACOSX_BUNDLE ${WIN_EXECUTABLE_TYPE} ${companion_SRCS} ${icon_RC}) -target_link_libraries(${COMPANION_NAME} PRIVATE generaledit modeledit qcustomplot miniz shared ${CPN_COMMON_LIB}) +target_link_libraries(${COMPANION_NAME} PRIVATE generaledit modeledit qcustomplot miniz shared updates ${CPN_COMMON_LIB}) PrintTargetReport("${COMPANION_NAME}") diff --git a/companion/src/apppreferencesdialog.cpp b/companion/src/apppreferencesdialog.cpp index 9877b3f22..d60595852 100644 --- a/companion/src/apppreferencesdialog.cpp +++ b/companion/src/apppreferencesdialog.cpp @@ -21,7 +21,6 @@ #include "apppreferencesdialog.h" #include "ui_apppreferencesdialog.h" #include "mainwindow.h" -#include "appdata.h" #include "helpers.h" #include "storage.h" #if defined(JOYSTICKS) @@ -30,14 +29,16 @@ #endif #include "moduledata.h" #include "compounditemmodels.h" +#include "updates/updateinterface.h" #include -AppPreferencesDialog::AppPreferencesDialog(QWidget * parent) : +AppPreferencesDialog::AppPreferencesDialog(QWidget * parent, UpdateFactories * factories) : QDialog(parent), ui(new Ui::AppPreferencesDialog), updateLock(false), - mainWinHasDirtyChild(false) + mainWinHasDirtyChild(false), + factories(factories) { ui->setupUi(this); setWindowIcon(CompanionIcon("apppreferences.png")); @@ -73,9 +74,6 @@ void AppPreferencesDialog::accept() { Profile & profile = g.currentProfile(); - g.autoCheckApp(ui->autoCheckCompanion->isChecked()); - g.OpenTxBranch(AppData::DownloadBranchType(ui->OpenTxBranch->currentIndex())); - g.autoCheckFw(ui->autoCheckFirmware->isChecked()); g.showSplash(ui->showSplash->isChecked()); g.promptProfile(ui->chkPromptProfile->isChecked()); g.simuSW(ui->simuSW->isChecked()); @@ -101,6 +99,28 @@ void AppPreferencesDialog::accept() g.jsSupport(false); g.jsCtrl(0); } + + // Updates tab + + g.updateCheckFreq(AppData::UpdateCheckFreq(ui->cboUpdateCheckFreq->currentIndex())); + g.downloadDir(ui->leDownloadDir->text()); + + g.decompressUseDwnld(ui->chkDecompressUseDwnld->isChecked()); + g.decompressDir(ui->leDecompressDir->text()); + + g.updateUseSD(ui->chkUpdateUseSD->isChecked()); + g.updateDir(ui->leUpdateDir->text()); + + QMapIterator it(factories->sortedComponentsList()); + + while (it.hasNext()) { + it.next(); + int i = it.value(); + + g.component[i].checkForUpdate(chkCheckForUpdate[i]->isChecked()); + g.component[i].releaseChannel((ComponentData::ReleaseChannel)cboReleaseChannel[i]->currentIndex()); + } + profile.defaultInternalModule(ui->defaultInternalModuleCB->currentData().toInt()); profile.channelOrder(ui->channelorderCB->currentIndex()); profile.defaultMode(ui->stickmodeCB->currentIndex()); @@ -169,11 +189,6 @@ void AppPreferencesDialog::initSettings() { const Profile & profile = g.currentProfile(); - // Disable these for now, until online elements are implemented - ui->autoCheckFirmware->setDisabled(true); - ui->autoCheckCompanion->setDisabled(true); - ui->OpenTxBranch->setDisabled(true); - ui->snapshotClipboardCKB->setChecked(g.snapToClpbrd()); ui->burnFirmware->setChecked(profile.burnFirmware()); ui->snapshotPath->setText(g.snapshotDir()); @@ -183,18 +198,6 @@ void AppPreferencesDialog::initSettings() ui->snapshotPathButton->setDisabled(true); } -#if !defined(ALLOW_NIGHTLY_BUILDS) - // remove nightly version option entirely unless it's the current one (because user might have nightly version also installed) - if (g.OpenTxBranch() != AppData::BRANCH_NIGHTLY_UNSTABLE) - ui->OpenTxBranch->removeItem(2); - // if it's already selected, add a warning message - else - ui->updatesLayout->addWidget(new QLabel("" % tr("Note: Nightly builds are not available in this version, Release/RC update channel will be used.") % "", this)); -#endif - ui->OpenTxBranch->setCurrentIndex(g.OpenTxBranch()); - - ui->autoCheckCompanion->setChecked(g.autoCheckApp()); - ui->autoCheckFirmware->setChecked(g.autoCheckFw()); ui->showSplash->setChecked(g.showSplash()); ui->chkPromptProfile->setChecked(g.promptProfile()); ui->historySize->setValue(g.historySize()); @@ -303,6 +306,134 @@ void AppPreferencesDialog::initSettings() } populateFirmwareOptions(getBaseFirmware()); + + // Updates tab + + ui->cboUpdateCheckFreq->addItems(AppData::updateCheckFreqsList()); + + connect(ui->btnResetUpdatesToDefaults, &QPushButton::clicked, [=]() { + if (QMessageBox::question(this, CPN_STR_APP_NAME, tr("Reset all update settings to defaults. Are you sure?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { + for (int i = 0; i < MAX_COMPONENTS; i++) { + if (g.component[i].existsOnDisk()) + g.getComponent(i).resetAll(); + } + g.resetUpdatesSettings(); + QMessageBox::warning(this, CPN_STR_APP_NAME, + tr("Update settings have been reset. You must restart Companion to avoid unexpected behaviour!")); + } + }); + + connect(ui->chkDecompressUseDwnld, &QCheckBox::toggled, [=](const bool checked) { + if (!checked) { + ui->leDecompressDir->setText(g.decompressDir()); + ui->leDecompressDir->setEnabled(true); + ui->btnDecompressSelect->setEnabled(true); + } + else { + ui->leDecompressDir->setText(g.downloadDir()); + ui->leDecompressDir->setEnabled(false); + ui->btnDecompressSelect->setEnabled(false); + } + }); + + connect(ui->chkUpdateUseSD, &QCheckBox::toggled, [=](const bool checked) { + if (!checked) { + ui->leUpdateDir->setText(g.updateDir()); + ui->leUpdateDir->setEnabled(true); + ui->btnUpdateSelect->setEnabled(true); + } + else { + ui->leUpdateDir->setText(g.currentProfile().sdPath()); + ui->leUpdateDir->setEnabled(false); + ui->btnUpdateSelect->setEnabled(false); + } + }); + + connect(ui->btnDownloadSelect, &QPushButton::clicked, [=]() { + QString dirPath = QFileDialog::getExistingDirectory(this,tr("Select your download folder"), g.downloadDir()); + if (!dirPath.isEmpty()) { + ui->leDownloadDir->setText(dirPath); + } + }); + + connect(ui->btnDecompressSelect, &QPushButton::clicked, [=]() { + QString dirPath = QFileDialog::getExistingDirectory(this,tr("Select your decompress folder"), g.decompressDir()); + if (!dirPath.isEmpty()) { + ui->leDecompressDir->setText(dirPath); + } + }); + + connect(ui->btnUpdateSelect, &QPushButton::clicked, [=]() { + QString dirPath = QFileDialog::getExistingDirectory(this,tr("Select your update destination folder"), g.updateDir()); + if (!dirPath.isEmpty()) { + ui->leUpdateDir->setText(dirPath); + } + }); + + int row = 0; + int col = 0; + + QGridLayout *grid = new QGridLayout(); + + col++; // leave col 0 blank + + QLabel *h1 = new QLabel(tr("Check")); + grid->addWidget(h1, row, col++); + + QLabel *h2 = new QLabel(tr("Release channel")); + grid->addWidget(h2, row, col++); + + QSpacerItem * spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum ); + grid->addItem(spacer, row, col++); + + QMapIterator it(factories->sortedComponentsList()); + + while (it.hasNext()) { + it.next(); + int i = it.value(); + + row++; + col = 0; + + lblName[i] = new QLabel(); + grid->addWidget(lblName[i], row, col++); + + chkCheckForUpdate[i] = new QCheckBox(); + grid->addWidget(chkCheckForUpdate[i], row, col++); + grid->setAlignment(chkCheckForUpdate[i], Qt::AlignHCenter); + + cboReleaseChannel[i] = new QComboBox(); + cboReleaseChannel[i]->addItems(ComponentData::releaseChannelsList()); + grid->addWidget(cboReleaseChannel[i], row, col++); + } + + ui->grpComponents->setLayout(grid); + + loadUpdatesTab(); +} + +void AppPreferencesDialog::loadUpdatesTab() +{ + ui->cboUpdateCheckFreq->setCurrentIndex(g.updateCheckFreq()); + ui->leDownloadDir->setText(g.downloadDir()); + // trigger toggled signal by changing design value and then setting to saved value + ui->chkDecompressUseDwnld->setChecked(!ui->chkDecompressUseDwnld->isChecked()); + ui->chkDecompressUseDwnld->setChecked(g.decompressUseDwnld()); + // trigger toggled signal by changing design value and then setting to saved value + ui->chkUpdateUseSD->setChecked(!ui->chkUpdateUseSD->isChecked()); + ui->chkUpdateUseSD->setChecked(g.updateUseSD()); + + QMapIterator it(factories->sortedComponentsList()); + + while (it.hasNext()) { + it.next(); + int i = it.value(); + + lblName[i]->setText(it.key()); + chkCheckForUpdate[i]->setChecked(g.component[i].checkForUpdate()); + cboReleaseChannel[i]->setCurrentIndex(g.component[i].releaseChannel()); + } } void AppPreferencesDialog::on_libraryPathButton_clicked() diff --git a/companion/src/apppreferencesdialog.h b/companion/src/apppreferencesdialog.h index 7e3f6f234..9b6e82331 100644 --- a/companion/src/apppreferencesdialog.h +++ b/companion/src/apppreferencesdialog.h @@ -18,25 +18,28 @@ * GNU General Public License for more details. */ -#ifndef _APPPREFERENCESDIALOG_H_ -#define _APPPREFERENCESDIALOG_H_ +#pragma once + +#include "eeprominterface.h" +#include "appdata.h" #include #include -#include "eeprominterface.h" +#include namespace Ui { class AppPreferencesDialog; } class Joystick; +class UpdateFactories; class AppPreferencesDialog : public QDialog { Q_OBJECT public: - explicit AppPreferencesDialog(QWidget * parent = 0); + explicit AppPreferencesDialog(QWidget * parent, UpdateFactories * factories); ~AppPreferencesDialog(); Joystick * joystick; @@ -84,6 +87,12 @@ class AppPreferencesDialog : public QDialog QMap optionsCheckBoxes; bool updateLock; bool mainWinHasDirtyChild; -}; -#endif // _APPPREFERENCESDIALOG_H_ + UpdateFactories *factories; + QLabel *lblName[MAX_COMPONENTS]; + QCheckBox *chkCheckForUpdate[MAX_COMPONENTS]; + QComboBox *cboReleaseChannel[MAX_COMPONENTS]; + + void loadUpdatesTab(); + +}; diff --git a/companion/src/apppreferencesdialog.ui b/companion/src/apppreferencesdialog.ui index 4ada1faed..3126fe96e 100644 --- a/companion/src/apppreferencesdialog.ui +++ b/companion/src/apppreferencesdialog.ui @@ -36,7 +36,7 @@ - + @@ -45,7 +45,7 @@ - 1 + 3 @@ -732,7 +732,7 @@ Mode 4: 6 - + @@ -745,8 +745,8 @@ Mode 4: - - + + 0 @@ -754,36 +754,89 @@ Mode 4: - Google Earth Executable + Startup Settings - - + + + + Qt::Horizontal + + + + + - + 0 0 - - - Releases (stable) - - - - - Release candidates (testing) - - - - - Nightly builds (unstable) - - + + Remember + - + + + + + + <html><head/><body><p>Keep a log of all debugging messages generated by the desktop Companion/Simulator applications. An EdgeTX developer may request this to help diagnose an issue.</p></body></html> + + + Application (Companion/Simulator) + + + + + + + <html><head/><body><p>Keep a log of all messages generated by the radio firmware when running in Simulator. This is the same information one would also see in the Simulator <span style=" font-style:italic;">Debug Output</span> window.</p></body></html> + + + Radio Firmware (in Simulator) + + + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + Debug Output Logging + + + + + + + Prompt for radio profile + + + + + + + Qt::Horizontal + + + + @@ -803,20 +856,86 @@ Mode 4: - - + + Qt::Horizontal - - - - Qt::Horizontal + + + + Show splash screen + + + true + + + + + 0 + 0 + + + + Action on New Model + + + + + + + + 0 + 0 + + + + User Splash Screens + + + + + + + + 0 + 0 + + + + Google Earth Executable + + + + + + + + + true + + + + + + + + 0 + 0 + + + + Select Executable + + + + + @@ -853,41 +972,50 @@ Mode 4: - - - - Show splash screen - - - true + + + + Qt::Horizontal - - - - - - true - - - - - - - - 0 - 0 - - - - Select Executable - - - - + + + + + 0 + 0 + + + + Splash Screen Library + + - + + + + + 0 + 0 + + + + Output Logs Folder + + + + + + + <html><head/><body><p>This option maintains the behaviour from older OpenTx versions where empty model slots are preserved when a model is deleted or moved. </p><p>When this option is de-selected, the other models may be re-arranged to fill the gap left by the removed model.</p></body></html> + + + Remove empty model slots when deleting models (only applies for radios w/out categories) + + + + @@ -911,93 +1039,10 @@ Mode 4: - - - - - 0 - 0 - - - - Remember - - - - - - - - 0 - 0 - - - - Output Logs Folder - - - - - - - - 0 - 0 - - - - Startup Settings - - - - - - - <html><head/><body><p>This option maintains the behaviour from older OpenTx versions where empty model slots are preserved when a model is deleted or moved. </p><p>When this option is de-selected, the other models may be re-arranged to fill the gap left by the removed model.</p></body></html> - - - Remove empty model slots when deleting models (only applies for radios w/out categories) - - - - - - - - 0 - 0 - - - - User Splash Screens - - + + - - - - - <html><head/><body><p>Keep a log of all debugging messages generated by the desktop Companion/Simulator applications. An EdgeTX developer may request this to help diagnose an issue.</p></body></html> - - - Application (Companion/Simulator) - - - - - - - <html><head/><body><p>Keep a log of all messages generated by the radio firmware when running in Simulator. This is the same information one would also see in the Simulator <span style=" font-style:italic;">Debug Output</span> window.</p></body></html> - - - Radio Firmware (in Simulator) - - - - - - @@ -1018,20 +1063,7 @@ Mode 4: - - - - - 0 - 0 - - - - Action on New Model - - - - + @@ -1058,54 +1090,7 @@ Mode 4: - - - - - 0 - 0 - - - - Splash Screen Library - - - - - - - Release channel - - - - - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - Debug Output Logging - - - - - - - Qt::Horizontal - - - - + @@ -1118,47 +1103,6 @@ Mode 4: - - - - - - Automatic check for firmware updates - - - true - - - - - - - Automatic check for Companion updates - - - true - - - - - - - - - Qt::Horizontal - - - - - - - Prompt for radio profile - - - - - - @@ -1425,6 +1369,161 @@ Mode 4: + + + Update Settings + + + + + + + + Check frequency + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Reset to Defaults + + + + + + + + + Folders + + + + + + Download + + + + + + + + + + Update + + + + + + + Create sub-folders in Download folder + + + true + + + + + + + false + + + + + + + Decompress + + + + + + + Use Radio Profile SD Structure + + + true + + + + + + + false + + + + + + + false + + + Select Folder + + + + + + + false + + + Select Folder + + + + + + + Select Folder + + + + + + + + + + Components + + + + + + + Qt::Vertical + + + + 671 + 240 + + + + + + @@ -1488,7 +1587,4 @@ Mode 4: - - - diff --git a/companion/src/companion.qrc b/companion/src/companion.qrc index 144bfeaad..62af87e77 100644 --- a/companion/src/companion.qrc +++ b/companion/src/companion.qrc @@ -405,7 +405,7 @@ themes/monochrome/16/compare.png themes/monochrome/16/saveas.png themes/monochrome/16/apppreferences.png - themes/monochrome/16/fwpreferences.png + themes/monochrome/16/download.png themes/monochrome/16/movedown.png themes/monochrome/16/currentmodel.png themes/monochrome/16/read_flash.png @@ -449,7 +449,7 @@ themes/monochrome/24/compare.png themes/monochrome/24/saveas.png themes/monochrome/24/apppreferences.png - themes/monochrome/24/fwpreferences.png + themes/monochrome/24/download.png themes/monochrome/24/movedown.png themes/monochrome/24/currentmodel.png themes/monochrome/24/read_flash.png @@ -493,7 +493,7 @@ themes/monochrome/32/compare.png themes/monochrome/32/saveas.png themes/monochrome/32/apppreferences.png - themes/monochrome/32/fwpreferences.png + themes/monochrome/32/download.png themes/monochrome/32/movedown.png themes/monochrome/32/currentmodel.png themes/monochrome/32/read_flash.png @@ -537,7 +537,7 @@ themes/monochrome/48/compare.png themes/monochrome/48/saveas.png themes/monochrome/48/apppreferences.png - themes/monochrome/48/fwpreferences.png + themes/monochrome/48/download.png themes/monochrome/48/movedown.png themes/monochrome/48/currentmodel.png themes/monochrome/48/read_flash.png @@ -581,7 +581,7 @@ themes/classic/16/compare.png themes/classic/16/saveas.png themes/classic/16/apppreferences.png - themes/classic/16/fwpreferences.png + themes/classic/16/download.png themes/classic/16/movedown.png themes/classic/16/currentmodel.png themes/classic/16/read_flash.png @@ -625,7 +625,7 @@ themes/classic/24/compare.png themes/classic/24/saveas.png themes/classic/24/apppreferences.png - themes/classic/24/fwpreferences.png + themes/classic/24/download.png themes/classic/24/movedown.png themes/classic/24/currentmodel.png themes/classic/24/read_flash.png @@ -669,7 +669,7 @@ themes/classic/32/compare.png themes/classic/32/saveas.png themes/classic/32/apppreferences.png - themes/classic/32/fwpreferences.png + themes/classic/32/download.png themes/classic/32/movedown.png themes/classic/32/currentmodel.png themes/classic/32/read_flash.png @@ -713,7 +713,7 @@ themes/classic/48/compare.png themes/classic/48/saveas.png themes/classic/48/apppreferences.png - themes/classic/48/fwpreferences.png + themes/classic/48/download.png themes/classic/48/movedown.png themes/classic/48/currentmodel.png themes/classic/48/read_flash.png @@ -757,7 +757,7 @@ themes/monoblue/16/compare.png themes/monoblue/16/saveas.png themes/monoblue/16/apppreferences.png - themes/monoblue/16/fwpreferences.png + themes/monoblue/16/download.png themes/monoblue/16/movedown.png themes/monoblue/16/currentmodel.png themes/monoblue/16/read_flash.png @@ -801,7 +801,7 @@ themes/monoblue/24/compare.png themes/monoblue/24/saveas.png themes/monoblue/24/apppreferences.png - themes/monoblue/24/fwpreferences.png + themes/monoblue/24/download.png themes/monoblue/24/movedown.png themes/monoblue/24/currentmodel.png themes/monoblue/24/read_flash.png @@ -845,7 +845,7 @@ themes/monoblue/32/compare.png themes/monoblue/32/saveas.png themes/monoblue/32/apppreferences.png - themes/monoblue/32/fwpreferences.png + themes/monoblue/32/download.png themes/monoblue/32/movedown.png themes/monoblue/32/currentmodel.png themes/monoblue/32/read_flash.png @@ -889,7 +889,7 @@ themes/monoblue/48/compare.png themes/monoblue/48/saveas.png themes/monoblue/48/apppreferences.png - themes/monoblue/48/fwpreferences.png + themes/monoblue/48/download.png themes/monoblue/48/movedown.png themes/monoblue/48/currentmodel.png themes/monoblue/48/read_flash.png @@ -933,7 +933,7 @@ themes/monowhite/16/compare.png themes/monowhite/16/saveas.png themes/monowhite/16/apppreferences.png - themes/monowhite/16/fwpreferences.png + themes/monowhite/16/download.png themes/monowhite/16/movedown.png themes/monowhite/16/currentmodel.png themes/monowhite/16/read_flash.png @@ -977,7 +977,7 @@ themes/monowhite/24/compare.png themes/monowhite/24/saveas.png themes/monowhite/24/apppreferences.png - themes/monowhite/24/fwpreferences.png + themes/monowhite/24/download.png themes/monowhite/24/movedown.png themes/monowhite/24/currentmodel.png themes/monowhite/24/read_flash.png @@ -1021,7 +1021,7 @@ themes/monowhite/32/compare.png themes/monowhite/32/saveas.png themes/monowhite/32/apppreferences.png - themes/monowhite/32/fwpreferences.png + themes/monowhite/32/download.png themes/monowhite/32/movedown.png themes/monowhite/32/currentmodel.png themes/monowhite/32/read_flash.png @@ -1065,7 +1065,7 @@ themes/monowhite/48/compare.png themes/monowhite/48/saveas.png themes/monowhite/48/apppreferences.png - themes/monowhite/48/fwpreferences.png + themes/monowhite/48/download.png themes/monowhite/48/movedown.png themes/monowhite/48/currentmodel.png themes/monowhite/48/read_flash.png @@ -1109,7 +1109,7 @@ themes/yerico/16/compare.png themes/yerico/16/saveas.png themes/yerico/16/apppreferences.png - themes/yerico/16/fwpreferences.png + themes/yerico/16/download.png themes/yerico/16/movedown.png themes/yerico/16/currentmodel.png themes/yerico/16/read_flash.png @@ -1153,7 +1153,7 @@ themes/yerico/24/compare.png themes/yerico/24/saveas.png themes/yerico/24/apppreferences.png - themes/yerico/24/fwpreferences.png + themes/yerico/24/download.png themes/yerico/24/movedown.png themes/yerico/24/currentmodel.png themes/yerico/24/read_flash.png @@ -1197,7 +1197,7 @@ themes/yerico/32/compare.png themes/yerico/32/saveas.png themes/yerico/32/apppreferences.png - themes/yerico/32/fwpreferences.png + themes/yerico/32/download.png themes/yerico/32/movedown.png themes/yerico/32/currentmodel.png themes/yerico/32/read_flash.png @@ -1241,7 +1241,7 @@ themes/yerico/48/compare.png themes/yerico/48/saveas.png themes/yerico/48/apppreferences.png - themes/yerico/48/fwpreferences.png + themes/yerico/48/download.png themes/yerico/48/movedown.png themes/yerico/48/currentmodel.png themes/yerico/48/read_flash.png diff --git a/companion/src/constants.h b/companion/src/constants.h index 5e7a2d9d2..7fd61e667 100644 --- a/companion/src/constants.h +++ b/companion/src/constants.h @@ -18,8 +18,7 @@ * GNU General Public License for more details. */ -#ifndef _CONSTANTS_H_ -#define _CONSTANTS_H_ +#pragma once #include @@ -84,5 +83,3 @@ #define CPN_STR_SW_INDICATOR_REV QCoreApplication::translate("RawSwitch", "!") // Switch reversed logic (NOT) indicator. #define EDGETX_HOME_PAGE_URL "https://edgetx.org" - -#endif // _CONSTANTS_H_ diff --git a/companion/src/firmwares/opentx/opentxinterface.cpp b/companion/src/firmwares/opentx/opentxinterface.cpp index 577325876..6b50a4034 100644 --- a/companion/src/firmwares/opentx/opentxinterface.cpp +++ b/companion/src/firmwares/opentx/opentxinterface.cpp @@ -1219,27 +1219,11 @@ unsigned long OpenTxEepromInterface::loadBackup(RadioData &radioData, const uint return errors.to_ulong(); } -QString OpenTxFirmware::getFirmwareBaseUrl() -{ - return g.openTxCurrentDownloadBranchUrl() % QStringLiteral("firmware/"); -} - -QString OpenTxFirmware::getFirmwareUrl() -{ - return getFirmwareBaseUrl() % QString("getfw.php?fw=%1.bin").arg(QString(QUrl::toPercentEncoding(id))); -} - QString OpenTxFirmware::getReleaseNotesUrl() { return QString("%1/downloads").arg(EDGETX_HOME_PAGE_URL); } -QString OpenTxFirmware::getStampUrl() -{ - return getFirmwareBaseUrl() % QStringLiteral("stamp-opentx.txt"); -} - - // Firmware registrations // NOTE: "recognized" build options are defined in /radio/util/fwoptions.py diff --git a/companion/src/firmwares/opentx/opentxinterface.h b/companion/src/firmwares/opentx/opentxinterface.h index e520cc18e..616f74933 100644 --- a/companion/src/firmwares/opentx/opentxinterface.h +++ b/companion/src/firmwares/opentx/opentxinterface.h @@ -111,11 +111,11 @@ class OpenTxFirmware: public Firmware virtual Firmware * getFirmwareVariant(const QString & id); - virtual QString getStampUrl(); + virtual QString getStampUrl() { return ""; } // depreciated virtual QString getReleaseNotesUrl(); - virtual QString getFirmwareUrl(); + virtual QString getFirmwareUrl() { return ""; } // depreciated; virtual int getCapability(Capability); @@ -129,7 +129,7 @@ class OpenTxFirmware: public Firmware protected: - QString getFirmwareBaseUrl(); + QString getFirmwareBaseUrl() { return ""; } // depreciated; }; diff --git a/companion/src/fwpreferencesdialog.cpp b/companion/src/fwpreferencesdialog.cpp deleted file mode 100644 index eaee4a7dd..000000000 --- a/companion/src/fwpreferencesdialog.cpp +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) OpenTX - * - * Based on code named - * th9x - http://code.google.com/p/th9x - * er9x - http://code.google.com/p/er9x - * gruvin9x - http://code.google.com/p/gruvin9x - * - * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * 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. - */ - -#include "fwpreferencesdialog.h" -#include "ui_fwpreferencesdialog.h" -#include "mainwindow.h" -#include "helpers.h" -#include "appdata.h" - -FirmwarePreferencesDialog::FirmwarePreferencesDialog(QWidget * parent) : - QDialog(parent), - ui(new Ui::FirmwarePreferencesDialog) -{ - ui->setupUi(this); - setWindowIcon(CompanionIcon("fwpreferences.png")); - initSettings(); - MainWindow * mw = qobject_cast(this->parent()); - if (mw) - connect(mw, &MainWindow::firmwareDownloadCompleted, this, &FirmwarePreferencesDialog::initSettings); -} - -FirmwarePreferencesDialog::~FirmwarePreferencesDialog() -{ - delete ui; -} - -void FirmwarePreferencesDialog::initSettings() -{ - ui->fwTypeLbl->setText(g.currentProfile().fwType()); - int version = g.fwRev.get(g.currentProfile().fwType()); - if (version > 0) - ui->lastRevisionLbl->setText(index2version(version)); - else - ui->lastRevisionLbl->setText(tr("Unknown")); -} - -void FirmwarePreferencesDialog::on_checkFWUpdates_clicked() -{ - MainWindow * mw = qobject_cast(this->parent()); - if (mw) - mw->checkForFirmwareUpdate(); -} - -void FirmwarePreferencesDialog::on_fw_dnld_clicked() -{ - MainWindow * mw = qobject_cast(this->parent()); - if (mw) - mw->dowloadLastFirmwareUpdate(); -} - -void FirmwarePreferencesDialog::on_sd_dnld_clicked() -{ - QString url = g.openTxCurrentDownloadBranchUrl() % QStringLiteral("sdcard/"); - QString fwType = g.profile[g.id()].fwType(); - QStringList list = fwType.split("-"); - QString firmware = QString("%1-%2").arg(list[0]).arg(list[1]); - if (g.boundedOpenTxBranch() != AppData::BRANCH_NIGHTLY_UNSTABLE) { - url.append(QString("%1/").arg(firmware)); - } - QDesktopServices::openUrl(url); -} diff --git a/companion/src/fwpreferencesdialog.ui b/companion/src/fwpreferencesdialog.ui deleted file mode 100644 index 54885149a..000000000 --- a/companion/src/fwpreferencesdialog.ui +++ /dev/null @@ -1,180 +0,0 @@ - - - FirmwarePreferencesDialog - - - - 0 - 0 - 416 - 154 - - - - - 0 - 0 - - - - - 0 - 0 - - - - Downloads - - - - 6 - - - 6 - - - 6 - - - 6 - - - 2 - - - - - Qt::Horizontal - - - QDialogButtonBox::Ok - - - - - - - - 0 - 0 - - - - Check for updates - - - - - - - - 0 - 0 - - - - - Sans Serif - 75 - true - - - - Firmware - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - Download firmware - - - - - - - Qt::Horizontal - - - - - - - Download SD contents - - - - - - - - - - - - - - - Latest Download - - - - - - - - 0 - 0 - - - - FwType - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - checkFWUpdates - fw_dnld - sd_dnld - buttonBox - - - - - - - buttonBox - accepted() - FirmwarePreferencesDialog - close() - - - 345 - 133 - - - 207 - 76 - - - - - diff --git a/companion/src/images/originals/icons Classical/download.png b/companion/src/images/originals/icons Classical/download.png new file mode 100644 index 0000000000000000000000000000000000000000..7574da86aa9c03e81af8e5152ee885c17034bfe4 GIT binary patch literal 82297 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 YmUT|+$ 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�YSTtBKuRICGW8=DL{aHg} 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 z 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 Mp00i_>zopr0Q9lE4*&oF literal 0 HcmV?d00001 diff --git a/companion/src/mainwindow.cpp b/companion/src/mainwindow.cpp index f12861a4d..29f6dd35d 100644 --- a/companion/src/mainwindow.cpp +++ b/companion/src/mainwindow.cpp @@ -24,13 +24,9 @@ #include "comparedialog.h" #include "logsdialog.h" #include "apppreferencesdialog.h" -#include "fwpreferencesdialog.h" #include "firmwareinterface.h" -#include "downloaddialog.h" #include "printdialog.h" #include "version.h" -#include "releasenotesdialog.h" -#include "releasenotesfirmwaredialog.h" #include "customizesplashdialog.h" #include "flasheepromdialog.h" #include "flashfirmwaredialog.h" @@ -49,51 +45,22 @@ #include "dialogs/filesyncdialog.h" #include "profilechooser.h" #include "constants.h" +#include "updates/updates.h" +#include "updates/updateinterface.h" #include #include #include #include -#include -#include -#include -#include -// update check flags -#define CHECK_COMPANION 1 -#define CHECK_FIRMWARE 2 -#define INTERACTIVE_DOWNLOAD 4 -#define AUTOMATIC_DOWNLOAD 8 - -#define EDGETX_DOWNLOADS_PAGE_URL QStringLiteral("https://edgetx.org/downloads") #define DONATE_STR QStringLiteral("https://opencollective.com/edgetx/donate") -#ifdef Q_OS_MACOS - #define COMPANION_STAMP QStringLiteral("companion-macosx.stamp") - #define COMPANION_INSTALLER QStringLiteral("macosx/opentx-companion-%1.dmg") - #define COMPANION_FILEMASK QT_TRANSLATE_NOOP("MainWindow", "Diskimage (*.dmg)") - #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to open the disk image to install the new version?") -#elif defined(Q_OS_WIN) - #define COMPANION_STAMP QStringLiteral("companion-windows.stamp") - #define COMPANION_INSTALLER QStringLiteral("windows/companion-windows-%1.exe") - #define COMPANION_FILEMASK QT_TRANSLATE_NOOP("MainWindow", "Executable (*.exe)") - #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to launch the installer?") -#else - #define COMPANION_STAMP QStringLiteral("companion-linux.stamp") - #define COMPANION_INSTALLER "" // no automated updates for linux - #define COMPANION_FILEMASK QStringLiteral("*.*") - #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to launch the installer?") -#endif - MainWindow::MainWindow(): - downloadDialog_forWait(nullptr), - checkForUpdatesState(0), - networkManager(nullptr), + updateFactories(nullptr), windowsListActions(new QActionGroup(this)) { // setUnifiedTitleAndToolBarOnMac(true); this->setWindowIcon(QIcon(":/icon.png")); - QNetworkProxyFactory::setUseSystemConfiguration(true); setAcceptDrops(true); mdiArea = new QMdiArea(this); @@ -179,8 +146,10 @@ MainWindow::MainWindow(): QTimer::singleShot(0, this, SLOT(autoClose())); } + updateFactories = new UpdateFactories(this); + if (checkProfileRadioExists(g.sessionId())) - QTimer::singleShot(updateDelay, this, SLOT(doAutoUpdates())); + QTimer::singleShot(updateDelay, this, &MainWindow::autoUpdates); else g.warningId(g.warningId() | AppMessages::MSG_NO_RADIO_TYPE); } @@ -191,6 +160,11 @@ MainWindow::~MainWindow() delete windowsListActions; windowsListActions = nullptr; } + + if (updateFactories) { + delete updateFactories; + updateFactories = nullptr; + } } void MainWindow::initWindowOptions() @@ -221,388 +195,6 @@ void MainWindow::displayWarnings() displayWarnings(); // in case more warnings need showing } -void MainWindow::doAutoUpdates() -{ - if (g.autoCheckApp()) - checkForUpdatesState |= CHECK_COMPANION; - if (g.autoCheckFw()) - checkForUpdatesState |= CHECK_FIRMWARE; - checkForUpdates(); -} - -void MainWindow::doUpdates() -{ - checkForUpdatesState = CHECK_COMPANION | CHECK_FIRMWARE | INTERACTIVE_DOWNLOAD; - checkForUpdates(); -} - -void MainWindow::checkForFirmwareUpdate() -{ - checkForUpdatesState = CHECK_FIRMWARE | INTERACTIVE_DOWNLOAD; - checkForUpdates(); -} - -void MainWindow::dowloadLastFirmwareUpdate() -{ - checkForUpdatesState = CHECK_FIRMWARE | AUTOMATIC_DOWNLOAD | INTERACTIVE_DOWNLOAD; - checkForUpdates(); -} - -QString MainWindow::getCompanionUpdateBaseUrl() const -{ - return g.openTxCurrentDownloadBranchUrl() % QStringLiteral("companion/"); -} - -void MainWindow::checkForUpdates() -{ - if (!(checkForUpdatesState & (CHECK_COMPANION | CHECK_FIRMWARE))) { - if (networkManager) { - networkManager->deleteLater(); - networkManager = nullptr; - } - checkForUpdatesState = 0; - return; - } - - QMessageBox::information(this, CPN_STR_APP_NAME, tr("Updates via Companion currently unavailable. Please go to the EdgeTX website for installation instructions. Update the application settings to disable this message.").arg("https://github.com/EdgeTX/edgetx.github.io/wiki/EdgeTX-Installation-Guide")); - return; - - if (networkManager) - disconnect(networkManager, 0, this, 0); - else - networkManager = new QNetworkAccessManager(this); - - QUrl url; - if (checkForUpdatesState & CHECK_COMPANION) { - checkForUpdatesState -= CHECK_COMPANION; - if (checkForUpdatesState & INTERACTIVE_DOWNLOAD) - openUpdatesWaitDialog(); - url.setUrl(getCompanionUpdateBaseUrl() % COMPANION_STAMP); - connect(networkManager, &QNetworkAccessManager::finished, this, &MainWindow::checkForCompanionUpdateFinished); - qDebug() << "Checking for Companion update " << url.url(); - } - else if (checkForUpdatesState & CHECK_FIRMWARE) { - checkForUpdatesState -= CHECK_FIRMWARE; - const QString stamp = getCurrentFirmware()->getStampUrl(); - if (!stamp.isEmpty()) { - if (checkForUpdatesState & INTERACTIVE_DOWNLOAD) - openUpdatesWaitDialog(); - url.setUrl(stamp); - connect(networkManager, &QNetworkAccessManager::finished, this, &MainWindow::checkForFirmwareUpdateFinished); - qDebug() << "Checking for firmware update " << url.url(); - } - } - if (!url.isValid()) { - checkForUpdates(); - return; - } - - QNetworkRequest request(url); - request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - QNetworkReply * repl = networkManager->get(request); - if (downloadDialog_forWait) - connect(downloadDialog_forWait, &DownloadDialog::rejected, repl, &QNetworkReply::abort); -} - -void MainWindow::onUpdatesError(const QString &err) -{ - QMessageBox::warning(this, CPN_STR_APP_NAME, err); - checkForUpdates(); -} - -void MainWindow::openUpdatesWaitDialog() -{ - if (!downloadDialog_forWait) { - downloadDialog_forWait = new DownloadDialog(nullptr, tr("Checking for updates")); - downloadDialog_forWait->show(); - } -} - -void MainWindow::closeUpdatesWaitDialog() -{ - if (downloadDialog_forWait) { - downloadDialog_forWait->close(); - delete downloadDialog_forWait; - downloadDialog_forWait = nullptr; - } -} - -QString MainWindow::seekCodeString(const QByteArray & qba, const QString & label) const -{ - int posLabel = qba.indexOf(label); - if (posLabel < 0) - return QString(); - int start = qba.indexOf("\"", posLabel + label.length()); - if (start < 0) - return QString(); - int end = qba.indexOf("\"", start + 1); - if (end < 0) - return QString(); - return qba.mid(start + 1, end - start - 1); -} - -void MainWindow::checkForCompanionUpdateFinished(QNetworkReply * reply) -{ - QByteArray qba = reply->readAll(); - reply->deleteLater(); - closeUpdatesWaitDialog(); - - QString version = seekCodeString(qba, "VERSION"); - const QString errorString = seekCodeString(qba, "ERROR"); - - if (errorString == "NO_RC") - return onUpdatesError(tr("No Companion release candidates are currently being served for this version, please switch release channel")); - else if (errorString == "NO_NIGHTLY") - return onUpdatesError(tr("No nightly Companion builds are currently being served for this version, please switch release channel")); - else if (errorString == "NO_RELEASE") - return onUpdatesError(tr("No Companion release builds are currently being served for this version, please switch release channel")); - - if (version.isNull()) - return onUpdatesError(tr("Companion update check failed, new version information not found.")); - - int webVersion = version2index(version); - int ownVersion = version2index(VERSION); - - if (ownVersion < webVersion) { -#if defined WIN32 || defined __APPLE__ - int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr("A new version of Companion is available (version %1)
" - "Would you like to download it?").arg(version) , - QMessageBox::Yes | QMessageBox::No); - - if (ret == QMessageBox::Yes) { - QDir dir(g.updatesDir()); - QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), dir.absoluteFilePath(QString(COMPANION_INSTALLER).arg(version)), tr(COMPANION_FILEMASK)); - - if (!fileName.isEmpty()) { - g.updatesDir(QFileInfo(fileName).dir().absolutePath()); - DownloadDialog * dd = new DownloadDialog(this, getCompanionUpdateBaseUrl() % QString(COMPANION_INSTALLER).arg(version), fileName); - installer_fileName = fileName; - connect(dd, SIGNAL(accepted()), this, SLOT(updateDownloaded())); - dd->exec(); - } - } -#else - QMessageBox::warning(this, tr("New release available"), tr("A new release of Companion is available, please check the EdgeTX website!").arg(EDGETX_DOWNLOADS_PAGE_URL)); -#endif - } - else { - if (checkForUpdatesState == INTERACTIVE_DOWNLOAD) { - QMessageBox::information(this, CPN_STR_APP_NAME, tr("No updates available at this time.")); - } - } - - checkForUpdates(); -} - -void MainWindow::updateDownloaded() -{ - int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr(COMPANION_INSTALL_QUESTION), QMessageBox::Yes | QMessageBox::No); - if (ret == QMessageBox::Yes) { - if (QDesktopServices::openUrl(QUrl::fromLocalFile(installer_fileName))) - QApplication::exit(); - } -} - -void MainWindow::firmwareDownloadAccepted() -{ - QString errormsg; - QFile file(g.profile[g.id()].fwName()); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { //reading HEX TEXT file - QMessageBox::critical(this, CPN_STR_TTL_ERROR, - tr("Error opening file %1:\n%2.") - .arg(g.profile[g.id()].fwName()) - .arg(file.errorString())); - return; - } - file.reset(); - QTextStream inputStream(&file); - QString hline = inputStream.readLine(); - if (hline.startsWith("ERROR:")) { - int errnum = hline.mid(6).toInt(); - switch(errnum) { - case 1: - errormsg = tr("Not enough flash available on this board for all the selected options"); - break; - case 2: - errormsg = tr("Compilation server temporary failure, try later"); - break; - case 3: - errormsg = tr("Compilation server too busy, try later"); - break; - case 4: - errormsg = tr("Compilation error"); - break; - case 5: - errormsg = tr("Invalid firmware"); - break; - case 6: - errormsg = tr("Invalid board"); - break; - case 7: - errormsg = tr("Invalid language"); - break; - default: - errormsg = tr("Unknown server failure, try later"); - break; - } - file.close(); - file.remove(); - QMessageBox::critical(this, CPN_STR_TTL_ERROR, errormsg); - return; - } - file.close(); - g.fwRev.set(Firmware::getCurrentVariant()->getId(), version2index(firmwareVersionString)); - if (g.profile[g.id()].burnFirmware()) { - int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Do you want to write the firmware to the radio now ?"), QMessageBox::Yes | QMessageBox::No); - if (ret == QMessageBox::Yes) { - writeFlash(g.profile[g.id()].fwName()); - } - } - emit firmwareDownloadCompleted(); -} - -void MainWindow::checkForFirmwareUpdateFinished(QNetworkReply * reply) -{ - bool download = false; - bool ignore = false; - - const QByteArray qba = reply->readAll(); - reply->deleteLater(); - closeUpdatesWaitDialog(); - - const QString versionString = seekCodeString(qba, "VERSION"); - const QString dateString = seekCodeString(qba, "DATE"); - const QString errorString = seekCodeString(qba, "ERROR"); - const QString blockedRadios = seekCodeString(qba, "BLOCK"); - long version; - - if (errorString == "NO_RC") - return onUpdatesError(tr("No firmware release candidates are currently being served for this version, please switch release channel")); - else if (errorString == "NO_NIGHTLY") - return onUpdatesError(tr("No firmware nightly builds are currently being served for this version, please switch release channel")); - else if (errorString == "NO_RELEASE") - return onUpdatesError(tr("No firmware release builds are currently being served for this version, please switch release channel")); - else if (errorString == "MOVE_TO_RC") { - QMessageBox msgbox; - msgbox.setIcon(QMessageBox::Question); - msgbox.setText(tr("Release candidate builds are now available for this version, would you like to switch to using them?")); - msgbox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - msgbox.setDefaultButton(QMessageBox::Yes); - - if(msgbox.exec() == QMessageBox::Yes) { - g.OpenTxBranch(AppData::DownloadBranchType(AppData::BRANCH_RC_TESTING)); - return onUpdatesError(tr("Channel changed to RC, please restart the download process")); - } - } - else if (errorString == "MOVE_TO_RELEASE") { - QMessageBox msgbox; - msgbox.setIcon(QMessageBox::Question); - msgbox.setText(tr("Official release builds are now available for this version, would you like to switch to using them?")); - msgbox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - msgbox.setDefaultButton(QMessageBox::Yes); - - if(msgbox.exec() == QMessageBox::Yes) { - g.OpenTxBranch(AppData::DownloadBranchType(AppData::BRANCH_RELEASE_STABLE)); - return onUpdatesError(tr("Channel changed to Release, please restart the download process")); - } - } - - QString variant = Firmware::getCurrentVariant()->getId(); - QStringList splitId = variant.split("-"); - if (blockedRadios.contains(splitId.value(1))) - return onUpdatesError(tr("This radio (%1) is not currently available in this firmware release channel").arg(getCurrentFirmware()->getName())); - - if (versionString.isNull() || dateString.isNull() || (version = version2index(versionString)) <= 0) - return onUpdatesError(tr("Firmware update check failed, new version information not found or invalid.")); - - QString fullVersionString = QString("%1 (%2)").arg(versionString).arg(dateString); - - if (checkForUpdatesState & AUTOMATIC_DOWNLOAD) { - checkForUpdatesState -= AUTOMATIC_DOWNLOAD; - download = true; - } - else { - int currentVersion = g.fwRev.get(Firmware::getCurrentVariant()->getId()); - QString currentVersionString = index2version(currentVersion); - - QString msgText; - if (currentVersion == 0) - msgText = tr("Firmware %1 does not seem to have ever been downloaded.\nVersion %2 is available.\nDo you want to download it now?\n\nWe recommend you view the release notes using the button below to learn about any changes that may be important to you.") - .arg(Firmware::getCurrentVariant()->getId()).arg(fullVersionString); - else if (version > currentVersion) - msgText = tr("A new version of %1 firmware is available:\n - current is %2\n - newer is %3\n\nDo you want to download it now?\n\nWe recommend you view the release notes using the button below to learn about any changes that may be important to you.") - .arg(Firmware::getCurrentVariant()->getId()).arg(currentVersionString).arg(fullVersionString); - else if (checkForUpdatesState == INTERACTIVE_DOWNLOAD) - QMessageBox::information(this, CPN_STR_APP_NAME, tr("No updates available at this time.")); - - if (currentVersion == 0 || version > currentVersion) { - QMessageBox msgBox; - msgBox.setWindowTitle(CPN_STR_APP_NAME); - QSpacerItem * horizontalSpacer = new QSpacerItem(500, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); - QGridLayout * layout = (QGridLayout*)msgBox.layout(); - layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount()); - - QString rn = getCurrentFirmware()->getReleaseNotesUrl(); - QAbstractButton *rnButton = nullptr; - msgBox.setText(msgText); - QAbstractButton *YesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole); - msgBox.addButton(tr("No"), QMessageBox::NoRole); - if (!rn.isEmpty()) { - rnButton = msgBox.addButton(tr("Release Notes"), QMessageBox::ActionRole); - } - msgBox.setIcon(QMessageBox::Question); - msgBox.resize(0, 0); - msgBox.exec(); - if (msgBox.clickedButton() == rnButton) { - QDesktopServices::openUrl(QUrl(rn)); - int ret2 = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Do you want to download version %1 now ?").arg(fullVersionString), QMessageBox::Yes | QMessageBox::No); - if (ret2 == QMessageBox::Yes) - download = true; - else - ignore = true; - } - else if (msgBox.clickedButton() == YesButton ) - download = true; - else - ignore = true; - } - } - - if (ignore) { - int res = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Ignore this version %1?").arg(fullVersionString), QMessageBox::Yes | QMessageBox::No); - if (res==QMessageBox::Yes) { - g.fwRev.set(Firmware::getCurrentVariant()->getId(), version); - } - } - else if (download == true) { - firmwareVersionString = versionString; - startFirmwareDownload(); - } - - checkForUpdates(); -} - -void MainWindow::startFirmwareDownload() -{ - QString url = Firmware::getCurrentVariant()->getFirmwareUrl(); - qDebug() << "Downloading firmware" << url; - QString ext = url.mid(url.lastIndexOf(".")); - QString defaultFilename = g.flashDir() + "/" + Firmware::getCurrentVariant()->getId(); - if (g.profile[g.id()].renameFwFiles()) { - defaultFilename += "-" + firmwareVersionString; - } - defaultFilename += ext; - - QString filename = QFileDialog::getSaveFileName(this, tr("Save As"), defaultFilename); - if (!filename.isEmpty()) { - g.profile[g.id()].fwName(filename); - g.flashDir(QFileInfo(filename).dir().absolutePath()); - DownloadDialog * dd = new DownloadDialog(this, url, filename); - connect(dd, SIGNAL(accepted()), this, SLOT(firmwareDownloadAccepted())); - dd->exec(); - } -} - void MainWindow::closeEvent(QCloseEvent *event) { g.mainWinGeo(saveGeometry()); @@ -643,6 +235,20 @@ void MainWindow::onLanguageChanged(QAction * act) setLanguage(lang); } +void MainWindow::autoUpdates() +{ + Updates *upd = new Updates(this, updateFactories); + upd->autoUpdates(); + delete upd; +} + +void MainWindow::manualUpdates() +{ + Updates *upd = new Updates(this, updateFactories); + upd->manualUpdates(); + delete upd; +} + void MainWindow::setTheme(int index) { g.theme(index); @@ -825,7 +431,7 @@ void MainWindow::loadProfile() void MainWindow::appPrefs() { - AppPreferencesDialog * dialog = new AppPreferencesDialog(this); + AppPreferencesDialog * dialog = new AppPreferencesDialog(this, updateFactories); dialog->setMainWinHasDirtyChild(anyChildrenDirty()); connect(dialog, &AppPreferencesDialog::firmwareProfileAboutToChange, this, &MainWindow::saveAll); connect(dialog, &AppPreferencesDialog::firmwareProfileChanged, this, &MainWindow::onCurrentProfileChanged); @@ -833,13 +439,6 @@ void MainWindow::appPrefs() dialog->deleteLater(); } -void MainWindow::fwPrefs() -{ - FirmwarePreferencesDialog * dialog = new FirmwarePreferencesDialog(this); - dialog->exec(); - dialog->deleteLater(); -} - void MainWindow::sdsync() { // remember user-selectable options for duration of session TODO: save to settings @@ -886,7 +485,7 @@ void MainWindow::sdsync() void MainWindow::changelog() { - QString link = "https://edgetx.org"; + QString link = "https://github.com/EdgeTX/edgetx/releases"; QDesktopServices::openUrl(QUrl(link)); } @@ -1191,14 +790,14 @@ void MainWindow::retranslateUi(bool showMsg) trAct(saveAsAct, tr("Save As..."), tr("Save Models and Settings file")); trAct(closeAct, tr("Close"), tr("Close Models and Settings file")); trAct(exitAct, tr("Exit"), tr("Exit the application")); - trAct(aboutAct, tr("About..."), tr("Show the application's About box")); + trAct(aboutAct, tr("About Companion..."), tr("Show the application's About box")); trAct(recentFilesAct, tr("Recent Files"), tr("List of recently used files")); trAct(profilesMenuAct, tr("Radio Profiles"), tr("Create or Select Radio Profiles")); trAct(logsAct, tr("View Log File..."), tr("Open and view log file")); trAct(appPrefsAct, tr("Settings..."), tr("Edit Settings")); - trAct(fwPrefsAct, tr("Download..."), tr("Download firmware and voice files")); - trAct(checkForUpdatesAct, tr("Check for Updates..."), tr("Check EdgeTX and Companion updates")); + trAct(updatesAct, tr("Updates..."), tr("Update EdgeTX and supporting resources")); + trAct(checkForUpdatesAct, tr("Check for updates..."), tr("Check for updates to EdgeTX and supporting resources")); trAct(changelogAct, tr("Release notes..."), tr("Show release notes")); trAct(compareAct, tr("Compare Models..."), tr("Compare models")); trAct(editSplashAct, tr("Edit Radio Splash Image..."), tr("Edit the splash image of your Radio")); @@ -1256,7 +855,7 @@ void MainWindow::createActions() logsAct = addAct("logs.png", SLOT(logFile()), tr("Ctrl+Alt+L")); appPrefsAct = addAct("apppreferences.png", SLOT(appPrefs()), QKeySequence::Preferences); - fwPrefsAct = addAct("fwpreferences.png", SLOT(fwPrefs()), tr("Ctrl+Alt+D")); + updatesAct = addAct("download.png", SLOT(manualUpdates()), tr("Ctrl+Alt+D")); compareAct = addAct("compare.png", SLOT(compare()), tr("Ctrl+Alt+R")); sdsyncAct = addAct("sdsync.png", SLOT(sdsync())); @@ -1283,7 +882,7 @@ void MainWindow::createActions() actCascadeWindows = addAct("", SLOT(cascadeSubWindows()), 0, mdiArea); actCloseAllWindows = addAct("", SLOT(closeAllSubWindows()), 0, mdiArea); - checkForUpdatesAct = addAct("update.png", SLOT(doUpdates())); + checkForUpdatesAct = addAct("update.png", SLOT(autoUpdates())); aboutAct = addAct("information.png", SLOT(about())); openDocURLAct = addAct("changelog.png", SLOT(openDocURL())); changelogAct = addAct("changelog.png", SLOT(changelog())); @@ -1301,7 +900,6 @@ void MainWindow::createActions() actTabbedWindows->setCheckable(true); compareAct->setEnabled(false); - fwPrefsAct->setEnabled(false); } void MainWindow::createMenus() @@ -1315,7 +913,7 @@ void MainWindow::createMenus() fileMenu->addAction(recentFilesAct); fileMenu->addSeparator(); fileMenu->addAction(logsAct); - fileMenu->addAction(fwPrefsAct); + fileMenu->addAction(updatesAct); fileMenu->addAction(compareAct); fileMenu->addAction(sdsyncAct); fileMenu->addSeparator(); @@ -1374,14 +972,13 @@ void MainWindow::createMenus() windowMenu->addSeparator(); helpMenu = menuBar()->addMenu(""); - helpMenu->addSeparator(); helpMenu->addAction(checkForUpdatesAct); - helpMenu->addSeparator(); - helpMenu->addAction(aboutAct); - helpMenu->addAction(openDocURLAct); - helpMenu->addSeparator(); + //helpMenu->addSeparator(); + //helpMenu->addAction(openDocURLAct); + //helpMenu->addSeparator(); helpMenu->addAction(changelogAct); - helpMenu->addSeparator(); + //helpMenu->addSeparator(); + helpMenu->addAction(aboutAct); recentFilesMenu = new QMenu(this); recentFilesMenu->setToolTipsVisible(true); @@ -1419,7 +1016,7 @@ void MainWindow::createToolBars() fileToolBar->addAction(closeAct); fileToolBar->addSeparator(); fileToolBar->addAction(logsAct); - fileToolBar->addAction(fwPrefsAct); + fileToolBar->addAction(updatesAct); fileToolBar->addSeparator(); fileToolBar->addAction(appPrefsAct); fileToolBar->addAction(profilesMenuAct); diff --git a/companion/src/mainwindow.h b/companion/src/mainwindow.h index 207857c57..3dd40afa9 100644 --- a/companion/src/mainwindow.h +++ b/companion/src/mainwindow.h @@ -18,17 +18,17 @@ * GNU General Public License for more details. */ -#ifndef _MAINWINDOW_H_ -#define _MAINWINDOW_H_ +#pragma once #include #include -#include "downloaddialog.h" #include "eeprominterface.h" #define SPLASH_TIME 5 class MdiChild; +class UpdateFactories; + QT_BEGIN_NAMESPACE class QAction; class QMenu; @@ -47,18 +47,11 @@ class MainWindow : public QMainWindow ~MainWindow(); signals: - void firmwareDownloadCompleted(); void firmwareChanged(); void startSync(); void onInternalModuleChanged(); - protected: - QString getCompanionUpdateBaseUrl() const; - QString seekCodeString(const QByteArray & qba, const QString & label) const; - protected slots: - void dowloadLastFirmwareUpdate(); - void startFirmwareDownload(); virtual void closeEvent(QCloseEvent *event); virtual void changeEvent(QEvent *e); virtual void dragEnterEvent(QDragEnterEvent *event); @@ -83,17 +76,7 @@ class MainWindow : public QMainWindow void onSubwindowModified(); void onCurrentProfileChanged(); - void checkForUpdates(); - void checkForFirmwareUpdate(); - - void checkForCompanionUpdateFinished(QNetworkReply * reply); - void checkForFirmwareUpdateFinished(QNetworkReply * reply); - void displayWarnings(); - void doAutoUpdates(); - void doUpdates(); - void updateDownloaded(); - void firmwareDownloadAccepted(); void newFile(); void openFile(); void save(); @@ -118,7 +101,6 @@ class MainWindow : public QMainWindow void about(); void compare(); void appPrefs(); - void fwPrefs(); void updateMenus(); void createProfile(); void copyProfile(); @@ -128,10 +110,9 @@ class MainWindow : public QMainWindow void importSettings(); void autoClose(); void chooseProfile(); + void manualUpdates(); + void autoUpdates(); - void openUpdatesWaitDialog(); - void closeUpdatesWaitDialog(); - void onUpdatesError(const QString & err); void openFile(const QString & fileName, bool updateLastUsedDir = false); private: @@ -165,11 +146,7 @@ class MainWindow : public QMainWindow QMdiArea *mdiArea; QString installer_fileName; - DownloadDialog * downloadDialog_forWait; - unsigned int checkForUpdatesState; - QString firmwareVersionString; - - QNetworkAccessManager *networkManager; + UpdateFactories *updateFactories; QVector recentFileActs; QVector profileActs; @@ -198,7 +175,7 @@ class MainWindow : public QMainWindow QAction *recentFilesAct; QAction *exitAct; QAction *appPrefsAct; - QAction *fwPrefsAct; + QAction *updatesAct; QAction *checkForUpdatesAct; QAction *sdsyncAct; QAction *changelogAct; @@ -226,5 +203,3 @@ class MainWindow : public QMainWindow QAction *actCascadeWindows; QAction *actCloseAllWindows; }; - -#endif // _MAINWINDOW_H_ diff --git a/companion/src/progresswidget.h b/companion/src/progresswidget.h index f50933033..85852b8f5 100644 --- a/companion/src/progresswidget.h +++ b/companion/src/progresswidget.h @@ -18,15 +18,10 @@ * GNU General Public License for more details. */ -#ifndef _PROGRESSWIDGET_H_ -#define _PROGRESSWIDGET_H_ +#pragma once #include -#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0)) - #define QtInfoMsg QtMsgType(4) -#endif - namespace Ui { class ProgressWidget; } @@ -71,5 +66,3 @@ class ProgressWidget : public QWidget bool m_forceOpen; bool m_hasDetails; }; - -#endif // _PROGRESSWIDGET_H_ diff --git a/companion/src/progresswidget.ui b/companion/src/progresswidget.ui index 8399257ae..460195956 100644 --- a/companion/src/progresswidget.ui +++ b/companion/src/progresswidget.ui @@ -49,6 +49,11 @@ 0 + + + 10 + + @@ -86,6 +91,11 @@ 0 + + + 10 + + 0 diff --git a/companion/src/releasenotesfirmwaredialog.cpp b/companion/src/releasenotesfirmwaredialog.cpp deleted file mode 100644 index 753599c52..000000000 --- a/companion/src/releasenotesfirmwaredialog.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) OpenTX - * - * Based on code named - * th9x - http://code.google.com/p/th9x - * er9x - http://code.google.com/p/er9x - * gruvin9x - http://code.google.com/p/gruvin9x - * - * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * 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. - */ - -#include "releasenotesfirmwaredialog.h" -#include "ui_htmldialog.h" -#include "helpers.h" - -ReleaseNotesFirmwareDialog::ReleaseNotesFirmwareDialog(QWidget * parent, const QString & rnurl): - QDialog(parent), - ui(new Ui::HtmlDialog) -{ - ui->setupUi(this); - - setWindowTitle(tr("OpenTX Release Notes")); - setWindowIcon(CompanionIcon("changelog.png")); - - manager = new QNetworkAccessManager(this); - connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(replyFinished(QNetworkReply *))); - QUrl url(rnurl); - QNetworkRequest request(url); - request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - manager->get(request); -} - -ReleaseNotesFirmwareDialog::~ReleaseNotesFirmwareDialog() -{ - delete ui; -} - -void ReleaseNotesFirmwareDialog::replyFinished(QNetworkReply * reply) -{ - ui->textEditor->setHtml(reply->readAll()); - ui->textEditor->setOpenExternalLinks(true); - reply->deleteLater(); -} diff --git a/companion/src/storage/CMakeLists.txt b/companion/src/storage/CMakeLists.txt index 874dfb5ce..bc8a9f184 100644 --- a/companion/src/storage/CMakeLists.txt +++ b/companion/src/storage/CMakeLists.txt @@ -16,6 +16,7 @@ set(storage_NAMES otx yaml crc + minizinterface ) set(storage_SRCS diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index fa7ec4470..3f99eb52f 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -289,6 +289,42 @@ void Profile::resetFwVariables() } +// ** ComponentData class******************** + +ComponentData::ComponentData() : CompStoreObj(), index(-1) +{ + qRegisterMetaTypeStreamOperators("ComponentData::ReleaseChannel"); + CompStoreObj::addObjectMapping(propertyGroup(), this); +} + +// The default copy operator can not be used since the index variable would be destroyed +ComponentData & ComponentData::operator= (const ComponentData & rhs) +{ + for (int i = metaObject()->propertyOffset(), e = metaObject()->propertyCount(); i < e; ++i) { + const QMetaProperty & prop = metaObject()->property(i); + if (!prop.isValid() || !prop.isWritable()) { + qWarning() << "Could not copy property" << QString(prop.name()) << "isValid:" << prop.isValid() << "isWritable:" << prop.isWritable(); + continue; + } + prop.write(this, prop.read(&rhs)); + } + return *this; +} + +bool ComponentData::existsOnDisk() +{ + return (m_settings.contains(settingsPath() % "name")); +} + +void ComponentData::clearRelease() +{ + release(CompStoreObj::propertyDefaultValue(this, "release").toString()); + id(CompStoreObj::propertyDefaultValue(this, "id").toInt()); + prerelease(CompStoreObj::propertyDefaultValue(this, "prerelease").toBool()); + date(CompStoreObj::propertyDefaultValue(this, "date").toString()); + version(CompStoreObj::propertyDefaultValue(this, "version").toString()); +} + // ** AppData class******************** AppData::AppData() : @@ -297,8 +333,8 @@ AppData::AppData() : { QMetaType::registerComparators(); qRegisterMetaTypeStreamOperators("SimulatorOptions"); - qRegisterMetaTypeStreamOperators("AppData::DownloadBranchType"); qRegisterMetaTypeStreamOperators("AppData::NewModelAction"); + qRegisterMetaTypeStreamOperators("AppData::UpdateCheckFreq"); CompStoreObj::addObjectMapping(propertyGroup(), this); @@ -315,6 +351,10 @@ AppData::AppData() : // Configure the joysticks for (int i=0; i AppData::getActiveProfiles() const return active; } +int AppData::getComponentIndex(QString name) const +{ + for (int i=0; i -1 && index < MAX_COMPONENTS) + return component[index]; + return component[0]; +} + +const ComponentData & AppData::getComponent(int index) const +{ + if (index > -1 && index < MAX_COMPONENTS) + return component[index]; + return component[0]; +} + +void AppData::resetUpdatesSettings() +{ + g.updateCheckFreqReset(); + g.downloadDirReset(); + g.decompressDirReset(); + g.decompressUseDwnldReset(); + g.updateDirReset(); + g.updateUseSDReset(); +} diff --git a/companion/src/storage/appdata.h b/companion/src/storage/appdata.h index c383be2e5..0d5db23cc 100644 --- a/companion/src/storage/appdata.h +++ b/companion/src/storage/appdata.h @@ -25,8 +25,7 @@ @{ */ -#ifndef _APPDATA_H_ -#define _APPDATA_H_ +#pragma once #include "constants.h" #include "macros.h" @@ -59,14 +58,9 @@ #define CPN_SETTINGS_INI_FILE QString(PRODUCT % " " % QCoreApplication::translate("Companion", "settings") % " %1.ini") #define CPN_SETTINGS_INI_PATH QString(CPN_SETTINGS_BACKUP_DIR % "/" % CPN_SETTINGS_INI_FILE) -#define CPN_URL_DOWNLOAD "https://downloads.edgetx.org/" -#define CPN_URL_DOWNLOAD_CUR_VERS CPN_URL_DOWNLOAD QT_STRINGIFY(VERSION_MAJOR) "." QT_STRINGIFY(VERSION_MINOR) "/" -#define CPN_URL_DOWNLOAD_CUR_REL CPN_URL_DOWNLOAD_CUR_VERS "release/" -#define CPN_URL_DOWNLOAD_CUR_RC CPN_URL_DOWNLOAD_CUR_VERS "rc/" -#define CPN_URL_DOWNLOAD_CUR_UNST CPN_URL_DOWNLOAD_CUR_VERS "nightlies/" - #define MAX_PROFILES 20 #define MAX_JOYSTICKS 8 +#define MAX_COMPONENTS 10 // It important that these function names are consistent everywhere. #define PROP_FSIG_INIT_IMPL _init() @@ -427,6 +421,51 @@ class Profile: public CompStoreObj } }; +//! \brief ComponentData class stores properties related to each updateable component. +class ComponentData: public CompStoreObj +{ + Q_OBJECT + public: + ComponentData & operator=(const ComponentData & rhs); + + enum ReleaseChannel { + RELEASE_CHANNEL_STABLE, + RELEASE_CHANNEL_PRERELEASE, + RELEASE_CHANNEL_NIGHTLY + }; + Q_ENUM(ReleaseChannel) + + void clearRelease(); + static QStringList releaseChannelsList() { return { tr("Releases"), tr("Pre-release"), tr("Nightly") } ; } + + inline ReleaseChannel boundedReleaseChannel() const { + return qBound(RELEASE_CHANNEL_STABLE, releaseChannel(), RELEASE_CHANNEL_NIGHTLY); + } + + public slots: + bool existsOnDisk(); + + protected: + explicit ComponentData(); + void setIndex(int idx) { index = idx; } + inline QString propertyGroup() const override { return QStringLiteral("Components"); } + inline QString settingsPath() const override { return QString("%1/component%2/").arg(propertyGroup()).arg(index); } + friend class AppData; + + private: + PROPERTYSTR (name) + PROPERTY (bool, checkForUpdate, false) + PROPERTY (ReleaseChannel, releaseChannel, RELEASE_CHANNEL_STABLE) + PROPERTYSTRD( release, "unknown") + PROPERTY (int, id, 0) + PROPERTY (bool, prerelease, false) + PROPERTYSTRD( version, "0") + PROPERTYSTRD( date, "") + + int index; + + CREATE_ENUM_FRIEND_STREAM_OPS(ComponentData::ReleaseChannel) +}; /*! \brief The AppData class acts as the main interface for all application settings. @@ -441,13 +480,6 @@ class AppData: public CompStoreObj { Q_OBJECT public: - enum DownloadBranchType { - BRANCH_RELEASE_STABLE, - BRANCH_RC_TESTING, - BRANCH_NIGHTLY_UNSTABLE - }; - Q_ENUM(DownloadBranchType) - enum NewModelAction { MODEL_ACT_NONE, MODEL_ACT_WIZARD, @@ -457,13 +489,24 @@ class AppData: public CompStoreObj }; Q_ENUM(NewModelAction) + enum UpdateCheckFreq { + UPDATE_CHECK_MANUAL, + UPDATE_CHECK_STARTUP, + UPDATE_CHECK_DAILY, + UPDATE_CHECK_WEEKLY, + UPDATE_CHECK_MONTHLY + }; + Q_ENUM(UpdateCheckFreq) + static QStringList newModelActionsList() { return { tr("None"), tr("Wizard"), tr("Editor"), tr("Template"), tr("Prompt") } ; } + static QStringList updateCheckFreqsList() { return { tr("Manually"), tr("Startup"), tr("Daily"), tr("Weekly"), tr("Monthly") } ; } explicit AppData(); void init() override; void initAll(); void resetAllSettings(); void storeAllSettings(); + void resetUpdatesSettings(); inline bool isFirstUse() const { return firstUse; } inline QString previousVersion() const { return upgradeFromVersion; } @@ -487,6 +530,13 @@ class AppData: public CompStoreObj //! List of all active profiles mapped by index. QMap getActiveProfiles() const; + //! Get a modifiable (non-const) index to ComponentData for \a name. Returns -1 if \a name not found. + int getComponentIndex(QString name) const; + //! Get a modifiable (non-const) reference to the ComponentData at \a index. Returns component[0] if \a index is invalid. + ComponentData & getComponent(int index); + //! Get a non-modifiable (const) reference to the ComponentData at \a index. Returns component[0] if \a index is invalid. + const ComponentData & getComponent(int index) const; + //! Find the set of settings from the last previous version installed, if any. Used at virgin startup to offer import option. bool findPreviousVersionSettings(QString * version) const; //! Converts any old/refactored settings to new ones and removes anything stale if \a removeUnused is true. This is only important when @@ -500,25 +550,10 @@ class AppData: public CompStoreObj bool exportSettings(QSettings * toSettings, bool clearDestination = true); bool exportSettingsToFile(const QString & expFile, QString & resultMsg); - inline DownloadBranchType boundedOpenTxBranch() const { -#if defined(ALLOW_NIGHTLY_BUILDS) - return qBound(BRANCH_RELEASE_STABLE, OpenTxBranch(), BRANCH_NIGHTLY_UNSTABLE); -#else - return qBound(BRANCH_RELEASE_STABLE, OpenTxBranch(), BRANCH_RC_TESTING); -#endif - } - - inline QString openTxCurrentDownloadBranchUrl() const { return openTxDownloadBranchUrl(boundedOpenTxBranch()); } - - static const QString openTxDownloadBranchUrl(DownloadBranchType type) - { - static const QStringList urlList({ CPN_URL_DOWNLOAD_CUR_REL, CPN_URL_DOWNLOAD_CUR_RC, CPN_URL_DOWNLOAD_CUR_UNST }); - return urlList.value(type, CPN_URL_DOWNLOAD_CUR_VERS); - } - Profile profile[MAX_PROFILES]; JStickData joystick[MAX_JOYSTICKS]; FwRevision fwRev; + ComponentData component[MAX_COMPONENTS]; signals: void currentProfileChanged(); @@ -541,47 +576,56 @@ class AppData: public CompStoreObj PROPERTYQBA (mdiWinState) PROPERTYQBA (compareWinGeo) - PROPERTYSTR3(armMcu, "arm_mcu", QStringLiteral("at91sam3s4-9x")) + PROPERTYSTR3(armMcu, "arm_mcu", QStringLiteral("at91sam3s4-9x")) PROPERTYSTR2(avrArguments, "avr_arguments") PROPERTYSTR2(avrPort, "avr_port") PROPERTYSTR2(avrdudeLocation, "avrdudeLocation") - PROPERTYSTR3(dfuArguments, "dfu_arguments", QStringLiteral("-a 0")) + PROPERTYSTR3(dfuArguments, "dfu_arguments", QStringLiteral("-a 0")) PROPERTYSTR2(dfuLocation, "dfu_location") PROPERTYSTR2(sambaLocation, "samba_location") - PROPERTYSTR3(sambaPort, "samba_port", QStringLiteral("\\USBserial\\COM23")) - PROPERTYSTR2(backupDir, "backupPath") - PROPERTYSTR2(eepromDir, "lastDir") - PROPERTYSTR2(flashDir, "lastFlashDir") - PROPERTYSTR2(imagesDir, "lastImagesDir") - PROPERTYSTR2(logDir, "lastLogDir") + PROPERTYSTR3(sambaPort, "samba_port", QStringLiteral("\\USBserial\\COM23")) + PROPERTYSTR2(libDir, "libraryPath") - PROPERTYSTR2(snapshotDir, "snapshotpath") - PROPERTYSTR2(updatesDir, "lastUpdatesDir") + + PROPERTYSTR3(backupDir, "backupPath", CPN_DOCUMENTS_LOCATION % "/backups") + PROPERTYSTR3(eepromDir, "lastDir", CPN_DOCUMENTS_LOCATION % "/eeproms") + PROPERTYSTR3(flashDir, "lastFlashDir", CPN_DOCUMENTS_LOCATION % "/flash") + PROPERTYSTR3(imagesDir, "lastImagesDir", CPN_DOCUMENTS_LOCATION % "/images") + PROPERTYSTR3(logDir, "lastLogDir", CPN_DOCUMENTS_LOCATION % "/debuglogs") + PROPERTYSTRD(appLogsDir, CPN_DOCUMENTS_LOCATION % "/debuglogs") + PROPERTYSTR3(snapshotDir, "snapshotpath", CPN_DOCUMENTS_LOCATION % "/snapshots") + PROPERTYSTRD(templatesDir, CPN_DOCUMENTS_LOCATION % "/templates") + PROPERTYSTRD(downloadDir, CPN_DOCUMENTS_LOCATION % "/downloads") + PROPERTYSTRD(decompressDir, CPN_DOCUMENTS_LOCATION % "/decompress") + PROPERTY (bool, decompressUseDwnld, true) + PROPERTYSTRD(updateDir, CPN_DOCUMENTS_LOCATION % "/updates") + PROPERTY (bool, updateUseSD, true) + PROPERTY (int, updateLogLevel, 0) PROPERTYSTR (locale) PROPERTYSTR (gePath) PROPERTYSTRD(mcu, QStringLiteral("m64")) PROPERTYSTRD(programmer, QStringLiteral("usbasp")) - PROPERTYSTRD(appLogsDir, CPN_DOCUMENTS_LOCATION % "/DebugLogs") - PROPERTY(DownloadBranchType, OpenTxBranch, BRANCH_RELEASE_STABLE) - PROPERTY(NewModelAction, newModelAction, MODEL_ACT_WIZARD) + PROPERTY(NewModelAction, newModelAction, MODEL_ACT_WIZARD) - PROPERTY4(int, embedSplashes, "embedded_splashes", 0) - PROPERTY4(int, fwServerFails, "fwserver", 0) - PROPERTY4(int, iconSize, "icon_size", 2) - PROPERTY4(int, jsCtrl, "js_ctrl", 0) - PROPERTY4(int, historySize, "history_size", 10) - PROPERTY (int, generalEditTab, 0) - PROPERTY (int, theme, 1) - PROPERTY (int, warningId, 0) + PROPERTY4(int, embedSplashes, "embedded_splashes", 0) + PROPERTY4(int, fwServerFails, "fwserver", 0) + PROPERTY4(int, iconSize, "icon_size", 2) + PROPERTY4(int, jsCtrl, "js_ctrl", 0) + PROPERTY4(int, historySize, "history_size", 10) + PROPERTY (int, generalEditTab, 0) + PROPERTY (int, theme, 1) + PROPERTY (int, warningId, 0) - PROPERTY4(bool, jsSupport, "js_support", false) - PROPERTY4(bool, showSplash, "show_splash", true) - PROPERTY4(bool, snapToClpbrd, "snapshot_to_clipboard", false) - PROPERTY4(bool, autoCheckApp, "startup_check_companion", false) - PROPERTY4(bool, autoCheckFw, "startup_check_fw", false) - PROPERTY4(bool, promptProfile, "startup_prompt_profile", false) + PROPERTY4(bool, jsSupport, "js_support", false) + PROPERTY4(bool, showSplash, "show_splash", true) + PROPERTY4(bool, snapToClpbrd, "snapshot_to_clipboard", false) + + PROPERTY(UpdateCheckFreq, updateCheckFreq, UPDATE_CHECK_MANUAL) + PROPERTYSTR(lastUpdateCheck) + + PROPERTY4(bool, promptProfile, "startup_prompt_profile", false) PROPERTY(bool, enableBackup, false) PROPERTY(bool, backupOnFlash, true) @@ -605,11 +649,8 @@ class AppData: public CompStoreObj bool firstUse; QString upgradeFromVersion; - CREATE_ENUM_FRIEND_STREAM_OPS(AppData::DownloadBranchType) CREATE_ENUM_FRIEND_STREAM_OPS(AppData::NewModelAction) + CREATE_ENUM_FRIEND_STREAM_OPS(AppData::UpdateCheckFreq) }; extern AppData g; - -//! @} -#endif // _APPDATA_H_ diff --git a/companion/src/storage/minizinterface.cpp b/companion/src/storage/minizinterface.cpp new file mode 100644 index 000000000..b41937ca0 --- /dev/null +++ b/companion/src/storage/minizinterface.cpp @@ -0,0 +1,292 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "minizinterface.h" +#include "progresswidget.h" + +#include +#include +#include +#include +#include + +MinizInterface::MinizInterface(ProgressWidget * progress, const ProgressCalcMethod progressMethod, const int & logLevel) : + progress(progress), + progressMethod(progressMethod), + progressValue(0), + logLevel(logLevel), + path(""), + archiveFile("") +{ + reportProgress(tr("Progress calculation method: file %1").arg(progressMethod == PCM_COUNT ? tr("count") : tr("size")), QtDebugMsg); +} + +MinizInterface::~MinizInterface() +{ + +} + +bool MinizInterface::zipPathToFile(const QString & sourcePath, const QString & archiveFile, bool append) +{ + if (progress) + progress->setInfo(tr("Compressing %1 to %2 with append %3").arg(sourcePath, archiveFile, append ? "true" : "false")); + else + qDebug() << QString("Compressing %1 to %2 with append %3").arg(sourcePath, archiveFile, append ? "true" : "false"); + + reportProgress(tr("Miniz version: %1").arg(MZ_VERSION), QtDebugMsg); + + progressValue = 0; + path = sourcePath; + this->archiveFile = archiveFile; + mz_zip_zero_struct(&zip_archive); + + QFileInfo afi(archiveFile); + + if (afi.exists() && !append) + reportProgress(tr("Existing archive will be overwritten"), QtCriticalMsg); + + if (afi.exists() && append) { + if (!mz_zip_reader_init_file(&zip_archive, qPrintable(archiveFile), 0)) { + reportProgress(tr("Unable to open existing archive"), QtFatalMsg); + return false; + } + else + reportProgress(tr("Existing archive opened"), QtDebugMsg); + + if (!mz_zip_writer_init_from_reader(&zip_archive, qPrintable(archiveFile))) { + reportProgress(tr("Unable to write to existing archive"), QtFatalMsg); + return false; + } + else + reportProgress(tr("Existing archive initialised"), QtDebugMsg); + } + else { + if (!mz_zip_writer_init_file(&zip_archive, qPrintable(archiveFile), 0)) { + reportProgress(tr("Failure to initialise archive"), QtFatalMsg); + return false; + } + else + reportProgress(tr("Archive initialised"), QtDebugMsg); + } + + QFileInfo pfi(path); + + int fileCount = 0; + + if (pfi.isDir()) { + if (progressMethod == PCM_COUNT) { + reportProgress(tr("Calculating number of items to archive"), QtDebugMsg); + + QDirIterator itcnt(path, QDirIterator::Subdirectories); + + while (itcnt.hasNext()) { + fileCount++; + } + } + + if (progress) { + if (progressMethod == PCM_COUNT) + progress->setMaximum(fileCount); + else + progress->setMaximum(pfi.size()); + } + + fileCount = 0; + QDirIterator it(path, QDirIterator::Subdirectories); + + while (it.hasNext()) { + it.next(); + if (!addFileToArchive(it.filePath())) + return false; + } + } + else { + fileCount = 1; + + if (progress) { + if (progressMethod == PCM_COUNT) + progress->setMaximum(fileCount); + else + progress->setMaximum(pfi.size()); + } + + if (!addFileToArchive(path)) + return false; + } + + if (!mz_zip_writer_finalize_archive(&zip_archive)) { + mz_zip_writer_end(&zip_archive); + reportProgress(tr("Failure to finalise archive"), QtFatalMsg); + return false; + } + + mz_zip_writer_end(&zip_archive); + + if (progress) + progress->setValue(progress->maximum()); + + reportProgress(tr("Compress complete")); + + return true; +} + +bool MinizInterface::addFileToArchive(const QString & path) +{ + if (!mz_zip_writer_add_file(&zip_archive, qPrintable(archiveFile), qPrintable(path), NULL, 0, MZ_DEFAULT_COMPRESSION)) { + mz_zip_writer_end(&zip_archive); + reportProgress(tr("Failure to add %1").arg(path), QtFatalMsg); + return false; + } + + if (progressMethod == PCM_COUNT) + ++progressValue; + else + progressValue += QFileInfo(path).size(); + + if (progress) + progress->setValue(progressValue); + + reportProgress(tr("Added file: %1").arg(path), QtDebugMsg); + + return true; +} + +bool MinizInterface::unzipArchiveToPath(const QString & archiveFile, const QString & destinationPath) +{ + if (progress) + progress->setInfo(tr("Decompressing %1 to %2").arg(archiveFile, destinationPath)); + else + qDebug() << QString("Decompressing %1 to %2").arg(archiveFile, destinationPath); + + reportProgress(tr("Miniz version: %1").arg(MZ_VERSION), QtDebugMsg); + + progressValue = 0; + path = destinationPath; + this->archiveFile = archiveFile; + mz_zip_zero_struct(&zip_archive); + + if (!mz_zip_reader_init_file(&zip_archive, qPrintable(archiveFile), 0)) { + reportProgress(tr("File does not appear to be a compressed archive"), QtFatalMsg); + return false; + } + + int fileCount = (int)mz_zip_reader_get_num_files(&zip_archive); + + if (fileCount == 0) { + mz_zip_reader_end(&zip_archive); + reportProgress(tr("Compressed archive does not contain any files"), QtFatalMsg); + return false; + } + + if (progress) { + if (progressMethod == PCM_COUNT) + progress->setMaximum(fileCount); + else + progress->setMaximum(QFileInfo(archiveFile).size()); + } + + mz_zip_archive_file_stat file_stat; + + if (!mz_zip_reader_file_stat(&zip_archive, 0, &file_stat)) { + mz_zip_reader_end(&zip_archive); + reportProgress(tr("File status error"), QtFatalMsg); + return false; + } + + for (int i = 0; i < fileCount; i++) { + if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) { + mz_zip_reader_end(&zip_archive); + reportProgress(tr("Unable to obtain file status for index: %1").arg(i), QtFatalMsg); + return false; + } + + QString destPath = path % "/" % QString(file_stat.m_filename); + + if (mz_zip_reader_is_file_a_directory(&zip_archive, i)) { + if (!createDirectory(destPath)) { + mz_zip_reader_end(&zip_archive); + return false; + } + } + else { + QFileInfo dfi(destPath); + if (!createDirectory(dfi.absolutePath())) { + mz_zip_reader_end(&zip_archive); + return false; + } + + if (!mz_zip_reader_extract_to_file(&zip_archive, i, qPrintable(destPath), 0)) { + mz_zip_reader_end(&zip_archive); + reportProgress(tr("Failed to extract %1 to %2").arg(dfi.fileName(), dfi.absolutePath()), QtFatalMsg); + return false; + } + else { + if (dfi.size() != (qint64)file_stat.m_uncomp_size) { + reportProgress(tr("File %1 extracted size %2 does not match original %3").arg(file_stat.m_filename).arg(dfi.size()).arg((qint64)file_stat.m_uncomp_size), QtFatalMsg); + // return false; disable until testing complete as there maybe some adjustment factor or even OS strangeness + } + } + + if (progressMethod == PCM_COUNT) + ++progressValue; + else + progressValue += file_stat.m_comp_size; + + if (progress) + progress->setValue(progressValue); + + reportProgress(tr("Extracted file: %1").arg(file_stat.m_filename), QtDebugMsg); + } + } + + mz_zip_reader_end(&zip_archive); + + if (progress) + progress->setValue(progress->maximum()); + + reportProgress(tr("Decompress complete")); + + return true; +} + +bool MinizInterface::createDirectory(const QString & path) +{ + if (!QDir(path).exists()) { + if (!QDir().mkpath(path)) { + reportProgress(tr("Failed to create directory: %1").arg(path), QtFatalMsg); + return false; + } + else + reportProgress(tr("Created directory: %1").arg(path)); + } + + return true; +} + +void MinizInterface::reportProgress(const QString & text, const int & type, bool richText) +{ + if (type >= logLevel) { + if (progress) + progress->addMessage(text, type); + else + qDebug() << text; + } +} diff --git a/companion/src/storage/minizinterface.h b/companion/src/storage/minizinterface.h new file mode 100644 index 000000000..2f24999bd --- /dev/null +++ b/companion/src/storage/minizinterface.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include "miniz.h" + +#include +#include + +class ProgressWidget; + +class MinizInterface +{ + Q_DECLARE_TR_FUNCTIONS(MinizInterface) + + public: + enum ProgressCalcMethod { + PCM_COUNT, + PCM_SIZE + }; + + MinizInterface(ProgressWidget * progress = nullptr, const ProgressCalcMethod progressMethod = PCM_COUNT, const int & logLevel = QtWarningMsg); + ~MinizInterface(); + + bool zipPathToFile(const QString & sourcePath, const QString & archiveFile, bool append = true); + bool unzipArchiveToPath(const QString & archiveFile, const QString & destinationPath); + + private: + ProgressWidget *progress; + ProgressCalcMethod progressMethod; + qint64 progressValue; + int logLevel; + QString path; + QString archiveFile; + + mz_zip_archive zip_archive; + + bool addFileToArchive(const QString & path); + bool createDirectory(const QString & path); + void reportProgress(const QString & text, const int & type = QtInfoMsg, bool richText = false); +}; diff --git a/companion/src/themes/classic/16/fwpreferences.png b/companion/src/themes/classic/16/download.png similarity index 100% rename from companion/src/themes/classic/16/fwpreferences.png rename to companion/src/themes/classic/16/download.png diff --git a/companion/src/themes/classic/24/fwpreferences.png b/companion/src/themes/classic/24/download.png similarity index 100% rename from companion/src/themes/classic/24/fwpreferences.png rename to companion/src/themes/classic/24/download.png diff --git a/companion/src/themes/classic/32/fwpreferences.png b/companion/src/themes/classic/32/download.png similarity index 100% rename from companion/src/themes/classic/32/fwpreferences.png rename to companion/src/themes/classic/32/download.png diff --git a/companion/src/themes/classic/48/fwpreferences.png b/companion/src/themes/classic/48/download.png similarity index 100% rename from companion/src/themes/classic/48/fwpreferences.png rename to companion/src/themes/classic/48/download.png diff --git a/companion/src/themes/monoblue/16/fwpreferences.png b/companion/src/themes/monoblue/16/download.png similarity index 100% rename from companion/src/themes/monoblue/16/fwpreferences.png rename to companion/src/themes/monoblue/16/download.png diff --git a/companion/src/themes/monoblue/24/fwpreferences.png b/companion/src/themes/monoblue/24/download.png similarity index 100% rename from companion/src/themes/monoblue/24/fwpreferences.png rename to companion/src/themes/monoblue/24/download.png diff --git a/companion/src/themes/monoblue/32/fwpreferences.png b/companion/src/themes/monoblue/32/download.png similarity index 100% rename from companion/src/themes/monoblue/32/fwpreferences.png rename to companion/src/themes/monoblue/32/download.png diff --git a/companion/src/themes/monoblue/48/fwpreferences.png b/companion/src/themes/monoblue/48/download.png similarity index 100% rename from companion/src/themes/monoblue/48/fwpreferences.png rename to companion/src/themes/monoblue/48/download.png diff --git a/companion/src/themes/monochrome/16/fwpreferences.png b/companion/src/themes/monochrome/16/download.png similarity index 100% rename from companion/src/themes/monochrome/16/fwpreferences.png rename to companion/src/themes/monochrome/16/download.png diff --git a/companion/src/themes/monochrome/24/fwpreferences.png b/companion/src/themes/monochrome/24/download.png similarity index 100% rename from companion/src/themes/monochrome/24/fwpreferences.png rename to companion/src/themes/monochrome/24/download.png diff --git a/companion/src/themes/monochrome/32/fwpreferences.png b/companion/src/themes/monochrome/32/download.png similarity index 100% rename from companion/src/themes/monochrome/32/fwpreferences.png rename to companion/src/themes/monochrome/32/download.png diff --git a/companion/src/themes/monochrome/48/fwpreferences.png b/companion/src/themes/monochrome/48/download.png similarity index 100% rename from companion/src/themes/monochrome/48/fwpreferences.png rename to companion/src/themes/monochrome/48/download.png diff --git a/companion/src/themes/monowhite/16/fwpreferences.png b/companion/src/themes/monowhite/16/download.png similarity index 100% rename from companion/src/themes/monowhite/16/fwpreferences.png rename to companion/src/themes/monowhite/16/download.png diff --git a/companion/src/themes/monowhite/24/fwpreferences.png b/companion/src/themes/monowhite/24/download.png similarity index 100% rename from companion/src/themes/monowhite/24/fwpreferences.png rename to companion/src/themes/monowhite/24/download.png diff --git a/companion/src/themes/monowhite/32/fwpreferences.png b/companion/src/themes/monowhite/32/download.png similarity index 100% rename from companion/src/themes/monowhite/32/fwpreferences.png rename to companion/src/themes/monowhite/32/download.png diff --git a/companion/src/themes/monowhite/48/fwpreferences.png b/companion/src/themes/monowhite/48/download.png similarity index 100% rename from companion/src/themes/monowhite/48/fwpreferences.png rename to companion/src/themes/monowhite/48/download.png diff --git a/companion/src/themes/yerico/16/fwpreferences.png b/companion/src/themes/yerico/16/download.png similarity index 100% rename from companion/src/themes/yerico/16/fwpreferences.png rename to companion/src/themes/yerico/16/download.png diff --git a/companion/src/themes/yerico/24/fwpreferences.png b/companion/src/themes/yerico/24/download.png similarity index 100% rename from companion/src/themes/yerico/24/fwpreferences.png rename to companion/src/themes/yerico/24/download.png diff --git a/companion/src/themes/yerico/32/fwpreferences.png b/companion/src/themes/yerico/32/download.png similarity index 100% rename from companion/src/themes/yerico/32/fwpreferences.png rename to companion/src/themes/yerico/32/download.png diff --git a/companion/src/themes/yerico/48/fwpreferences.png b/companion/src/themes/yerico/48/download.png similarity index 100% rename from companion/src/themes/yerico/48/fwpreferences.png rename to companion/src/themes/yerico/48/download.png diff --git a/companion/src/updates/CMakeLists.txt b/companion/src/updates/CMakeLists.txt new file mode 100644 index 000000000..3277a08e3 --- /dev/null +++ b/companion/src/updates/CMakeLists.txt @@ -0,0 +1,39 @@ +include_directories(${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + +set(updates_NAMES + updates + updatesdialog + updateoptionsdialog + repomodels + updateinterface + updatefirmware + updatecompanion + updatesdcard + updatesounds + updatethemes + updatemultiprotocol +) + +foreach(name ${updates_NAMES}) + set(updates_SRCS ${updates_SRCS} ${name}.cpp) + set(updates_HDRS ${updates_HDRS} ${name}.h) +endforeach() + +set(updates_UIS + updatesdialog.ui + updateoptionsdialog.ui +) + +set(updates_QTS + updates.h + updatesdialog.h + updateoptionsdialog.h + repomodels.h + updateinterface.h +) + +qt5_wrap_ui(updates_SRCS ${updates_UIS}) +qt5_wrap_cpp(updates_SRCS ${updates_QTS}) + +add_library(updates ${updates_SRCS} ${updates_HDRS}) +target_link_libraries(updates PRIVATE ${CPN_COMMON_LIB}) diff --git a/companion/src/downloaddialog.cpp b/companion/src/updates/downloaddialog.cpp similarity index 75% rename from companion/src/downloaddialog.cpp rename to companion/src/updates/downloaddialog.cpp index 5b9b2dfa0..db691f98a 100644 --- a/companion/src/downloaddialog.cpp +++ b/companion/src/updates/downloaddialog.cpp @@ -1,7 +1,8 @@ /* - * Copyright (C) OpenTX + * Copyright (C) EdgeTX * * Based on code named + * opentx - https://github.com/opentx/opentx * th9x - http://code.google.com/p/th9x * er9x - http://code.google.com/p/er9x * gruvin9x - http://code.google.com/p/gruvin9x @@ -25,7 +26,7 @@ #include -DownloadDialog::DownloadDialog(QWidget *parent, QString src, QString tgt): +DownloadDialog::DownloadDialog(QWidget *parent, QString src, QString tgt, QString contentType, QString title): QDialog(parent), ui(new Ui::DownloadDialog), reply(nullptr), @@ -33,25 +34,33 @@ DownloadDialog::DownloadDialog(QWidget *parent, QString src, QString tgt): aborted(false) { ui->setupUi(this); - setWindowIcon(CompanionIcon("fwpreferences.png")); - ui->progressBar->setValue(1); + setWindowIcon(CompanionIcon("download.png")); + ui->progressBar->setValue(0); ui->progressBar->setMinimum(0); - ui->progressBar->setMaximum(0); + ui->progressBar->setMaximum(100); if (tgt.isEmpty()) { setWindowTitle(src); return; // just show wait dialog. } + setWindowTitle(windowTitle() % title); + file = new QFile(tgt); if (!file->open(QIODevice::WriteOnly)) { - QMessageBox::critical(this, CPN_STR_APP_NAME, - tr("Unable to save the file %1: %2.") - .arg(tgt).arg(file->errorString())); + QMessageBox::critical(this, CPN_STR_APP_NAME, tr("Unable to open the download file %1 for writing.\nError: %2").arg(tgt).arg(file->errorString())); QTimer::singleShot(0, this, SLOT(fileError())); } else { - reply = qnam.get(QNetworkRequest(QUrl(src))); + url.setUrl(src); + qDebug() << "url:" << url.url(); + request.setUrl(url); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferNetwork); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + + request.setRawHeader(QByteArray("Accept"), contentType.toUtf8()); + + reply = qnam.get(request); connect(reply, SIGNAL(finished()), this, SLOT(httpFinished())); connect(reply, SIGNAL(readyRead()), this, SLOT(httpReadyRead())); connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(updateDataReadProgress(qint64, qint64))); @@ -81,6 +90,7 @@ void DownloadDialog::httpFinished() file->close(); const bool ok = !(reply->error() || aborted); + if (!ok) { file->remove(); if (!aborted) @@ -117,15 +127,3 @@ void DownloadDialog::fileError() file = nullptr; reject(); } - -#if 0 -void DownloadDialog::closeEvent( QCloseEvent * event) -{ - // Delay closing 2 seconds to avoid unpleasant flashing download dialogs - QTime closeTime= QTime::currentTime().addSecs(2); - while( QTime::currentTime() < closeTime ) - QCoreApplication::processEvents(QEventLoop::AllEvents, 100); - - event->accept(); -} -#endif diff --git a/companion/src/downloaddialog.h b/companion/src/updates/downloaddialog.h similarity index 87% rename from companion/src/downloaddialog.h rename to companion/src/updates/downloaddialog.h index b2a5bb5ad..71b2c4140 100644 --- a/companion/src/downloaddialog.h +++ b/companion/src/updates/downloaddialog.h @@ -1,7 +1,8 @@ /* - * Copyright (C) OpenTX + * Copyright (C) EdgeTX * * Based on code named + * opentx - https://github.com/opentx/opentx * th9x - http://code.google.com/p/th9x * er9x - http://code.google.com/p/er9x * gruvin9x - http://code.google.com/p/gruvin9x @@ -18,8 +19,7 @@ * GNU General Public License for more details. */ -#ifndef _DOWNLOADDIALOG_H_ -#define _DOWNLOADDIALOG_H_ +#pragma once #include #include @@ -36,34 +36,26 @@ class DownloadDialog : public QDialog Q_OBJECT public: - explicit DownloadDialog(QWidget *parent = 0, QString src = "", QString tgt = ""); + explicit DownloadDialog(QWidget *parent = 0, QString src = "", QString tgt = "", QString contentType = "", QString title = ""); ~DownloadDialog(); public slots: - virtual void reject() override; private slots: - void fileError(); - void httpFinished(); - void httpReadyRead(); - void updateDataReadProgress(qint64 bytesRead, qint64 totalBytes); private: -#if 0 - void closeEvent(QCloseEvent * event); -#endif Ui::DownloadDialog *ui; QNetworkAccessManager qnam; QNetworkReply *reply; QFile *file; bool aborted; + QNetworkRequest request; + QUrl url; }; - -#endif // _DOWNLOADDIALOG_H_ diff --git a/companion/src/downloaddialog.ui b/companion/src/updates/downloaddialog.ui similarity index 100% rename from companion/src/downloaddialog.ui rename to companion/src/updates/downloaddialog.ui diff --git a/companion/src/updates/repomodels.cpp b/companion/src/updates/repomodels.cpp new file mode 100644 index 000000000..5824d5c48 --- /dev/null +++ b/companion/src/updates/repomodels.cpp @@ -0,0 +1,456 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "repomodels.h" +#include "appdata.h" + +#include +#include + +QString metaDataTypeToString(MetaDataType val) +{ + switch ((int)val) { + case MDT_Release: + return "Release"; + case MDT_Releases: + return "Releases"; + case MDT_Asset: + return "Asset"; + case MDT_ReleaseAssets: + return "Release Assets"; + default: + return CPN_STR_UNKNOWN_ITEM; + } +} + +/* + ReleasesItemModel +*/ + +ReleasesItemModel::ReleasesItemModel() : + UpdatesItemModel("Releases"), + m_settingsIdx(-1), + m_nightlyName(""), + m_releaseChannel(-1) +{ + setSortRole(UpdatesItemModel::IMDR_Date); +} + +bool ReleasesItemModel::releaseChannelChanged() +{ + if (m_settingsIdx > -1 && m_releaseChannel != (int)g.component[m_settingsIdx].releaseChannel()) + return true; + else + return false; +} + +void ReleasesItemModel::parseMetaData(const int mdt, QJsonDocument * jsonDoc) +{ + json = jsonDoc; + + if (m_settingsIdx > -1) + m_releaseChannel = g.component[m_settingsIdx].releaseChannel(); + + switch (mdt) { + case MDT_Release: + parseRelease(); + break; + case MDT_Releases: + parseReleases(); + break; + } +} + +void ReleasesItemModel::parseRelease() +{ + clear(); + + if (json->isObject()) { + const QJsonObject &obj = json->object(); + parseJsonObject(obj); + } +} + +void ReleasesItemModel::parseReleases() +{ + clear(); + + if (json->isArray()) { + const QJsonArray &array = json->array(); + foreach (const QJsonValue &v, array) + { + if (v.isObject()) { + const QJsonObject &obj = v.toObject(); + parseJsonObject(obj); + } + } + } + + sort(0, Qt::DescendingOrder); +} + +void ReleasesItemModel::parseJsonObject(const QJsonObject & obj) +{ + QStandardItem * item = new QStandardItem(); + + QString tag = QString(); + bool prerelease = false; + + if (!obj.value("name").isUndefined()) { + item->setText(obj.value("name").toString()); + } + + if (!obj.value("id").isUndefined()) { + item->setData(obj.value("id").toInt(), UpdatesItemModel::IMDR_Id); + } + + if (!obj.value("tag_name").isUndefined()) { + tag = obj.value("tag_name").toString(); + item->setData(tag, UpdatesItemModel::IMDR_Tag); + } + + if (!obj.value("prerelease").isUndefined()) { + prerelease = obj.value("prerelease").toBool(); + item->setData(prerelease, UpdatesItemModel::IMDR_Prerelease); + } + + if (!obj.value("created_at").isUndefined()) { + item->setData(QDateTime::fromString(obj.value("created_at").toString(), Qt::ISODate), UpdatesItemModel::IMDR_Date); + } + + item->setData(isReleaseAvailable(tag, prerelease), UpdatesItemModel::IMDR_Available); + + appendRow(item); +} + +bool ReleasesItemModel::isReleaseAvailable(const QString tagname, const bool prerelease) +{ + if (prerelease) { + if (m_releaseChannel == ComponentData::RELEASE_CHANNEL_STABLE) { + //qDebug() << "Release channel stable" << tagname << "is pre-release" ; + return false; + } + else if (m_releaseChannel == ComponentData::RELEASE_CHANNEL_PRERELEASE && tagname.toLower() == m_nightlyName) { + //qDebug() << "Release channel pre-release" << tagname << "is nightly" ; + return false; + } + } + //qDebug() << "Release available:" << tagname; + return true; +} + +/* + AssetsItemModel +*/ + +AssetsItemModel::AssetsItemModel() : + UpdatesItemModel("Assets") +{ + +} + +void AssetsItemModel::parseMetaData(int mdt, QJsonDocument * jsonDoc) +{ + json = jsonDoc; + + switch (mdt) { + case MDT_Asset: + parseAsset(); + break; + case MDT_ReleaseAssets: + parseReleaseAssets(); + break; + } +} + +void AssetsItemModel::parseAsset() +{ + clear(); + + if (json->isObject()) { + const QJsonObject &obj = json->object(); + parseJsonObject(obj); + } +} + +void AssetsItemModel::parseReleaseAssets() +{ + clear(); + + if (json->isArray()) { + const QJsonArray &array = json->array(); + foreach (const QJsonValue &v, array) + { + if (v.isObject()) { + const QJsonObject &obj = v.toObject(); + parseJsonObject(obj); + } + } + } + + sort(0); +} + +void AssetsItemModel::parseJsonObject(const QJsonObject & obj) +{ + QStandardItem * item = new QStandardItem(); + + if (!obj.value("name").isUndefined()) { + item->setText(obj.value("name").toString()); + } + + if (!obj.value("id").isUndefined()) { + item->setData(obj.value("id").toInt(), UpdatesItemModel::IMDR_Id); + } + + if (!obj.value("content_type").isUndefined()) { + item->setData(obj.value("content_type").toString(), UpdatesItemModel::IMDR_Content); + } + + if (!obj.value("updated_at").isUndefined()) { + item->setData(QDateTime::fromString(obj.value("updated_at").toString(), Qt::ISODate), UpdatesItemModel::IMDR_Date); + } + + item->setData(true, UpdatesItemModel::IMDR_Available); + + appendRow(item); +} + +/* + UpdatesFilteredItemModel +*/ + +UpdatesFilteredItemModel::UpdatesFilteredItemModel(UpdatesItemModel * sourceModel, QString name) : + QSortFilterProxyModel(nullptr), + filterFlags(0), + m_name(name) +{ + filterText.setPattern(""); + filterText.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + setFilterRole(UpdatesItemModel::IMDR_Available); + setFilterKeyColumn(0); + setDynamicSortFilter(true); + setSourceModel(sourceModel); +} + +void UpdatesFilteredItemModel::setFilterFlags(const int flags) +{ + if (filterFlags != flags) { + filterFlags = flags; + filterText.setPattern(""); + invalidateFilter(); + } +} + +void UpdatesFilteredItemModel::setFilterPattern(const QString & pattern) +{ + if (filterText.pattern() != pattern) { + filterText.setPattern(pattern); + filterFlags = 0; + invalidateFilter(); + } +} + +bool UpdatesFilteredItemModel::filterAcceptsRow(int sourceRow, const QModelIndex & sourceParent) const +{ + const QModelIndex & srcIdx = sourceModel()->index(sourceRow, 0, sourceParent); + + if (!srcIdx.isValid() || !sourceModel()->data(srcIdx, filterRole()).toBool()) + return false; + + if (!filterFlags && filterText.pattern().isEmpty()) + return true; + + if (filterFlags) { + bool ok; + const int flags = sourceModel()->data(srcIdx, UpdatesItemModel::IMDR_Flags).toInt(&ok); + + return (ok && (!flags || (filterFlags & flags))); + } + else { + return sourceModel()->data(srcIdx, Qt::DisplayRole).toString().contains(filterText); + } +} + +void UpdatesFilteredItemModel::dumpContents() +{ + qDebug() << "Contents of" << m_name; + + for (int i = 0; i < rowCount(); ++i) { + qDebug() << "row:" << i + << "text:" << data(index(i, 0), Qt::DisplayRole).toString() + << "id:" << data(index(i, 0), UpdatesItemModel::IMDR_Id).toInt() + << "date:" << data(index(i, 0), UpdatesItemModel::IMDR_Date).toDateTime() + << "tag:" << data(index(i, 0), UpdatesItemModel::IMDR_Tag).toString() + << "avail:" << data(index(i, 0), UpdatesItemModel::IMDR_Available).toBool() + << "flags:" << data(index(i, 0), UpdatesItemModel::IMDR_Flags).toInt() + << "content:" << data(index(i, 0), UpdatesItemModel::IMDR_Content).toString() + << "prerelease:" << data(index(i, 0), UpdatesItemModel::IMDR_Prerelease).toBool(); + } +} + +QMap UpdatesFilteredItemModel::itemMetaData(const int id) +{ + const QModelIndexList idxlst = match(index(0, 0), UpdatesItemModel::IMDR_Id, QVariant(id), 1, Qt::MatchExactly); + + if (!idxlst.isEmpty()) + return itemData(idxlst.at(0)); + + return QMap(); +} + +QVariant UpdatesFilteredItemModel::metaDataValue(const int id, const int role) +{ + //qDebug() << "id:" << id << "role:"; + + const QModelIndexList idxlst = match(index(0, 0), UpdatesItemModel::IMDR_Id, QVariant(id), 1, Qt::MatchExactly); + + if (!idxlst.isEmpty()) + return data(idxlst.at(0), role); + + return QVariant(); +} + +bool UpdatesFilteredItemModel::setMetaDataValue(const int id, const int role, const QVariant value) +{ + //qDebug() << "id:" << id << "role:" << role << "value:" << value; + + const QModelIndexList idxlst = match(index(0, 0), UpdatesItemModel::IMDR_Id, QVariant(id), 1, Qt::MatchExactly); + + if (!idxlst.isEmpty()) + return setData(idxlst.at(0), value, role); + + return false; +} + +int UpdatesFilteredItemModel::id(const int row) +{ + const QModelIndex & idx = index(row, 0); + + if (!idx.isValid()) + return 0; + + bool ok; + const int id = data(idx, UpdatesItemModel::IMDR_Id).toInt(&ok); + + if (ok) + return id; + + return 0; +} + +int UpdatesFilteredItemModel::id(const QVariant value, const Qt::MatchFlags flags, const int role) +{ + const QModelIndexList idxlst = match(index(0, 0), role, value, 1, flags); + + if (!idxlst.isEmpty()) { + bool ok; + const int id = data(idxlst.at(0), UpdatesItemModel::IMDR_Id).toInt(&ok); + + if (ok) + return id; + } + + return 0; +} + +QStringList UpdatesFilteredItemModel::list() +{ + QStringList strl; + + for (int i = 0; i < rowCount(); i++) + strl << data(index(i, 0)).toString(); + + return strl; +} + + +/* + ReleasesFilteredItemModel +*/ + +ReleasesFilteredItemModel::ReleasesFilteredItemModel(UpdatesItemModel * sourceModel) : + UpdatesFilteredItemModel(sourceModel, "Filtered Releases") +{ + setSortRole(UpdatesItemModel::IMDR_Date); +} + +int ReleasesFilteredItemModel::channelLatestId() +{ + return rows() > 0 ? id(0) : 0; // assumes model sorted descending date +} + +/* + AssetsFilteredItemModel +*/ + +AssetsFilteredItemModel::AssetsFilteredItemModel(UpdatesItemModel * sourceModel) : + UpdatesFilteredItemModel(sourceModel, "Filtered Assets") +{ + +} + +bool AssetsFilteredItemModel::setFlags(const int id, const int flags) +{ + return setMetaDataValue(id, UpdatesItemModel::IMDR_Flags, QVariant(flags)); +} + +bool AssetsFilteredItemModel::setSubDirectory(const int id, const QString path) +{ + return setMetaDataValue(id, UpdatesItemModel::IMDR_SubDirectory, QVariant(path)); +} + +bool AssetsFilteredItemModel::setCopyFilter(const int id, const QString filter) +{ + return setMetaDataValue(id, UpdatesItemModel::IMDR_CopyFilter, QVariant(filter)); +} + +/* + ReleasesMetaData +*/ + +ReleasesMetaData::ReleasesMetaData(QWidget * parent) : + QWidget(parent), + m_repo(""), + m_id(0) +{ + itemModel = new ReleasesItemModel(); + filteredItemModel = new ReleasesFilteredItemModel(itemModel); +} + +bool ReleasesMetaData::refreshRequired() +{ + if (m_id != 0) + return itemModel->releaseChannelChanged(); + + return true; +} + +/* + AssetsMetaData +*/ + +AssetsMetaData::AssetsMetaData(QWidget * parent) : + QWidget(parent) +{ + itemModel = new AssetsItemModel(); + filteredItemModel = new AssetsFilteredItemModel(itemModel); +} diff --git a/companion/src/updates/repomodels.h b/companion/src/updates/repomodels.h new file mode 100644 index 000000000..037a26095 --- /dev/null +++ b/companion/src/updates/repomodels.h @@ -0,0 +1,300 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +constexpr char GITHUB_API_REPOS[] {"https://api.github.com/repos"}; +constexpr char GH_REPOS_EDGETX[] {"https://api.github.com/repos/EdgeTX"}; + +constexpr char GH_ACCEPT_HEADER_BINARY[] {"application/octet-stream"}; +constexpr char GH_ACCEPT_HEADER_METADATA[] {"application/vnd.github.v3+json"}; +constexpr char GH_ACCEPT_HEADER_RAW[] {"application/vnd.github.v3.raw"}; + +enum MetaDataType { + MDT_Release, + MDT_Releases, + MDT_Asset, + MDT_ReleaseAssets +}; + +QString metaDataTypeToString(MetaDataType val); + +class UpdatesItemModel : public QStandardItemModel +{ + Q_OBJECT + + public: + enum ItemModelDataRoles { + IMDR_Id = Qt::UserRole, + IMDR_Date, + IMDR_Tag, + IMDR_Prerelease, + IMDR_Content, + IMDR_Available, + IMDR_Flags, + IMDR_CopyFilter, + IMDR_SubDirectory + }; + Q_ENUM(ItemModelDataRoles) + + explicit UpdatesItemModel(QString name = "") : + QStandardItemModel(nullptr), + json(nullptr), + m_name(name) {} + + virtual ~UpdatesItemModel() {} + + protected: + QJsonDocument * json; + + private: + QString m_name; +}; + +class ReleasesItemModel : public UpdatesItemModel +{ + Q_OBJECT + + friend class ReleasesMetaData; + + public: + explicit ReleasesItemModel(); + virtual ~ReleasesItemModel() {} + + void setNightlyName(const QString name) { m_nightlyName = name.toLower(); } + void setSettingsIndex(const int index) { m_settingsIdx = index; } + + void parseMetaData(const int mdt, QJsonDocument * json); + int settingsIndex() { return m_settingsIdx; } + bool releaseChannelChanged(); + + private: + int m_settingsIdx; + QString m_nightlyName; + int m_releaseChannel; + + bool isReleaseAvailable(const QString tagname, const bool prerelease); + + void parseRelease(); + void parseReleases(); + void parseJsonObject(const QJsonObject & obj); +}; + +class AssetsItemModel : public UpdatesItemModel +{ + Q_OBJECT + + friend class AssetsMetaData; + + public: + explicit AssetsItemModel(); + virtual ~AssetsItemModel() {} + + void parseMetaData(int mdt, QJsonDocument * json); + + private: + void parseAsset(); + void parseReleaseAssets(); + void parseJsonObject(const QJsonObject & obj); +}; + +class UpdatesFilteredItemModel: public QSortFilterProxyModel +{ + Q_OBJECT + public: + explicit UpdatesFilteredItemModel(UpdatesItemModel * sourceModel, QString name = ""); + virtual ~UpdatesFilteredItemModel() {}; + + void dumpContents(); + QStringList list(); + + int id(const int row); + int id(const QVariant value, const Qt::MatchFlags flags = Qt::MatchExactly, const int role = Qt::DisplayRole); + int rows() { return rowCount(); } + + QString date(const int id) { return metaDataValue(id, UpdatesItemModel::IMDR_Date).toString(); } + QString name(const int id) { return metaDataValue(id, Qt::DisplayRole).toString(); } + + public slots: + void setFilterFlags(const int flags); + void setFilterPattern(const QString & pattern); + + signals: + + protected: + int filterFlags; + QRegularExpression filterText; + + bool filterAcceptsRow(int sourceRow, const QModelIndex & sourceParent) const override; + + QMap itemMetaData(int id); + QVariant metaDataValue(const int id, const int role); + bool setMetaDataValue(const int id, const int role, const QVariant value); + + private: + QString m_name; +}; + +class ReleasesFilteredItemModel: public UpdatesFilteredItemModel +{ + Q_OBJECT + + public: + + protected: + friend class ReleasesMetaData; + + explicit ReleasesFilteredItemModel(UpdatesItemModel * sourceModel); + virtual ~ReleasesFilteredItemModel() {}; + + int channelLatestId(); + + bool prerelease(const int id) { return metaDataValue(id, UpdatesItemModel::IMDR_Prerelease).toBool(); } + QString version(const int id) { return metaDataValue(id, UpdatesItemModel::IMDR_Tag).toString(); } + + private: +}; + +class AssetsFilteredItemModel : public UpdatesFilteredItemModel +{ + Q_OBJECT + + public: + + protected: + friend class AssetsMetaData; + + explicit AssetsFilteredItemModel(UpdatesItemModel * sourceModel); + virtual ~AssetsFilteredItemModel() {}; + + bool setFlags(const int id, const int flags); + bool setSubDirectory(const int id, const QString path); + bool setCopyFilter(const int id, const QString filter); + + QString filename(int id) { return metaDataValue(id, Qt::DisplayRole).toString(); } + + private: +}; + +class ReleasesMetaData : public QWidget +{ + Q_OBJECT + public: + explicit ReleasesMetaData(QWidget * parent); + virtual ~ReleasesMetaData() {}; + + bool refreshRequired(); + + void setRepo(QString repo) { m_repo = repo; } + void setNightlyName(QString name) { itemModel->setNightlyName(name); } + void setSettingsIndex(int index) { itemModel->setSettingsIndex(index); } + + void setId(int id) { m_id = id; } + int id() { return m_id; } + + int getSetId() { m_id = filteredItemModel->channelLatestId(); return m_id; } + + int getSetId(int row) { m_id = filteredItemModel->id(row); return m_id; } + + int getSetId(QVariant value, Qt::MatchFlags flags = Qt::MatchExactly, int role = Qt::DisplayRole) + { m_id = filteredItemModel->id(value, flags, role); return m_id; } + + void parseMetaData(int mdt, QJsonDocument * jsonDoc) + { itemModel->parseMetaData(mdt, jsonDoc); } + + int count() { return filteredItemModel->rows(); } + QStringList list() { return filteredItemModel->list(); } + void dumpContentsFiltered() { filteredItemModel->dumpContents(); } + + QString date() { return filteredItemModel->date(m_id); } + QString name() { return filteredItemModel->name(m_id); } + bool prerelease() { return filteredItemModel->prerelease(m_id); } + QString version() { return filteredItemModel->version(m_id); } + + QString urlReleases() { return m_repo % "/releases"; } + QString urlReleaseLatest() { return urlReleases() % "/latest"; } + QString urlRelease() { return urlReleases() % QString("/%1").arg(m_id); } + + private: + ReleasesItemModel *itemModel; + ReleasesFilteredItemModel *filteredItemModel; + QString m_repo; + int m_id; +}; + +class AssetsMetaData : public QWidget +{ + Q_OBJECT + public: + explicit AssetsMetaData(QWidget * parent); + virtual ~AssetsMetaData() {}; + + void setRepo(QString repo) { m_repo = repo; } + + void setId(int id) { m_id = id; } + int id() { return m_id; } + + int getSetId(int row) { m_id = filteredItemModel->id(row); return m_id; } + + int getSetId(QVariant value, Qt::MatchFlags flags = Qt::MatchExactly, int role = Qt::DisplayRole) + { m_id = filteredItemModel->id(value, flags, role); return m_id; } + + void parseMetaData(int mdt, QJsonDocument * jsonDoc) { itemModel->parseMetaData(mdt, jsonDoc); } + + bool setFlags(int flags) { return filteredItemModel->setFlags(m_id, flags); } + + void setFilterFlags(int flags) { filteredItemModel->setFilterFlags(flags); } + void setFilterPattern(const QString & pattern) { filteredItemModel->setFilterPattern(pattern); } + + bool setSubDirectory(const QString path) { return filteredItemModel->setSubDirectory(m_id, path); } + bool setCopyFilter(const QString filter) { return filteredItemModel->setCopyFilter(m_id, filter); } + + int count() { return filteredItemModel->rows(); } + QStringList list() { return filteredItemModel->list(); } + void dumpContentsFiltered() { filteredItemModel->dumpContents(); } + + QString date() { return filteredItemModel->date(m_id); } + QString name() { return filteredItemModel->name(m_id); } + QString filename() { return filteredItemModel->metaDataValue(m_id, Qt::DisplayRole).toString(); } + QString contentType() { return filteredItemModel->metaDataValue(m_id, UpdatesItemModel::IMDR_Content).toString(); } + QString subDirectory() { return filteredItemModel->metaDataValue(m_id, UpdatesItemModel::IMDR_SubDirectory).toString(); } + QString copyFilter() { return filteredItemModel->metaDataValue(m_id, UpdatesItemModel::IMDR_CopyFilter).toString(); } + int flags() { return filteredItemModel->metaDataValue(m_id, UpdatesItemModel::IMDR_Flags).toInt(); } + + QString urlReleaseAssets(int releaseId, int max) + { return urlReleases() % QString("/%1/assets").arg(releaseId) % (max > -1 ? QString("\?per_page=%1").arg(max) : ""); } + QString urlAsset() { return urlReleases() % QString("/assets/%1").arg(m_id); } + + QString urlContent(QString path) { return m_repo % QString("/contents/%1").arg(path); } + + private: + AssetsItemModel *itemModel; + AssetsFilteredItemModel *filteredItemModel; + QString m_repo; + int m_id; + + QString urlReleases() { return m_repo % "/releases"; } +}; diff --git a/companion/src/updates/updatecompanion.cpp b/companion/src/updates/updatecompanion.cpp new file mode 100644 index 000000000..af1be0336 --- /dev/null +++ b/companion/src/updates/updatecompanion.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updatecompanion.h" +#include "constants.h" +#include "version.h" + +#include +#include +#include + +#ifdef Q_OS_MACOS + #define + #define OS_FILEPATTERN "edgetx-cpn-osx" + #define OS_INSTALLER_EXTN "*.dmg" + #define OS_INSTALL_QUESTION tr("Would you like to open the disk image to install the new version?") +#elif defined(Q_OS_WIN64) + #define OS_FILEPATTERN "edgetx-cpn-win64" + #define OS_INSTALLER_EXTN "*.exe" + #define OS_INSTALL_QUESTION tr("Would you like to launch the installer?") +#elif defined(Q_OS_WIN32) + #define OS_FILEPATTERN "edgetx-cpn-win32" + #define OS_INSTALLER_EXTN "*.exe" + #define OS_INSTALL_QUESTION tr("Would you like to launch the installer?") +#else + #define OS_FILEPATTERN "edgetx-cpn-linux" + #define OS_INSTALLER_EXTN "*.AppImage" + #define OS_INSTALL_QUESTION tr("Would you like to open the app image to install the new version?") +#endif + +UpdateCompanion::UpdateCompanion(QWidget * parent) : + UpdateInterface(parent) +{ + setName("Companion"); + setRepo(QString(GH_REPOS_EDGETX).append("/edgetx")); + setReleasesNightlyName("nightly"); + + dfltParams->data.flags = (UPDFLG_Common | UPDFLG_Locked | UPDFLG_AsyncInstall) & ~UPDFLG_CopyDest; + + UpdateParameters::AssetParams &ap = dfltParams->addAsset(); + ap.filterType = UpdateParameters::UFT_Startswith; + ap.filter = QString("%1").arg(QString(OS_FILEPATTERN)); + ap.maxExpected = 1; + ap.flags = dfltParams->data.flags; +} + +bool UpdateCompanion::asyncInstall() +{ + progressMessage(tr("Async install")); + + assets->setFilterFlags(UPDFLG_AsyncInstall); + reportProgress(tr("Asset filter applied: %1 Assets found: %2").arg(updateFlagsToString(UPDFLG_AsyncInstall)).arg(assets->count()), QtDebugMsg); + + if (assets->count() != 1) { + reportProgress(tr("Expected 1 asset for install but none found"), QtCriticalMsg); + return false; + } + + assets->getSetId(0); + + QString installerPath = decompressDir % QString("/A%1/%2").arg(assets->id()).arg(QFileInfo(assets->filename()).completeBaseName()); + + QDirIterator it(installerPath, { OS_INSTALLER_EXTN }, QDir::Files, QDirIterator::Subdirectories); + + bool found = false; + + while (it.hasNext()) + { + installerPath = it.next(); + found = true; + break; + } + + if (!found) { + reportProgress(tr("Installer not found in %1 using filter %2").arg(installerPath).arg(OS_INSTALLER_EXTN), QtCriticalMsg); + return false; + } + + int ret = QMessageBox::question(parentWidget(), CPN_STR_APP_NAME, OS_INSTALL_QUESTION, QMessageBox::Yes | QMessageBox::No); + + if (ret == QMessageBox::Yes) { + if (QDesktopServices::openUrl(QUrl::fromLocalFile(installerPath))) + qApp->exit(); + } + + return true; +} + +const QString UpdateCompanion::currentRelease() +{ +#if defined(VERSION_TAG) + return UpdateInterface::latestRelease(); +#else + dfltParams->data.currentRelease = QString("EdgeTX v%1.%2 (Custom)").arg(VERSION_MAJOR).arg(VERSION_MINOR); + runParams->data.currentRelease = dfltParams->data.currentRelease; + return dfltParams->data.currentRelease; +#endif +} + +const QString UpdateCompanion::currentVersion() +{ +#if defined(VERSION_TAG) + return UpdateInterface::latestRelease(); +#else + return QString(QString("v%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR)); +#endif +} + +const bool UpdateCompanion::isLatestRelease() +{ +#if defined(VERSION_TAG) + return UpdateInterface::isLatestRelease(); +#else + return true; +#endif +} + +const bool UpdateCompanion::isUpdateAvailable() +{ +#if defined(VERSION_TAG) + return UpdateInterface::isUpdateAvailable(); +#else + return false; +#endif +} diff --git a/companion/src/releasenotesdialog.cpp b/companion/src/updates/updatecompanion.h similarity index 50% rename from companion/src/releasenotesdialog.cpp rename to companion/src/updates/updatecompanion.h index 35cbe00b1..fe3d596af 100644 --- a/companion/src/releasenotesdialog.cpp +++ b/companion/src/updates/updatecompanion.h @@ -1,7 +1,8 @@ /* - * Copyright (C) OpenTX + * Copyright (C) EdgeTX * * Based on code named + * opentx - https://github.com/opentx/opentx * th9x - http://code.google.com/p/th9x * er9x - http://code.google.com/p/er9x * gruvin9x - http://code.google.com/p/gruvin9x @@ -18,29 +19,23 @@ * GNU General Public License for more details. */ -#include "releasenotesdialog.h" -#include "ui_htmldialog.h" -#include +#pragma once -ReleaseNotesDialog::ReleaseNotesDialog(QWidget * parent) : - QDialog(parent), - ui(new Ui::HtmlDialog) +#include "updateinterface.h" + +class UpdateCompanion : public UpdateInterface { - ui->setupUi(this); + Q_DECLARE_TR_FUNCTIONS(UpdateCompanion) - setWindowTitle(tr("Companion Release Notes")); - setWindowIcon(CompanionIcon("changelog.png")); + public: - QFile file(":/releasenotes.txt"); - if (file.open( QIODevice::ReadOnly | QIODevice::Text ) ) { - ui->textEditor->setHtml(file.readAll()); - ui->textEditor->setOpenExternalLinks(true); - } - ui->textEditor->scroll(0, 0); - ui->textEditor->setOpenExternalLinks(true); -} + explicit UpdateCompanion(QWidget * parent); + virtual ~UpdateCompanion() {} -ReleaseNotesDialog::~ReleaseNotesDialog() -{ - delete ui; -} + protected: + virtual bool asyncInstall() override; + virtual const QString currentRelease(); + virtual const QString currentVersion(); + virtual const bool isLatestRelease(); + virtual const bool isUpdateAvailable(); +}; diff --git a/companion/src/updates/updatefirmware.cpp b/companion/src/updates/updatefirmware.cpp new file mode 100644 index 000000000..d51b9e6e9 --- /dev/null +++ b/companion/src/updates/updatefirmware.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updatefirmware.h" +#include "appdata.h" +#include "flashfirmwaredialog.h" + +UpdateFirmware::UpdateFirmware(QWidget * parent) : + UpdateInterface(parent) +{ + setName("Firmware"); + setRepo(QString(GH_REPOS_EDGETX).append("/edgetx")); + setReleasesNightlyName("nightly"); + + dfltParams->data.flags = UPDFLG_Common | UPDFLG_Locked | UPDFLG_AsyncInstall; + + UpdateParameters::AssetParams &ap = dfltParams->addAsset(); + ap.filterType = UpdateParameters::UFT_Startswith; + ap.filter = "edgetx-firmware"; + ap.maxExpected = 1; + ap.flags = dfltParams->data.flags | UPDFLG_CopyFiles; + ap.destSubDir = "FIRMWARE"; + ap.copyFilterType = UpdateParameters::UFT_Expression; + ap.copyFilter = QString("^%1-.*\\.bin$").arg(fwFlavour); +} + +bool UpdateFirmware::asyncInstall() +{ + progressMessage(tr("Async install")); + + assets->setFilterFlags(UPDFLG_AsyncInstall); + reportProgress(tr("Asset filter applied: %1 Assets found: %2").arg(updateFlagsToString(UPDFLG_AsyncInstall)).arg(assets->count()), QtDebugMsg); + + if (assets->count() != 1) { + reportProgress(tr("Expected 1 asset for install but none found"), QtCriticalMsg); + return false; + } + + QString destPath = updateDir; + + if (!assets->subDirectory().isEmpty()) + destPath.append("/" % assets->subDirectory()); + + QRegularExpression filter(runParams->data.assets.at(0).copyFilter, QRegularExpression::CaseInsensitiveOption); + + QDirIterator it(destPath); + + bool found = false; + + while (it.hasNext()) + { + QFileInfo file(it.next()); + + if ((!filter.pattern().isEmpty()) && (!file.fileName().contains(filter))) + continue; + + destPath.append("/" % file.fileName()); + found = true; + break; + } + + if (!found) { + reportProgress(tr("Firmware not found in %1 using filter %2").arg(destPath).arg(filter.pattern()), QtCriticalMsg); + return false; + } + + g.currentProfile().fwName(destPath); + + reportProgress(tr("Write firmware to radio option setting: %1").arg(g.currentProfile().burnFirmware() ? tr("true") : tr("false")), QtDebugMsg); + + if (g.currentProfile().burnFirmware()) { + int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Do you want to write the firmware to the radio now ?"), QMessageBox::Yes | QMessageBox::No); + if (ret == QMessageBox::Yes) { + FlashFirmwareDialog *dlg = new FlashFirmwareDialog(this); + dlg->exec(); + dlg->deleteLater(); + } + } + + return true; +} diff --git a/companion/src/updates/updatefirmware.h b/companion/src/updates/updatefirmware.h new file mode 100644 index 000000000..b96d24143 --- /dev/null +++ b/companion/src/updates/updatefirmware.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include "updateinterface.h" + +class UpdateFirmware : public UpdateInterface +{ + Q_DECLARE_TR_FUNCTIONS(UpdateFirmware) + + public: + + explicit UpdateFirmware(QWidget * parent); + virtual ~UpdateFirmware() {} + + protected: + virtual bool asyncInstall() override; +}; diff --git a/companion/src/updates/updateinterface.cpp b/companion/src/updates/updateinterface.cpp new file mode 100644 index 000000000..b8eeaac66 --- /dev/null +++ b/companion/src/updates/updateinterface.cpp @@ -0,0 +1,1550 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updateinterface.h" +#include "appdata.h" +#include "minizinterface.h" +#include "helpers.h" +#include "updatefirmware.h" +#include "updatecompanion.h" +#include "updatesdcard.h" +#include "updatesounds.h" +#include "updatethemes.h" +#include "updatemultiprotocol.h" + +#include +#include +#include +#include +#include +#include + +/* + + UpdateParameters + +*/ + +UpdateParameters::UpdateParameters(QWidget * parent) : + QWidget(parent) +{ + data.flags = 0; + data.decompressUseDwnld = true; + data.updateUseSD = true; +} + +UpdateParameters& UpdateParameters::operator=(const UpdateParameters& source) +{ + // Perform a deep copy + + // check for self-assignment + + + if (this != &source) { + data.flags = source.data.flags; + data.currentRelease = source.data.currentRelease; + data.updateRelease = source.data.updateRelease; + data.downloadDir = source.data.downloadDir; + data.decompressUseDwnld = source.data.decompressUseDwnld; + data.decompressDir = source.data.decompressDir; + data.updateUseSD = source.data.updateUseSD; + data.updateDir = source.data.updateDir; + + while (!data.assets.isEmpty()) { + data.assets.removeLast(); + } + + for (int i = 0; i < source.data.assets.size(); i++) { + data.assets.append(source.data.assets[i]); + } + } + + return *this; +} + +UpdateParameters::AssetParams & UpdateParameters::addAsset() +{ + AssetParams ap; + ap.flags = 0; + ap.filterType = UFT_None; + ap.maxExpected = -1; + ap.copyFilterType = UFT_None; + + data.assets.append(ap); + + return data.assets[data.assets.size() - 1]; +} + +// static +QString UpdateParameters::buildFilterPattern(const UpdateParameters::UpdateFilterType filterType, const QString & filter) +{ + QString pattern = filter; + + switch ((int)filterType) { + case UpdateParameters::UFT_Exact: + pattern.prepend("^"); + pattern.append("$"); + break; + case UpdateParameters::UFT_Startswith: + pattern.prepend("^"); + break; + case UpdateParameters::UFT_Endswith: + pattern.append("$"); + break; + case UpdateParameters::UFT_Contains: + case UpdateParameters::UFT_Expression: + break; + default: + pattern = ""; + } + + return pattern; +} + +// static +QStringList UpdateParameters::updateFilterTypeList() +{ + return { tr("None"), tr("Exact"), tr("Startswith"), tr("Endswith"), tr("Contains"), tr("Expression") }; +} + +// static +QString UpdateParameters::updateFilterTypeToString(UpdateFilterType uft) +{ + return updateFilterTypeList().value(uft, CPN_STR_UNKNOWN_ITEM); +} + +/* + + UpdateInterface + +*/ + +UpdateInterface::UpdateInterface(QWidget * parent) : + QWidget(parent), + reply(nullptr), + buffer(new QByteArray()), + file(nullptr) +{ + progress = nullptr; + logLevel = g.updateLogLevel(); + settingsIdx = -1; + + QNetworkProxyFactory::setUseSystemConfiguration(true); + + releases = new ReleasesMetaData(this); + assets = new AssetsMetaData(this); + dfltParams = new UpdateParameters(this); + runParams = new UpdateParameters(this); + + getRadioProfileSettings(); + + setReleasesNightlyName(""); + dfltParams->data.flags = UPDFLG_Common; + resultsPerPage = -1; + + initParamFolders(dfltParams); +} + +UpdateInterface::~UpdateInterface() +{ + if (reply) + delete reply; + delete buffer; + if (file) + delete file; + delete releases; + delete assets; + delete dfltParams; + delete runParams; +} + +bool UpdateInterface::autoUpdate(ProgressWidget * progress) +{ + this->progress = progress; + + if (dfltParams->data.flags & UPDFLG_Update && !isLatestRelease()) + return update(); + + return true; +} + +bool UpdateInterface::manualUpdate(ProgressWidget * progress) +{ + this->progress = progress; + + if (runParams->data.flags & UPDFLG_Update) + return update(); + + return true; +} + +bool UpdateInterface::update() +{ + if (progress) { + progress->setInfo(tr("Processing updates for: %1").arg(name)); + progress->setValue(0); + progress->setMaximum(100); + } + + reportProgress(tr("Processing updates for: %1").arg(name), QtInfoMsg); + + if ((runParams->data.flags & UPDFLG_Preparation) && !preparation()) { + reportProgress(tr("%1 step preparation failed").arg(name), QtCriticalMsg); + return false; + } + + if ((runParams->data.flags & UPDFLG_Download) && !download()) { + reportProgress(tr("%1 step download failed").arg(name), QtCriticalMsg); + return false; + } + + if ((runParams->data.flags & UPDFLG_Decompress) && !decompress()) { + reportProgress(tr("%1 step decompress failed").arg(name), QtCriticalMsg); + return false; + } + + if ((runParams->data.flags & UPDFLG_CopyDest) && !copyToDestination()) { + reportProgress(tr("%1 step copy to destination failed").arg(name), QtCriticalMsg); + return false; + } + + if ((runParams->data.flags & UPDFLG_Housekeeping) && !housekeeping()) { + reportProgress(tr("%1 step housekeeping failed").arg(name), QtCriticalMsg); + return false; + } + + if ((runParams->data.flags & UPDFLG_AsyncInstall) && !asyncInstall()) { + reportProgress(tr("%1 step start async failed").arg(name), QtCriticalMsg); + return false; + } + + reportProgress(tr("%1 update successful").arg(name), QtInfoMsg); + + QMessageBox::information(this, CPN_STR_APP_NAME, tr("%1 update successful").arg(name)); + + return true; +} + +// static +QString UpdateInterface::updateFlagsToString(UpdateFlags val) +{ + switch ((int)val) { + case UPDFLG_None: + return "None"; + case UPDFLG_Update: + return "Update"; + case UPDFLG_Locked: + return "Locked"; + case UPDFLG_Preparation: + return "Preparation"; + case UPDFLG_Download: + return "Download"; + case UPDFLG_DelDownload: + return "Delete Download"; + case UPDFLG_Decompress: + return "Decompress"; + case UPDFLG_DelDecompress: + return "Delete Decompress"; + case UPDFLG_CopyDest: + return "Copy to Destination"; + case UPDFLG_CopyFiles: + return "Copy Files"; + case UPDFLG_CopyStructure: + return "Copy Structure"; + case UPDFLG_Housekeeping: + return "Housekeeping"; + case UPDFLG_AsyncInstall: + return "Async Install"; + default: + return CPN_STR_UNKNOWN_ITEM; + } +} + +// static +QString UpdateInterface::downloadDataTypeToString(DownloadDataType val) +{ + switch ((int)val) { + case DDT_Binary: + return "Binary"; + case DDT_Content: + return "Content"; + case DDT_MetaData: + return "Meta Data"; + default: + return CPN_STR_UNKNOWN_ITEM; + } +} + +// static +QString UpdateInterface::semanticVersion(QString version) +{ + QStringList strl = version.split("."); + + while (strl.count() < 4) { + strl.append("0"); + } + + return strl.join("."); +} + +void UpdateInterface::reportProgress(const QString & text, const int type) +{ + if (logLevel == QtDebugMsg || (logLevel == QtInfoMsg && type > QtDebugMsg) || (type < QtInfoMsg && type >= logLevel)) { + if (progress) + progress->addMessage(text, type); + else + qDebug() << text; + } + + if (type == QtCriticalMsg || type == QtFatalMsg) + criticalMsg(text); +} + +void UpdateInterface::progressMessage(const QString & text) +{ + if (progress) + progress->setInfo(text); + else + qDebug() << text; +} + +void UpdateInterface::criticalMsg(const QString & msg) +{ + QMessageBox::critical(nullptr, CPN_STR_APP_NAME % ": " % tr("Update Interface"), msg); +} + +void UpdateInterface::setName(QString name) +{ + this->name = name; + setSettingsIdx(); + currentRelease(); +} + +void UpdateInterface::initParamFolders(UpdateParameters * params) +{ + if (g.downloadDir().trimmed().isEmpty()) + g.downloadDirReset(true); + + params->data.downloadDir = g.downloadDir().trimmed(); + + params->data.decompressUseDwnld = g.decompressUseDwnld(); + + if (params->data.decompressUseDwnld) + params->data.decompressDir = params->data.downloadDir; + else { + if (g.decompressDir().trimmed().isEmpty()) + g.decompressDirReset(true); + + params->data.decompressDir = g.decompressDir().trimmed(); + } + + params->data.updateUseSD = g.updateUseSD(); + + if (params->data.updateUseSD && (!g.currentProfile().sdPath().trimmed().isEmpty())) + params->data.updateDir = g.currentProfile().sdPath().trimmed(); + else { + if (g.updateDir().trimmed().isEmpty()) + g.updateDirReset(true); + + params->data.updateDir = g.updateDir().trimmed(); + } +} + +void UpdateInterface::resetRunParams() +{ + progress = nullptr; + *runParams = *dfltParams; + runParams->data.flags &= ~UPDFLG_Update; +} + +int UpdateInterface::getSettingsIdx() +{ + int idx = g.getComponentIndex(name); + + if (idx < 0) { + for (idx = 0; idx < MAX_COMPONENTS && g.component[idx].existsOnDisk(); idx++) + ; + if (idx >= MAX_COMPONENTS) { + reportProgress(tr("No free slot to save interface settings!"), QtCriticalMsg); + idx = -1; + } + else { + g.component[idx].init(); + g.component[idx].name(name); + } + } + + return idx; +} + +void UpdateInterface::setSettingsIdx() +{ + const int idx = getSettingsIdx(); + settingsIdx = idx; + releases->setSettingsIndex(idx); +} + +void UpdateInterface::getRadioProfileSettings() +{ + // eg edgetx-ts16s-en + QStringList strl = QStringList(g.currentProfile().fwType().split('-')); + + if (strl.size() < 1) { + reportProgress(tr("Settings radio profile firmware type unexpected format"), QtWarningMsg); + return; + } + + if (strl.size() > 1) { + fwFlavour = strl.at(1); + fwFlavour = fwFlavour.replace('+', 'p'); + } + + if (strl.size() > 2) + language = strl.at(2); +} + +void UpdateInterface::clearRelease() +{ + g.component[settingsIdx].clearRelease(); + currentRelease(); +} + +const QString UpdateInterface::currentRelease() +{ + dfltParams->data.currentRelease = g.component[settingsIdx].release(); + runParams->data.currentRelease = dfltParams->data.currentRelease; + return dfltParams->data.currentRelease; +} + +const QString UpdateInterface::latestRelease() +{ + if (repoReleasesMetaData()) { + releases->getSetId(); + if (releases->id()) + dfltParams->data.updateRelease = releases->name(); + else + dfltParams->data.updateRelease = ""; + } + + return dfltParams->data.updateRelease; +} + +const QString UpdateInterface::updateRelease() +{ + if (runParams->data.updateRelease.isEmpty()) { + if (dfltParams->data.updateRelease.isEmpty()) + latestRelease(); + + runParams->data.updateRelease = dfltParams->data.updateRelease; + } + + return runParams->data.updateRelease; +} + +const bool UpdateInterface::isUpdateAvailable() +{ + if (g.component[settingsIdx].checkForUpdate()) + return !isLatestRelease(); + else { + return false; + } +} + +const bool UpdateInterface::isLatestVersion(const QString & installed, const QString & latest) +{ + QStringList a = semanticVersion(installed).split("."); + QStringList b = semanticVersion(latest).split("."); + + // not equal comparision used to force update if client upgraded and the release is subsequently withdrawn or subscribed to nightlies + for (int i = 0; i < 4; i++) { + if (a.at(i) != b.at(i)) { + return false; + } + } + + return true; +} + +const bool UpdateInterface::isLatestRelease() +{ + // subsequent processes rely on this call being successful so advise nothing to do + if (!repoReleasesMetaData()) { + return true; + } + + QString currentVer = currentVersion(); + QString latestVer = releases->version(); + // nightlies often have the same version so also check id + if (isLatestVersion(currentVer, latestVer) && g.component[settingsIdx].id() == releases->id()) { + return true; + } + else { + return false; + } +} + +const QString UpdateInterface::currentVersion() +{ + return g.component[settingsIdx].version(); +} + +const QStringList UpdateInterface::getReleases() +{ + repoReleasesMetaData(); + return releases->list(); +} + +bool UpdateInterface::repoReleasesMetaData() +{ + if (releases->refreshRequired()) { + if (!downloadReleasesMetaData()) { + reportProgress(tr("Unable to download release meta data"), QtDebugMsg); + releases->setId(0); + return false; + } + } + + releases->getSetId(); + + return true; +} + +bool UpdateInterface::repoReleaseAssetsMetaData() +{ + if (!downloadReleaseAssetsMetaData(releases->id())) { + reportProgress(tr("Unable to download release channel assets information"), QtDebugMsg); + return false; + } + + return true; +} + +bool UpdateInterface::setRunFolders() +{ + QString fldr(releases->name()); + fldr = fldr.replace("\"", ""); + fldr = fldr.replace("'", ""); + fldr = fldr.replace(" ", "_"); + + downloadDir = runParams->data.downloadDir % "/" % name % "/" % fldr; + + if (!checkCreateDirectory(downloadDir, UPDFLG_Download)) + return false; + + reportProgress(tr("Download directory: %1").arg(downloadDir), QtDebugMsg); + + decompressDir = runParams->data.decompressDir % "/" % name % "/" % fldr; + + if (!checkCreateDirectory(decompressDir, UPDFLG_Decompress)) + return false; + + reportProgress(tr("Decompress directory: %1").arg(decompressDir), QtDebugMsg); + + updateDir = runParams->data.updateDir; + + if (!checkCreateDirectory(updateDir, UPDFLG_CopyDest)) + return false; + + reportProgress(tr("Update directory: %1").arg(updateDir), QtDebugMsg); + + return true; +} + +bool UpdateInterface::checkCreateDirectory(const QString & dir, const UpdateFlags flag) +{ + if (runParams->data.flags & flag) { + if (dir.isEmpty()) { + reportProgress(tr("%1 directory not configured in application settings!").arg(updateFlagsToString(flag)), QtCriticalMsg); + return false; + } + else if (!QDir().mkpath(dir)) { + reportProgress(tr("Failed to create %1 directory %2!").arg(updateFlagsToString(flag)).arg(dir), QtCriticalMsg); + return false; + } + } + + return true; +} + +bool UpdateInterface::downloadReleasesMetaData() +{ + progressMessage(tr("Download releases metadata")); + downloadMetaData(MDT_Releases, releases->urlReleases()); + return downloadSuccess; +} + +bool UpdateInterface::downloadReleaseLatestMetaData() +{ + progressMessage(tr("Download latest release metadata")); + downloadMetaData(MDT_Release, releases->urlReleaseLatest()); + return downloadSuccess; +} + +bool UpdateInterface::downloadReleaseMetaData(const int releaseId) +{ + progressMessage(tr("Download release %1 metadata").arg(releaseId)); + releases->setId(releaseId); + downloadMetaData(MDT_Release, releases->urlRelease()); + return downloadSuccess; +} + +bool UpdateInterface::downloadReleaseAssetsMetaData(const int releaseId) +{ + progressMessage(tr("Download release %1 assets metadata").arg(releaseId)); + downloadMetaData(MDT_ReleaseAssets, assets->urlReleaseAssets(releaseId, resultsPerPage)); + return downloadSuccess; +} + +bool UpdateInterface::downloadAssetMetaData(const int assetId) +{ + progressMessage(tr("Download asset %1 metadata").arg(assetId)); + assets->setId(assetId); + downloadMetaData(MDT_Asset, assets->urlAsset()); + return downloadSuccess; +} + +void UpdateInterface::downloadMetaData(const MetaDataType mdt, const QString & url) +{ + reportProgress(tr("Download metadata for: %1").arg(metaDataTypeToString(mdt).toLower())); + download(DDT_MetaData, mdt, url, GH_ACCEPT_HEADER_METADATA, QString()); +} + +bool UpdateInterface::downloadTextFileToBuffer(const QString & path) +{ + progressMessage(tr("Download file %1").arg(path)); + reportProgress(tr("Download file: %1").arg(path), QtInfoMsg); + + downloadFileToBuffer(assets->urlContent(path)); + return downloadSuccess; +} + +void UpdateInterface::downloadFileToBuffer(const QString & url) +{ + download(DDT_Content, 0, url, GH_ACCEPT_HEADER_RAW, QString()); +} + +void UpdateInterface::download(const DownloadDataType type, const int subtype, const QString & urlStr, + const char * header, const QString & filePath) +{ + if (progress) { + progress->setValue(0); + progress->setMaximum(100); + } + + buffer->clear(); + downloadSuccess = false; + + if (type == DDT_SaveToFile) { + file = new QFile(filePath); + if (!file->open(QIODevice::WriteOnly)) { + reportProgress(tr("Unable to open the download file %1 for writing.\nError: %2").arg(filePath).arg(file->errorString()), QtCriticalMsg); + return; + } + } + else + file = nullptr; + + url.setUrl(urlStr); + + if (!url.isValid()) { + reportProgress(tr("Invalid URL: %1").arg(urlStr), QtCriticalMsg); + return; + } + else + reportProgress(tr("URL: %1").arg(urlStr), QtDebugMsg); + + request.setUrl(url); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferNetwork); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + + request.setRawHeader(QByteArray("Accept"), QByteArray(header)); + + reply = manager.get(request); + + connect(reply, QOverload::of(&QNetworkReply::error), [=] (QNetworkReply::NetworkError code) { + // leave it to the finished slot to deal with error condition + reportProgress(tr("Network error has occurred. Error code: %1").arg(code), QtDebugMsg); + }); + + connect(reply, &QNetworkReply::readyRead, [=]() { + if (type == DDT_SaveToFile) { + file->write(reply->readAll()); + } + else { + const QByteArray qba = reply->readAll(); + buffer->append(qba); + } + }); + + connect(reply, &QNetworkReply::finished, [&]() { + onDownloadFinished(reply, type, subtype); + }); + + connect(reply, &QNetworkReply::downloadProgress, [=](const qint64 bytesRead, const qint64 totalBytes) { + if (progress) { + progress->setMaximum(totalBytes); + progress->setValue(bytesRead); + } + }); + + while (!reply->isFinished()) { + qApp->processEvents(); + } +} + +void UpdateInterface::onDownloadFinished(QNetworkReply * reply, DownloadDataType type, int subtype) +{ + if (type == DDT_SaveToFile) { + file->flush(); + file->close(); + } + + if (progress) + progress->setValue(progress->maximum()); + + if (reply->error()) { + reportProgress(tr("Unable to download %1.\nError:%2\n%3").arg(downloadDataTypeToString(type)).arg(reply->error()).arg(reply->errorString()), QtCriticalMsg); + + if (type == DDT_SaveToFile) { + file->remove(); + delete file; + file = nullptr; + } + + return; + } + + if (type == DDT_MetaData) { + parseMetaData(subtype); + } + else if (type == DDT_SaveToFile) { + delete file; + file = nullptr; + downloadSuccess = true; + } + else { + downloadSuccess = true; + } +} + +void UpdateInterface::parseMetaData(int mdt) +{ + reportProgress(tr("Parse and load data models with metadata"), QtDebugMsg); + + QJsonDocument *json = new QJsonDocument(); + + if (!convertDownloadToJson(json)) { + return; + } + + switch (mdt) { + case MDT_Release: + case MDT_Releases: + releases->parseMetaData(mdt, json); + break; + case MDT_Asset: + case MDT_ReleaseAssets: + assets->parseMetaData(mdt, json); + break; + } + + delete json; + downloadSuccess = true; +} + +bool UpdateInterface::convertDownloadToJson(QJsonDocument * json) +{ + QJsonParseError res; + + *json = QJsonDocument::fromJson(*buffer, &res); + + if (res.error || json->isNull()) { + reportProgress(tr("Unable to convert downloaded data to json format.\nError:%1\n%2").arg(res.error).arg(res.errorString()), QtCriticalMsg); + return false; + } + + return true; +} + +bool UpdateInterface::getSetAssets(const int flags, const UpdateParameters::UpdateFilterType assetsFilterType, const QString & assetsFilter, const int maxAssetsExpected, + const QString & subDirectory, const UpdateParameters::UpdateFilterType copyFilterType, const QString & copyFilter) +{ + QString pattern(UpdateParameters::buildFilterPattern(assetsFilterType, assetsFilter)); + assets->setFilterPattern(pattern); + reportProgress(tr("Asset filter applied: %1").arg(pattern), QtDebugMsg); + + if (assets->count() < 0) { + reportProgress(tr("No assets not found in release '%1' using filter pattern '%2'").arg(releases->name().arg(assetsFilter)), QtCriticalMsg); + return false; + } + else if (maxAssetsExpected > -1 && assets->count() > maxAssetsExpected) { + reportProgress(tr("%1 assets found when %2 expected in release '%3' using filter pattern '%4'") + .arg(assets->count()).arg(maxAssetsExpected).arg(releases->name().arg(assetsFilter)), QtCriticalMsg); + return false; + } + + reportProgress(tr("Assets found: %1").arg(assets->count()), QtDebugMsg); + + for (int i = 0; i < assets->count(); i++) { + assets->getSetId(i); + if (!assets->setFlags(flags)) { + reportProgress(tr("Unable to set processing flags for asset %1").arg(assets->filename()), QtCriticalMsg); + return false; + } + + if (!assets->setSubDirectory(subDirectory)) { + reportProgress(tr("Unable to set sub directory for asset %1").arg(assets->filename()), QtCriticalMsg); + return false; + } + + if (!assets->setCopyFilter(UpdateParameters::buildFilterPattern(copyFilterType, copyFilter))) { + reportProgress(tr("Unable to set copy filter for asset %1").arg(assets->filename()), QtCriticalMsg); + return false; + } + } + + return true; +} + +bool UpdateInterface::downloadFlaggedAssets() +{ + assets->setFilterFlags(UPDFLG_Download); + reportProgress(tr("Asset filter applied: %1 Assets found: %2").arg(updateFlagsToString(UPDFLG_Download)).arg(assets->count()), QtDebugMsg); + + for (int i = 0; i < assets->count(); ++i) { + if (!downloadAsset(i)) + return false; + } + + return true; +} + +bool UpdateInterface::downloadAsset(int row) +{ + assets->getSetId(row); + downloadBinaryToFile(assets->urlAsset(), assets->filename()); + return downloadSuccess; +} + +void UpdateInterface::downloadBinaryToFile(const QString & url, const QString & filename) +{ + downloadSuccess = false; + + QFileInfo f(downloadDir % "/" % QString("A%1/%2").arg(assets->id()).arg(filename)); + + progressMessage(tr("Downloading: %1").arg(filename)); + reportProgress(tr("Download: %1").arg(filename), QtInfoMsg); + + if (f.exists()) { + reportProgress(tr("File exists: %1").arg(filename), QtDebugMsg); + int reply = QMessageBox::question(this, CPN_STR_APP_NAME, tr("File %1 exists. Download again?").arg(filename), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); + if (reply == QMessageBox::Cancel) { + reportProgress(tr("User action: Cancel"), QtDebugMsg); + downloadSuccess = false; + return; + } + else if (reply == QMessageBox::No) { + reportProgress(tr("User action: Use previous download"), QtDebugMsg); + downloadSuccess = true; + return; + } + else { + reportProgress(tr("User action: Download again"), QtDebugMsg); + } + } + else if (!QDir().mkpath(f.path())) { + reportProgress(tr("Failed to create directory %1!").arg(f.path()), QtCriticalMsg); + return; + } + + download(DDT_SaveToFile, 0, url, GH_ACCEPT_HEADER_BINARY, f.absoluteFilePath()); +} + + + +bool UpdateInterface::decompressFlaggedAssets() +{ + assets->setFilterFlags(UPDFLG_Decompress); + reportProgress(tr("Asset filter applied: %1 Assets found: %2").arg(updateFlagsToString(UPDFLG_Decompress)).arg(assets->count()), QtDebugMsg); + + for (int i = 0; i < assets->count(); ++i) { + if (!decompressAsset(i)) + return false; + } + + return true; +} + +bool UpdateInterface::decompressAsset(int row) +{ + assets->getSetId(row); + + if (!(assets->flags() & UPDFLG_Decompress)) + return true; + + QString filename = assets->filename(); + + if (filename.isEmpty()) + return false; + + QFileInfo f(downloadDir % "/" % QString("A%1/%2").arg(assets->id()).arg(assets->filename())); + + if (!f.exists()) + return false; + + reportProgress(tr("Decompress: %1").arg(filename), QtInfoMsg); + + decompressArchive(f.absoluteFilePath(), decompressDir % "/" % QString("A%1").arg(assets->id())); + + return true; +} + +bool UpdateInterface::decompressArchive(const QString & archivePath, const QString & destDir) +{ + QString dest = destDir; + + if (!dest.endsWith("/")) + dest.append("/"); + + dest.append(QFileInfo(archivePath).completeBaseName()); + + MinizInterface mIface(progress, MinizInterface::PCM_SIZE, logLevel); + + if (!mIface.unzipArchiveToPath(archivePath, dest)) { + criticalMsg(tr("Failed to decompress %1").arg(archivePath)); + return false; + } + + return true; +} + +bool UpdateInterface::copyFlaggedAssets() +{ + assets->setFilterFlags(UPDFLG_CopyDest); + reportProgress(tr("Asset filter applied: %1 Assets found: %2").arg(updateFlagsToString(UPDFLG_CopyDest)).arg(assets->count()), QtDebugMsg); + + for (int i = 0; i < assets->count(); ++i) { + assets->getSetId(i); + if (!copyAsset()) + return false; + } + + return true; +} + +bool UpdateInterface::copyAsset() +{ + if (assets->flags() & UPDFLG_CopyStructure) + return copyStructure(); + else if (assets->flags() & UPDFLG_CopyFiles) + return copyFiles(); + + return true; +} + +bool UpdateInterface::copyStructure() +{ + reportProgress(tr("Copy directory structure"), QtInfoMsg); + + if (progress) { + progress->setValue(0); + progress->setMaximum(100); + } + + QDir srcPath(decompressDir % QString("/A%1/%2").arg(assets->id()).arg(QFileInfo(assets->filename()).completeBaseName())); + + if (!srcPath.exists()) { + reportProgress(tr("Decompressed file structure not found at %1").arg(QDir::toNativeSeparators(srcPath.path())), QtCriticalMsg); + return false; + } + + reportProgress(tr("Copy file structure from: %1").arg(QDir::toNativeSeparators(srcPath.path())), QtDebugMsg); + + QString baseDir = updateDir; + + if (!assets->subDirectory().isEmpty()) + baseDir.append("/" % assets->subDirectory()); + + // create directory structure first as there is no order to QDirIterator if both files and directories selected + // also supports creation of empty directories + QDirIterator itdirs(srcPath.path(), QDir::AllDirs | QDir::NoDot | QDir::NoDotDot, QDirIterator::Subdirectories); + + int cnt = 0; + + while (itdirs.hasNext()) + { + QFileInfo src(itdirs.next()); + + QString subPath(src.absoluteFilePath()); + subPath.replace(srcPath.path(), ""); + + QString dir = baseDir % subPath; + + if (!QDir().mkpath(dir)) { + reportProgress(tr("Failed to create directory %1").arg(dir), QtCriticalMsg); + return false; + } + + reportProgress(tr("Check/create directory: %1").arg(dir), QtDebugMsg); + cnt++; + } + + reportProgress(tr("Directories checked/created: %1").arg(cnt), QtInfoMsg); + + QRegularExpression filter(assets->copyFilter(), QRegularExpression::CaseInsensitiveOption); + reportProgress(tr("Copy filter pattern: %1").arg(filter.pattern()), QtDebugMsg); + + QDirIterator itcnt(srcPath.path(), QDir::Files, QDirIterator::Subdirectories); + + cnt = 0; + + reportProgress(tr("Calculating number of files"), QtDebugMsg); + + while (itcnt.hasNext()) + { + QFileInfo src(itcnt.next()); + + if ((!filter.pattern().isEmpty()) && (!src.fileName().contains(filter))) + continue; + + cnt++; + } + + if (progress) + progress->setMaximum(cnt); + + QDirIterator itfiles(srcPath.path(), QDir::Files, QDirIterator::Subdirectories); + + cnt = 0; + + while (itfiles.hasNext()) + { + QFileInfo src(itfiles.next()); + + if ((!filter.pattern().isEmpty()) && (!src.fileName().contains(filter))) + continue; + + QString subPath(src.absoluteFilePath()); + subPath.replace(srcPath.path(), ""); + + QFile dest(baseDir % subPath); + + if (dest.exists()) { + if (!dest.remove()) { + reportProgress(tr("Failed to delete existing file %1").arg(dest.fileName()), QtCriticalMsg); + return false; + } + } + + if (!QFile::copy(src.absoluteFilePath(), dest.fileName())) { + reportProgress(tr("Failed to copy file %1").arg(src.absoluteFilePath()), QtCriticalMsg); + return false; + } + + reportProgress(tr("Copied %1 to %2").arg(src.fileName()).arg(dest.fileName()), QtDebugMsg); + cnt++; + if (progress) + progress->setValue(cnt); + } + + if (progress) + progress->setValue(progress->maximum()); + + reportProgress(tr("Files copied: %1").arg(cnt), QtInfoMsg); + + return true; +} + +bool UpdateInterface::copyFiles() +{ + reportProgress(tr("Copy files"), QtInfoMsg); + + if (progress) { + progress->setValue(0); + progress->setMaximum(100); + } + + QString srcPath; + + if (assets->flags() & UPDFLG_Decompress) + srcPath = decompressDir % QString("/A%1/%2").arg(assets->id()).arg(QFileInfo(assets->filename()).completeBaseName()); + else if (assets->flags() & UPDFLG_Download) + srcPath = downloadDir % QString("/A%1").arg(assets->id()); + else { + reportProgress(tr("Unable to determine source directory for asset %1").arg(assets->filename()), QtCriticalMsg); + return false; + } + + reportProgress(tr("Copy files from %1").arg(srcPath), QtDebugMsg); + + QString destPath = updateDir; + + if (!assets->subDirectory().isEmpty()) + destPath.append("/" % assets->subDirectory()); + + if (!QDir().mkpath(destPath)) { + reportProgress(tr("Failed to create directory %1").arg(QDir::toNativeSeparators(destPath)), QtCriticalMsg); + return false; + } + + reportProgress(tr("Check/create directory: %1").arg(destPath), QtDebugMsg); + reportProgress(tr("Directories checked/created: %1").arg(1), QtDebugMsg); + + QRegularExpression filter(assets->copyFilter(), QRegularExpression::CaseInsensitiveOption); + reportProgress(tr("Copy filter pattern: %1").arg(filter.pattern()), QtDebugMsg); + + QDirIterator itcnt(srcPath, QDir::Files, QDirIterator::Subdirectories); + + int cnt = 0; + + reportProgress(tr("Calculating number of files"), QtDebugMsg); + + while (itcnt.hasNext()) + { + QFileInfo src(itcnt.next()); + + if ((!filter.pattern().isEmpty()) && (!src.fileName().contains(filter))) + continue; + + cnt++; + } + + if (progress) + progress->setMaximum(cnt); + + QDirIterator itfiles(srcPath, QDir::Files, QDirIterator::Subdirectories); + + cnt = 0; + + while (itfiles.hasNext()) + { + QFileInfo src(itfiles.next()); + + if ((!filter.pattern().isEmpty()) && (!src.fileName().contains(filter))) + continue; + + QFile dest(destPath % "/" % src.fileName()); + + if (dest.exists()) { + if (!dest.remove()) { + reportProgress(tr("Failed to delete existing file %1").arg(dest.fileName()), QtCriticalMsg); + return false; + } + reportProgress(tr("Deleted file: %1").arg(dest.fileName()), QtDebugMsg); + } + + if (!QFile::copy(src.absoluteFilePath(), dest.fileName())) { + reportProgress(tr("Failed to copy file %1").arg(src.absoluteFilePath()), QtCriticalMsg); + return false; + } + + reportProgress(tr("Copied %1 to %2").arg(src.fileName()).arg(dest.fileName()), QtDebugMsg); + cnt++; + if (progress) + progress->setValue(cnt); + } + + if (!cnt) { + reportProgress(tr("No downloaded or decompressed files matched filter '%1'").arg(assets->copyFilter()), QtCriticalMsg); + return false; + } + + if (progress) + progress->setValue(progress->maximum()); + + reportProgress(tr("Files copied: %1").arg(cnt), QtInfoMsg); + + return true; +} + +bool UpdateInterface::preparation() +{ + progressMessage(tr("Preparing")); + int cnt = 0; + + if (progress) { + progress->setValue(cnt); + progress->setMaximum(5); + } + + if (!setRunFolders()) { + reportProgress(tr("Set run folders failed"), QtDebugMsg); + return false; + } + + if (progress) + progress->setValue(++cnt); + + if (!repoReleasesMetaData()) { + return false; + } + + if (progress) + progress->setValue(++cnt); + + if (!releases->getSetId(runParams->data.updateRelease)) { + reportProgress(tr("Set release id from update release name failed"), QtDebugMsg); + return false; + } + + if (progress) + progress->setValue(++cnt); + + if (!repoReleaseAssetsMetaData()) + return false; + + if (progress) + progress->setValue(++cnt); + + if (!flagAssets()) + return false; + + if (progress) + progress->setValue(++cnt); + + return true; +} + +bool UpdateInterface::flagAssets() +{ + progressMessage(tr("Flagging assets")); + + for (int i = 0; i < runParams->data.assets.size(); i++) { + const UpdateParameters::AssetParams &ap = runParams->data.assets.at(i); + if (!getSetAssets(ap.flags, ap.filterType, ap.filter, ap.maxExpected, ap.destSubDir, ap.copyFilterType, ap.copyFilter)) + return false; + } + + return true; +} + +bool UpdateInterface::download() +{ + progressMessage(tr("Downloading assets")); + + if (!downloadFlaggedAssets()) { + reportProgress(tr("Unable to download flagged assets"), QtDebugMsg); + return false; + } + + return true; +} + +bool UpdateInterface::decompress() +{ + progressMessage(tr("Decompressing assets")); + + if (!decompressFlaggedAssets()) { + reportProgress(tr("Unable to decompress flagged assets"), QtDebugMsg); + return false; + } + + return true; +} + +bool UpdateInterface::copyToDestination() +{ + progressMessage(tr("Copying to destination")); + + if (!copyFlaggedAssets()) { + reportProgress(tr("Unable to copy assets"), QtDebugMsg); + return false; + } + + return true; +} + +bool UpdateInterface::housekeeping() +{ + progressMessage(tr("Housekeeping")); + int cnt = 0; + + if (progress) { + progress->setValue(cnt); + progress->setMaximum(3); + } + + if (!saveReleaseSettings()) { + reportProgress(tr("Failed to save release settings"), QtDebugMsg); + return false; + } + + if (progress) + progress->setValue(++cnt); + + if (runParams->data.assets.at(0).flags & UPDFLG_DelDownload) { + QDir d(downloadDir); + if (!d.removeRecursively()) + reportProgress(tr("Failed to delete downloads folder %1").arg(downloadDir), QtCriticalMsg); + } + + if (progress) + progress->setValue(++cnt); + + if (!g.decompressUseDwnld() && (runParams->data.assets.at(0).flags & UPDFLG_DelDecompress)) { + QDir d(decompressDir); + if (!d.removeRecursively()) + reportProgress(tr("Failed to delete decompress folder %1").arg(decompressDir), QtCriticalMsg); + } + + if (progress) + progress->setValue(++cnt); + + return true; +} + +bool UpdateInterface::asyncInstall() +{ + return true; +} + +bool UpdateInterface::saveReleaseSettings() +{ + g.component[settingsIdx].release(releases->name()); + g.component[settingsIdx].version(releases->version()); + g.component[settingsIdx].id(releases->id()); + g.component[settingsIdx].date(releases->date()); + + return true; +} + +/* + UpdateFactories +*/ + +UpdateFactories::UpdateFactories(QWidget * parent) : + QWidget(parent) +{ + registerUpdateFactories(); +} + +UpdateFactories::~UpdateFactories() +{ + unregisterUpdateFactories(); +} + +void UpdateFactories::registerUpdateFactory(UpdateFactoryInterface * factory) +{ + registeredUpdateFactories.append(factory); + qDebug() << "Registered update factory:" << factory->name(); +} + +void UpdateFactories::registerUpdateFactories() +{ + // Note: register in logical updating sequence + // SDCard should be first as it may clean up/erase commmon destination directory + registerUpdateFactory(new UpdateFactory(this)); + registerUpdateFactory(new UpdateFactory(this)); + registerUpdateFactory(new UpdateFactory(this)); + registerUpdateFactory(new UpdateFactory(this)); + registerUpdateFactory(new UpdateFactory(this)); + + // Note: Companion must be last as its install requires the app to be closed and thus would interrupt the update loop + registerUpdateFactory(new UpdateFactory(this)); +} + +void UpdateFactories::unregisterUpdateFactories() +{ + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) + delete factory; +} + +const UpdateParameters * const UpdateFactories::getDefaultParams(const QString & name) +{ + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + return factory->instance()->getDefaultParams(); + break; + } + } + + return nullptr; +} + +UpdateParameters * const UpdateFactories::getRunParams(const QString & name) +{ + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + return factory->instance()->getRunParams(); + break; + } + } + + return nullptr; +} + +void UpdateFactories::resetRunParams(const QString & name) +{ + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + factory->instance()->resetRunParams(); + break; + } + } +} + +void UpdateFactories::resetAllRunParams() +{ + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + factory->instance()->resetRunParams(); + } +} + +void UpdateFactories::setRunUpdate(const QString & name) +{ + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + factory->instance()->setRunUpdate(); + break; + } + } +} + +const QMap UpdateFactories::sortedComponentsList() +{ + QMap map; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + map.insert(factory->name(), factory->instance()->settingsIdx); + } + + return map; +} + +void UpdateFactories::clearRelease(const QString & name) +{ + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + factory->instance()->clearRelease(); + break; + } + } +} + +const QString UpdateFactories::currentRelease(const QString & name) +{ + QString ret; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + ret = factory->instance()->currentRelease(); + break; + } + } + + return ret; +} + +const QString UpdateFactories::updateRelease(const QString & name) +{ + QString ret; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + ret = factory->instance()->updateRelease(); + break; + } + } + + return ret; +} + +const bool UpdateFactories::isLatestRelease(const QString & name) +{ + bool ret = true; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + ret = factory->instance()->isLatestRelease(); + break; + } + } + + return ret; +} + +const QString UpdateFactories::latestRelease(const QString & name) +{ + QString ret; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + ret = factory->instance()->latestRelease(); + break; + } + } + + return ret; +} + +const QStringList UpdateFactories::releases(const QString & name) +{ + QStringList ret; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (name == factory->name()) { + ret = factory->instance()->getReleases(); + break; + } + } + + return ret; +} + +bool UpdateFactories::autoUpdate(ProgressWidget * progress) +{ + bool ok = false; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + ok = factory->instance()->autoUpdate(progress); + if (!ok) + break; + } + + return ok; +} + +bool UpdateFactories::manualUpdate(ProgressWidget * progress) +{ + bool ok = false; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + ok = factory->instance()->manualUpdate(progress); + if (!ok) + break; + } + + return ok; +} + +const bool UpdateFactories::updatesAvailable() +{ + bool ret = false; + + foreach (UpdateFactoryInterface * factory, registeredUpdateFactories) { + if (factory->instance()->isUpdateAvailable()) { + ret = true; + } + } + + return ret; +} diff --git a/companion/src/updates/updateinterface.h b/companion/src/updates/updateinterface.h new file mode 100644 index 000000000..798c860f0 --- /dev/null +++ b/companion/src/updates/updateinterface.h @@ -0,0 +1,299 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include "repomodels.h" +#include "constants.h" +#include "progresswidget.h" + +#include +#include +#include +#include + +class UpdateParameters : public QWidget +{ + Q_OBJECT + + public: + enum UpdateFilterType { + UFT_None, + UFT_Exact, + UFT_Startswith, + UFT_Endswith, + UFT_Contains, + UFT_Expression, + }; + + static QStringList updateFilterTypeList(); + + static QString updateFilterTypeToString(UpdateFilterType uft); + + struct AssetParams{ + int flags; + UpdateFilterType filterType; + QString filter; + int maxExpected; + QString destSubDir; + UpdateFilterType copyFilterType; + QString copyFilter; + }; + + struct GeneralParams{ + int flags; + QString currentRelease; + QString updateRelease; + QString downloadDir; + bool decompressUseDwnld; + QString decompressDir; + bool updateUseSD; + QString updateDir; + QVector assets; + }; + + GeneralParams data; + + UpdateParameters(QWidget * parent); + virtual ~UpdateParameters() {} + + AssetParams & addAsset(); + static QString buildFilterPattern(const UpdateFilterType filterType, const QString & filter); + + UpdateParameters& operator=(const UpdateParameters& source); +}; + +class UpdateInterface : public QWidget +{ + Q_OBJECT + + public: + + enum DownloadDataType { + DDT_Binary, + DDT_Content, + DDT_SaveToFile, + DDT_MetaData, + }; + Q_ENUM(DownloadDataType) + + enum UpdateFlags { + UPDFLG_None = 0, + UPDFLG_Update = 1 << 1, + UPDFLG_Locked = 1 << 2, + UPDFLG_Preparation = 1 << 3, + UPDFLG_Download = 1 << 4, + UPDFLG_DelDownload = 1 << 5, + UPDFLG_Decompress = 1 << 6, + UPDFLG_DelDecompress = 1 << 7, + UPDFLG_CopyDest = 1 << 8, + UPDFLG_CopyFiles = 1 << 9, + UPDFLG_CopyStructure = 1 << 10, + UPDFLG_Housekeeping = 1 << 11, + UPDFLG_AsyncInstall = 1 << 12, + UPDFLG_Common = UPDFLG_Preparation | UPDFLG_Download | UPDFLG_Decompress | UPDFLG_CopyDest | UPDFLG_Housekeeping, + }; + Q_ENUM(UpdateFlags) + + explicit UpdateInterface(QWidget * parent); + virtual ~UpdateInterface(); + + const QString getName() const { return name; } + + protected: + friend class UpdateFactories; + + ReleasesMetaData *releases; + AssetsMetaData *assets; + UpdateParameters *dfltParams; + UpdateParameters *runParams; + ProgressWidget *progress; + + QString name; + int settingsIdx; + int resultsPerPage; + QString downloadDir; + QString decompressDir; + QString updateDir; + QString fwFlavour; + QString language; + int logLevel; + + virtual bool update(); + virtual bool preparation(); + virtual bool flagAssets(); + virtual bool download(); + virtual bool decompress(); + virtual bool copyToDestination(); + virtual bool copyAsset(); + virtual bool housekeeping(); + virtual bool asyncInstall(); + + void setName(QString name); + void setRepo(QString repo) { releases->setRepo(repo); assets->setRepo(repo); } + void setResultsPerPage(int cnt) { resultsPerPage = cnt; } + void setReleasesNightlyName(QString name) { releases->setNightlyName(name); } + + void initParamFolders(UpdateParameters * params); + const UpdateParameters * const getDefaultParams() { return dfltParams; } + UpdateParameters * getRunParams() { return runParams; } + void resetRunParams(); + void setRunUpdate() { runParams->data.flags |= UPDFLG_Update; } + + const QStringList getReleases(); + + virtual const bool isUpdateAvailable(); + + virtual bool autoUpdate(ProgressWidget * progress = nullptr); + virtual bool manualUpdate(ProgressWidget * progress = nullptr); + + virtual const QString currentVersion(); + + void clearRelease(); + + virtual const QString currentRelease(); + virtual const QString updateRelease(); + virtual const bool isLatestRelease(); + virtual const bool isLatestVersion(const QString & current, const QString & latest); + virtual const QString latestRelease(); + + void getRadioProfileSettings(); + + bool repoReleasesMetaData(); + bool repoReleaseAssetsMetaData(); + + bool downloadReleasesMetaData(); + bool downloadReleaseLatestMetaData(); + bool downloadReleaseMetaData(const int releaseId); + + bool downloadReleaseAssetsMetaData(const int releaseId); + bool downloadAssetMetaData(const int assetId); + bool getSetAssets(const int flags, const UpdateParameters::UpdateFilterType assetFilterType, const QString & assetsFilter, + const int maxAssetsExpected, const QString & subDirectory, + const UpdateParameters::UpdateFilterType copyFilterType, const QString & copyFilter); + bool downloadAsset(int row); + bool downloadFlaggedAssets(); + bool decompressAsset(int row); + bool decompressFlaggedAssets(); + bool copyFlaggedAssets(); + bool copyStructure(); + bool copyFiles(); + + bool downloadTextFileToBuffer(const QString & path); + void downloadFileToBuffer(const QString & url); + QByteArray * getDownloadBuffer() { return buffer; } + + bool convertDownloadToJson(QJsonDocument * json); + + bool decompressArchive(const QString & archivePath, const QString & destPath); + bool saveReleaseSettings(); + void reportProgress(const QString & text, const int type = QtInfoMsg); + void progressMessage(const QString & text); + void criticalMsg(const QString & msg); + static QString downloadDataTypeToString(DownloadDataType val); + static QString updateFlagsToString(UpdateFlags val); + + private slots: + void onDownloadFinished(QNetworkReply * reply, DownloadDataType ddt, int subtype); + + private: + QNetworkAccessManager manager; + QNetworkRequest request; + QNetworkReply *reply; + QByteArray *buffer; + QFile *file; + QUrl url; + + bool downloadSuccess; + + static QString semanticVersion(QString version); + + int getSettingsIdx(); + void setSettingsIdx(); + bool setRunFolders(); + bool checkCreateDirectory(const QString & dirSetting, const UpdateFlags flag); + + void downloadMetaData(const MetaDataType mdt, const QString & url); + void download(const DownloadDataType type, const int subtype, const QString & urlStr, const char * header, const QString & filePath); + void downloadBinaryToFile(const QString & url, const QString & filename); + + void parseMetaData(int mdt); +}; + +class UpdateFactoryInterface +{ + public: + explicit UpdateFactoryInterface() {} + virtual ~UpdateFactoryInterface() {} + virtual UpdateInterface * instance() = 0; + virtual QString name() = 0; +}; + +template +class UpdateFactory : public UpdateFactoryInterface +{ + public: + explicit UpdateFactory(QWidget * parent) : + UpdateFactoryInterface(), + m_instance(new T(parent)) {} + + virtual ~UpdateFactory() {} + + virtual UpdateInterface * instance() { return m_instance; } + virtual QString name() { return m_instance->getName(); } + + private: + UpdateInterface *m_instance; +}; + +class UpdateFactories : public QWidget +{ + Q_OBJECT + + public: + explicit UpdateFactories(QWidget * parent = nullptr); + virtual ~UpdateFactories(); + + void registerUpdateFactory(UpdateFactoryInterface * factory); + void registerUpdateFactories(); + void unregisterUpdateFactories(); + + const UpdateParameters * const getDefaultParams(const QString & name); + UpdateParameters * const getRunParams(const QString & name); + void resetRunParams(const QString & name); + void resetAllRunParams(); + void setRunUpdate(const QString & name); + const QMap sortedComponentsList(); + + void clearRelease(const QString & name); + const QString currentRelease(const QString & name); + const QString updateRelease(const QString & name); + const bool isLatestRelease(const QString & name); + const QString latestRelease(const QString & name); + const QStringList releases(const QString & name); + + bool autoUpdate(ProgressWidget * progress = nullptr); + bool manualUpdate(ProgressWidget * progress = nullptr); + const bool updatesAvailable(); + + private: + QVector registeredUpdateFactories; +}; diff --git a/companion/src/updates/updatemultiprotocol.cpp b/companion/src/updates/updatemultiprotocol.cpp new file mode 100644 index 000000000..c9e978610 --- /dev/null +++ b/companion/src/updates/updatemultiprotocol.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updatemultiprotocol.h" + +UpdateMultiProtocol::UpdateMultiProtocol(QWidget * parent) : + UpdateInterface(parent) +{ + setName("Multiprotocol"); + setRepo(QString(GITHUB_API_REPOS).append("/pascallanger/DIY-Multiprotocol-TX-Module")); + setResultsPerPage(50); // GitHub REST API default 30 + + { + UpdateParameters::AssetParams &ap = dfltParams->addAsset(); + ap.filterType = UpdateParameters::UFT_Startswith; + ap.filter = "MultiLuaScripts"; + ap.maxExpected = 1; + ap.flags = dfltParams->data.flags | UPDFLG_Locked | UPDFLG_CopyStructure; + } + + { + UpdateParameters::AssetParams &ap = dfltParams->addAsset(); + ap.filterType = UpdateParameters::UFT_Expression; + ap.filter = "^mm-stm-serial-.*\\.bin$"; + ap.flags = (dfltParams->data.flags | UPDFLG_CopyFiles) &~ (UPDFLG_Decompress | UPDFLG_DelDecompress); + ap.destSubDir = "FIRMWARE"; + } +} diff --git a/companion/src/updates/updatemultiprotocol.h b/companion/src/updates/updatemultiprotocol.h new file mode 100644 index 000000000..91c42e2e4 --- /dev/null +++ b/companion/src/updates/updatemultiprotocol.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#pragma once + +#include "updateinterface.h" + +class UpdateMultiProtocol : public UpdateInterface +{ + Q_DECLARE_TR_FUNCTIONS(UpdateMultiProtocol) + + public: + + explicit UpdateMultiProtocol(QWidget * parent); + virtual ~UpdateMultiProtocol() {} + +}; diff --git a/companion/src/updates/updateoptionsdialog.cpp b/companion/src/updates/updateoptionsdialog.cpp new file mode 100644 index 000000000..03e2eb670 --- /dev/null +++ b/companion/src/updates/updateoptionsdialog.cpp @@ -0,0 +1,259 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updateoptionsdialog.h" +#include "ui_updateoptionsdialog.h" +#include "appdata.h" + +#include +#include +#include +#include +#include +#include + +UpdateOptionsDialog::UpdateOptionsDialog(QWidget * parent, UpdateFactories * factories, const int idx) : + QDialog(parent), + ui(new Ui::UpdateOptionsDialog), + factories(factories), + idx(idx), + name(g.component[idx].name()), + dfltParams(factories->getDefaultParams(name)), + runParams(factories->getRunParams(name)) +{ + ui->setupUi(this); + + setWindowTitle(tr("%1 %2").arg(name).arg(tr("Options"))); + + ui->leCurrent->setText(dfltParams->data.currentRelease); + + connect(ui->btnForget, &QPushButton::clicked, [=]() { + if (QMessageBox::question(this, CPN_STR_APP_NAME, tr("Clear current release information.\nWarning: There is no undo! Are you sure?"), + QMessageBox::Yes |QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { + factories->clearRelease(name); + ui->leCurrent->setText(dfltParams->data.currentRelease); + emit changed(idx); + } + }); + + ui->cboReleases->addItems(factories->releases(name)); + + int i = -1; + + QVBoxLayout *grpAssetsLayout = new QVBoxLayout(); + + foreach (const UpdateParameters::AssetParams &ap, dfltParams->data.assets) { + int flags = ap.flags; + const bool locked = flags & UpdateInterface::UPDFLG_Locked; + i++; + + QHBoxLayout *layout1 = new QHBoxLayout(); + + if (i > 0) { + QFrame *line = new QFrame(this); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + line->setLineWidth(1); + line->setMidLineWidth(0); + grpAssetsLayout->addWidget(line); + } + + QLabel *aflbl = new QLabel(tr("Filter")); + layout1->addWidget(aflbl); + + QComboBox *cboAssetFilterType = new QComboBox(); + cboAssetFilterType->setEnabled(!locked); + cboAssetFilterType->addItems(UpdateParameters::updateFilterTypeList()); + cboAssetFilterTypes << cboAssetFilterType; + layout1->addWidget(cboAssetFilterType); + + QLineEdit *leAssetFilter = new QLineEdit(); + leAssetFilter->setEnabled(!locked); + leAssetFilters << leAssetFilter; + layout1->addWidget(leAssetFilter); + + connect(leAssetFilter, &QLineEdit::editingFinished, [=]() { + QRegularExpression re(leAssetFilter->text()); + leAssetFilter->setStyleSheet((re.isValid() ? "QLineEdit { color : black; }" : "QLineEdit { color : red; }")); + }); + + connect(cboAssetFilterType, QOverload::of(&QComboBox::currentIndexChanged), [=](const int index) { + if (index == UpdateParameters::UFT_None) { + leAssetFilter->setEnabled(false); + leAssetFilter->setText(""); + } + else + leAssetFilter->setEnabled(!locked); + }); + + QSpacerItem *hsp1 = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum ); + layout1->addItem(hsp1); + + grpAssetsLayout->addLayout(layout1); + + QHBoxLayout *layout2 = new QHBoxLayout(); + + QCheckBox *chkDownload = new QCheckBox(tr("Download")); + chkDownloads << chkDownload; + layout2->addWidget(chkDownload); + + QCheckBox *chkDecompress = new QCheckBox(tr("Decompress")); + chkDecompress->setEnabled(flags & UpdateInterface::UPDFLG_Decompress); + chkDecompresses << chkDecompress; + layout2->addWidget(chkDecompress); + + QCheckBox *chkInstall = new QCheckBox(tr("Install")); + chkInstall->setEnabled(flags & UpdateInterface::UPDFLG_AsyncInstall); + chkInstalls << chkInstall; + layout2->addWidget(chkInstall); + + QSpacerItem *hsp2 = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum ); + layout2->addItem(hsp2); + + grpAssetsLayout->addLayout(layout2); + + QGridLayout *layout3 = new QGridLayout(); + + QCheckBox *chkCopy = new QCheckBox(tr("Copy")); + chkCopy->setEnabled(flags & UpdateInterface::UPDFLG_CopyDest); + chkCopies << chkCopy; + layout3->addWidget(chkCopy, 0, 0); + + QLabel *lblCopyFilter = new QLabel(tr("Filter")); + lblCopyFilter->setEnabled(flags & UpdateInterface::UPDFLG_CopyDest); + lblCopyFilters << lblCopyFilter; + layout3->addWidget(lblCopyFilter, 0, 1); + + QComboBox *cboCopyFilterType = new QComboBox(); + cboCopyFilterType->setEnabled(!locked); + cboCopyFilterType->addItems(UpdateParameters::updateFilterTypeList()); + cboCopyFilterTypes << cboCopyFilterType; + layout3->addWidget(cboCopyFilterType, 0, 2); + + QLineEdit *leCopyFilter = new QLineEdit(); + leCopyFilter->setEnabled(flags & UpdateInterface::UPDFLG_CopyDest && (!locked)); + leCopyFilters << leCopyFilter; + layout3->addWidget(leCopyFilter, 0, 3); + + connect(leCopyFilter, &QLineEdit::editingFinished, [=]() { + QRegularExpression re(leCopyFilter->text()); + leCopyFilter->setStyleSheet((re.isValid() ? "QLineEdit { color : black; }" : "QLineEdit { color : red; }")); + }); + + connect(cboCopyFilterType, QOverload::of(&QComboBox::currentIndexChanged), [=](const int index) { + if (index == UpdateParameters::UFT_None) { + leCopyFilter->setEnabled(false); + leCopyFilter->setText(""); + } + else + leCopyFilter->setEnabled(!locked); + }); + + QLabel *lblSubFolder = new QLabel(tr("Sub folder")); + lblSubFolder->setEnabled(flags & UpdateInterface::UPDFLG_CopyDest); + lblSubFolders << lblSubFolder; + layout3->addWidget(lblSubFolder, 1, 1); + + QLineEdit *leSubFolder = new QLineEdit(); + leSubFolder->setEnabled(flags & UpdateInterface::UPDFLG_CopyDest && (!locked)); + leSubFolders << leSubFolder; + layout3->addWidget(leSubFolder, 1, 2, 1, 2); + + grpAssetsLayout->addLayout(layout3); + } + + ui->grpAssets->setLayout(grpAssetsLayout); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, [=]() { + QDialog::accept(); + }); + + connect(ui->buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton * button) { + if (ui->buttonBox->standardButton(button) == QDialogButtonBox::RestoreDefaults) { + factories->resetRunParams(name); + update(); + } + else if (ui->buttonBox->standardButton(button) == QDialogButtonBox::Discard) { + QDialog::reject(); + } + else if (ui->buttonBox->standardButton(button) == QDialogButtonBox::Save) { + runParams->data.updateRelease = ui->cboReleases->currentText(); + + for (int i = 0; i < runParams->data.assets.size(); i++) { + UpdateParameters::AssetParams &ap = runParams->data.assets[i]; + + if (!(ap.flags & UpdateInterface::UPDFLG_Locked)) { + ap.filterType = (UpdateParameters::UpdateFilterType)cboAssetFilterTypes.at(i)->currentIndex(); + ap.filter = leAssetFilters.at(i)->text(); + ap.copyFilterType = (UpdateParameters::UpdateFilterType)cboCopyFilterTypes.at(i)->currentIndex(); + ap.copyFilter = leCopyFilters.at(i)->text(); + ap.destSubDir = leSubFolders.at(i)->text(); + } + + chkDownloads.at(i)->isChecked() ? ap.flags |= UpdateInterface::UPDFLG_Download : ap.flags &= ~UpdateInterface::UPDFLG_Download; + chkDecompresses.at(i)->isChecked() ? ap.flags |= UpdateInterface::UPDFLG_Decompress :ap.flags &= ~UpdateInterface::UPDFLG_Decompress; + chkInstalls.at(i)->isChecked() ? ap.flags |= UpdateInterface::UPDFLG_AsyncInstall : ap.flags &= ~UpdateInterface::UPDFLG_AsyncInstall; + chkCopies.at(i)->isChecked() ? ap.flags |= UpdateInterface::UPDFLG_CopyDest : ap.flags &= ~UpdateInterface::UPDFLG_CopyDest; + ui->chkDelDownload->isChecked() ? ap.flags |= UpdateInterface::UPDFLG_DelDownload : ap.flags &= ~UpdateInterface::UPDFLG_DelDownload; + ui->chkDelDecompress->isChecked() ? ap.flags |= UpdateInterface::UPDFLG_DelDecompress : ap.flags &= ~UpdateInterface::UPDFLG_DelDecompress; + } + + emit changed(idx); + } + }); + + update(); + + QTimer::singleShot(50, this, [=]() { + adjustSize(); + }); +} + +UpdateOptionsDialog::~UpdateOptionsDialog() +{ + delete ui; +} + +void UpdateOptionsDialog::update() +{ + ui->cboReleases->setCurrentText(runParams->data.updateRelease); + + int i = 0; + + foreach (const UpdateParameters::AssetParams &ap, runParams->data.assets) { + cboAssetFilterTypes.at(i)->setCurrentIndex((int)ap.filterType); + leAssetFilters.at(i)->setText(ap.filter); + + chkDownloads.at(i)->setChecked(ap.flags & UpdateInterface::UPDFLG_Download); + chkDecompresses.at(i)->setChecked(ap.flags & UpdateInterface::UPDFLG_Decompress); + chkInstalls.at(i)->setChecked(ap.flags & UpdateInterface::UPDFLG_AsyncInstall); + + chkCopies.at(i)->setChecked(ap.flags & UpdateInterface::UPDFLG_CopyDest); + cboCopyFilterTypes.at(i)->setCurrentIndex((int)ap.copyFilterType); + leCopyFilters.at(i)->setText(ap.copyFilter); + leSubFolders.at(i)->setText(ap.destSubDir); + + ui->chkDelDownload->setChecked(ap.flags & UpdateInterface::UPDFLG_DelDownload); + ui->chkDelDecompress->setChecked(ap.flags & UpdateInterface::UPDFLG_DelDecompress); + + i++; + } +} diff --git a/companion/src/updates/updateoptionsdialog.h b/companion/src/updates/updateoptionsdialog.h new file mode 100644 index 000000000..24c38aa14 --- /dev/null +++ b/companion/src/updates/updateoptionsdialog.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include "updateinterface.h" + +#include +#include +#include +#include +#include +#include + +namespace Ui { + class UpdateOptionsDialog; +} + +class QAbstractButton; + +class UpdateOptionsDialog : public QDialog +{ + Q_OBJECT + + public: + UpdateOptionsDialog(QWidget * parent, UpdateFactories * factories, const int idx); + virtual ~UpdateOptionsDialog(); + + signals: + void changed(int index); + + private slots: + void update(); + + private: + Ui::UpdateOptionsDialog *ui; + const UpdateFactories *factories; + const int idx; + const QString name; + const UpdateParameters *dfltParams; + UpdateParameters *runParams; + + QVector cboAssetFilterTypes; + QVector leAssetFilters; + QVector chkDownloads; + QVector chkDecompresses; + QVector chkInstalls; + QVector chkCopies; + QVector lblCopyFilters; + QVector cboCopyFilterTypes; + QVector leCopyFilters; + QVector lblSubFolders; + QVector leSubFolders; +}; diff --git a/companion/src/updates/updateoptionsdialog.ui b/companion/src/updates/updateoptionsdialog.ui new file mode 100644 index 000000000..4652c5e00 --- /dev/null +++ b/companion/src/updates/updateoptionsdialog.ui @@ -0,0 +1,171 @@ + + + UpdateOptionsDialog + + + + 0 + 0 + 677 + 393 + + + + Advanced Options + + + + + + Release + + + + + + + 0 + 0 + + + + Clear the last updated release information + + + Forget + + + + + + + + + + Current + + + + + + + Update to + + + + + + + true + + + + + + + + + + Assets + + + + + + + General + + + + + + + 0 + 0 + + + + Delete downloads + + + + + + + + 0 + 0 + + + + Delete decompressions + + + + + + + Qt::Horizontal + + + + 286 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Discard|QDialogButtonBox::RestoreDefaults|QDialogButtonBox::Save + + + + + + + + + + diff --git a/companion/src/updates/updates.cpp b/companion/src/updates/updates.cpp new file mode 100644 index 000000000..c938107ba --- /dev/null +++ b/companion/src/updates/updates.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updates.h" +#include "updateinterface.h" +#include "updatesdialog.h" +#include "progressdialog.h" +#include "progresswidget.h" +#include "appdata.h" +#include "helpers.h" + +#include + +Updates::Updates(QWidget * parent, UpdateFactories * updateFactories) : + QWidget(parent), + factories(updateFactories) +{ +} + +Updates::~Updates() +{ +} + +void Updates::autoUpdates() +{ + if (g.updateCheckFreq() == AppData::UPDATE_CHECK_MANUAL) + return; + + if (g.lastUpdateCheck().trimmed().isEmpty()) + g.lastUpdateCheck(QDateTime::currentDateTime().addDays(-60).toString(Qt::ISODate)); + + QDateTime dt = QDateTime::fromString(g.lastUpdateCheck(), Qt::ISODate); + + if (g.updateCheckFreq() == AppData::UPDATE_CHECK_DAILY) + dt = dt.addDays(1); + else if (g.updateCheckFreq() == AppData::UPDATE_CHECK_WEEKLY) + dt = dt.addDays(7); + else if (g.updateCheckFreq() == AppData::UPDATE_CHECK_MONTHLY) + dt = dt.addDays(30); + + if (dt > QDateTime::currentDateTime().toLocalTime()) { + qDebug() << "Update next due:" << dt.toString(Qt::ISODate); + return; + } + + g.lastUpdateCheck(QDateTime::currentDateTime().toString(Qt::ISODate)); + + if (!factories->updatesAvailable()) + return; + + if (QMessageBox::question(parentWidget(), CPN_STR_APP_NAME % ": " % tr("Checking for Updates"), + tr("One or more updates are available. Update now?"), + (QMessageBox::Yes | QMessageBox::No), QMessageBox::No) == QMessageBox::Yes) { + + factories->resetAllRunParams(); + factories->autoUpdate(); + } +} + +void Updates::manualUpdates() +{ + factories->resetAllRunParams(); + UpdatesDialog *dlg = new UpdatesDialog(this, factories); + + if (dlg->exec()) { + ProgressDialog progressDialog(this, tr("Update Components"), CompanionIcon("fuses.png"), true); + progressDialog.progress()->lock(true); + progressDialog.progress()->setInfo(tr("Starting...")); + bool ok = factories->manualUpdate(progressDialog.progress()); + progressDialog.progress()->lock(false); + progressDialog.progress()->setInfo(tr("Finished %1").arg(ok ? tr("successfully") : tr("with errors"))); + progressDialog.exec(); + } + + delete dlg; +} diff --git a/companion/src/releasenotesdialog.h b/companion/src/updates/updates.h similarity index 66% rename from companion/src/releasenotesdialog.h rename to companion/src/updates/updates.h index 276828003..09cec55f8 100644 --- a/companion/src/releasenotesdialog.h +++ b/companion/src/updates/updates.h @@ -1,7 +1,8 @@ /* - * Copyright (C) OpenTX + * Copyright (C) EdgeTX * * Based on code named + * opentx - https://github.com/opentx/opentx * th9x - http://code.google.com/p/th9x * er9x - http://code.google.com/p/er9x * gruvin9x - http://code.google.com/p/gruvin9x @@ -18,26 +19,24 @@ * GNU General Public License for more details. */ -#ifndef _RELEASENOTESDIALOG_H_ -#define _RELEASENOTESDIALOG_H_ +#pragma once -#include -#include "helpers.h" +#include +#include -namespace Ui { - class HtmlDialog; -} +class UpdateFactories; -class ReleaseNotesDialog : public QDialog +class Updates : public QWidget { Q_OBJECT public: - explicit ReleaseNotesDialog(QWidget *parent = 0); - ~ReleaseNotesDialog(); + explicit Updates(QWidget * parent, UpdateFactories * updateFactories); + virtual ~Updates(); + + void autoUpdates(); + void manualUpdates(); private: - Ui::HtmlDialog * ui; + UpdateFactories *factories; }; - -#endif // _RELEASENOTESDIALOG_H_ diff --git a/companion/src/updates/updatesdcard.cpp b/companion/src/updates/updatesdcard.cpp new file mode 100644 index 000000000..2927599d8 --- /dev/null +++ b/companion/src/updates/updatesdcard.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updatesdcard.h" + +#include + +UpdateSDCard::UpdateSDCard(QWidget * parent) : + UpdateInterface(parent) +{ + setName("SD Card"); + setRepo(QString(GH_REPOS_EDGETX).append("/edgetx-sdcard")); + + dfltParams->data.flags = UPDFLG_Common | UPDFLG_Locked; + + UpdateParameters::AssetParams &ap = dfltParams->addAsset(); + ap.filterType = UpdateParameters::UFT_Startswith; + ap.filter = QString("%1-").arg(fwFlavour); + ap.maxExpected = 1; + ap.flags = dfltParams->data.flags | UPDFLG_CopyStructure; +} + +bool UpdateSDCard::flagAssets() +{ + progressMessage(tr("Flagging assets")); + /* + { + "targets": [ + ["Flysky NV14", "nv14-", "nv14"], + ["FrSky Horus X10", "x10-", "horus"], + */ + + const QString mappingfile = "sdcard.json"; + + if (!downloadTextFileToBuffer(mappingfile)) { + return false; + } + + QJsonDocument *json = new QJsonDocument(); + + if (!convertDownloadToJson(json)) { + return false; + } + + UpdateParameters::AssetParams ap = runParams->data.assets.at(0); + + QRegularExpression filter(UpdateParameters::buildFilterPattern(ap.filterType, ap.filter), QRegularExpression::CaseInsensitiveOption); + bool found = false; + QString sdimage; + + if (json->isObject()) { + const QJsonObject &obj = json->object(); + + if (obj.value("targets").isArray()) { + const QJsonArray &t = obj.value("targets").toArray(); + + foreach (const QJsonValue &v, t) { + + if (v.isArray()) { + const QJsonArray &i = v.toArray(); + if (i.size() > 2) { + if (i.at(1).toString().contains(filter)) { + sdimage = i.at(2).toString(); + found = true; + break; + } + } + } + } + } + } + + if (!found) { + reportProgress(tr("Radio flavour %1 not listed in %2").arg(fwFlavour).arg(mappingfile), QtCriticalMsg); + return false; + } + + delete json; + + if (!getSetAssets(ap.flags, UpdateParameters::UFT_Startswith, sdimage, ap.maxExpected, ap.destSubDir, ap.copyFilterType, ap.copyFilter)) + return false; + + return true; +} diff --git a/companion/src/updates/updatesdcard.h b/companion/src/updates/updatesdcard.h new file mode 100644 index 000000000..c3674f6c2 --- /dev/null +++ b/companion/src/updates/updatesdcard.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include "updateinterface.h" + +class UpdateSDCard : public UpdateInterface +{ + Q_DECLARE_TR_FUNCTIONS(UpdateSDCard) + + public: + + explicit UpdateSDCard(QWidget * parent); + virtual ~UpdateSDCard() {} + + protected: + virtual bool flagAssets() override; +}; diff --git a/companion/src/updates/updatesdialog.cpp b/companion/src/updates/updatesdialog.cpp new file mode 100644 index 000000000..2878ce641 --- /dev/null +++ b/companion/src/updates/updatesdialog.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updatesdialog.h" +#include "ui_updatesdialog.h" +#include "updateoptionsdialog.h" + +#include +#include +#include +#include + +UpdatesDialog::UpdatesDialog(QWidget * parent, UpdateFactories * factories) : + QDialog(parent), + ui(new Ui::UpdatesDialog), + factories(factories) +{ + // downloading the release meta data takes a few seconds for each component so display progress + QDialog *status = new QDialog(parent); + status->setWindowTitle(tr("Progress")); + QVBoxLayout *layout = new QVBoxLayout(status); + QLabel *msg = new QLabel(status); + msg->setFixedWidth(400); + msg->setContentsMargins(50, 50, 50, 50); + layout->addWidget(msg); + status->show(); + + ui->setupUi(this); + + ui->leDownloadDir->setText(g.downloadDir()); + + connect(ui->chkDecompressUseDwnld, &QCheckBox::stateChanged, [=](const int checked) { + if (!checked) { + ui->leDecompressDir->setText(g.decompressDir()); + ui->leDecompressDir->setEnabled(true); + ui->btnDecompressSelect->setEnabled(true); + } + else { + ui->leDecompressDir->setText(g.downloadDir()); + ui->leDecompressDir->setEnabled(false); + ui->btnDecompressSelect->setEnabled(false); + } + }); + + ui->chkDecompressUseDwnld->setChecked(!g.decompressUseDwnld()); + ui->chkDecompressUseDwnld->setChecked(g.decompressUseDwnld()); + + connect(ui->chkUpdateUseSD, &QCheckBox::stateChanged, [=](const int checked) { + if (!checked) { + ui->leUpdateDir->setText(g.updateDir()); + ui->leUpdateDir->setEnabled(true); + ui->btnUpdateSelect->setEnabled(true); + } + else { + ui->leUpdateDir->setText(g.currentProfile().sdPath()); + ui->leUpdateDir->setEnabled(false); + ui->btnUpdateSelect->setEnabled(false); + } + }); + + ui->chkUpdateUseSD->setChecked(!g.updateUseSD()); + ui->chkUpdateUseSD->setChecked(g.updateUseSD()); + + connect(ui->btnDownloadSelect, &QPushButton::clicked, [=]() { + QString dirPath = QFileDialog::getExistingDirectory(this,tr("Select your download folder"), g.downloadDir()); + if (!dirPath.isEmpty()) { + ui->leDownloadDir->setText(dirPath); + } + }); + + connect(ui->btnDecompressSelect, &QPushButton::clicked, [=]() { + QString dirPath = QFileDialog::getExistingDirectory(this,tr("Select your decompress folder"), g.decompressDir()); + if (!dirPath.isEmpty()) { + ui->leDecompressDir->setText(dirPath); + } + }); + + connect(ui->btnUpdateSelect, &QPushButton::clicked, [=]() { + QString dirPath = QFileDialog::getExistingDirectory(this,tr("Select your update destination folder"), g.updateDir()); + if (!dirPath.isEmpty()) { + ui->leUpdateDir->setText(dirPath); + } + }); + + int row = 0; + int col = 0; + + QGridLayout *grid = new QGridLayout(); + + QLabel *h1 = new QLabel(); + grid->addWidget(h1, row, col++); + + QLabel *h2 = new QLabel(tr("Name")); + grid->addWidget(h2, row, col++); + + QLabel *h3 = new QLabel(tr("Channel")); + grid->addWidget(h3, row, col++); + + QLabel *h4 = new QLabel(tr("Current Release")); + grid->addWidget(h4, row, col++); + + QLabel *h5 = new QLabel(tr("Update Release")); + grid->addWidget(h5, row, col++); + + col++; // options button + + QSpacerItem * spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum ); + grid->addItem(spacer, row, col++); + + QMapIterator it(factories->sortedComponentsList()); + + while (it.hasNext()) { + it.next(); + int i = it.value(); + + const QString name = it.key(); + + if (g.component[i].checkForUpdate()) { + row++; + col = 0; + + msg->setText(tr("Retrieving latest release information for %1").arg(name)); + chkUpdate[i] = new QCheckBox(); + chkUpdate[i]->setProperty("index", i); + chkUpdate[i]->setChecked(!factories->isLatestRelease(name)); + grid->addWidget(chkUpdate[i], row, col++); + grid->setAlignment(chkUpdate[i], Qt::AlignHCenter); + + QLabel *lblName = new QLabel(name); + grid->addWidget(lblName, row, col++); + + QLabel *lblRC = new QLabel(ComponentData::releaseChannelsList().at(g.component[i].releaseChannel())); + grid->addWidget(lblRC, row, col++); + + lblCurrentRel[i] = new QLabel(factories->currentRelease(name)); + grid->addWidget(lblCurrentRel[i], row, col++); + + lblUpdateRel[i] = new QLabel(factories->updateRelease(name)); + grid->addWidget(lblUpdateRel[i], row, col++); + + btnOptions[i] = new QPushButton(tr("Options")); + connect(btnOptions[i], &QPushButton::clicked, [=]() { + UpdateOptionsDialog *dlg = new UpdateOptionsDialog(this, factories, i); + connect(dlg, &UpdateOptionsDialog::changed, [=](const int i) { + QString name = g.component[i].name(); + lblCurrentRel[i]->setText(factories->currentRelease(name)); + lblUpdateRel[i]->setText(factories->updateRelease(name)); + }); + dlg->exec(); + dlg->deleteLater(); + }); + + grid->addWidget(btnOptions[i], row, col++); + } + } + + ui->grpComponents->setLayout(grid); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, [=]() { + QMapIterator it(factories->sortedComponentsList()); + + while (it.hasNext()) { + it.next(); + int i = it.value(); + + if (g.component[i].checkForUpdate()) { + if (chkUpdate[i]->isChecked()) { + const QString name = it.key(); + UpdateParameters *runParams = factories->getRunParams(name); + runParams->data.flags |= UpdateInterface::UPDFLG_Update; + runParams->data.downloadDir = ui->leDownloadDir->text(); + runParams->data.decompressUseDwnld = ui->chkDecompressUseDwnld->isChecked(); + runParams->data.decompressDir = ui->leDecompressDir->text(); + runParams->data.updateUseSD = ui->chkUpdateUseSD->isChecked(); + runParams->data.updateDir = ui->leUpdateDir->text(); + } + } + } + + QDialog::accept(); + }); + + connect(ui->buttonBox, &QDialogButtonBox::rejected, [=]() { + QDialog::reject(); + }); + + ui->leDownloadDir->home(false); + + status->hide(); + status->deleteLater(); + + QTimer::singleShot(50, this, [=]() { + adjustSize(); + }); +} + +UpdatesDialog::~UpdatesDialog() +{ + delete ui; +} diff --git a/companion/src/fwpreferencesdialog.h b/companion/src/updates/updatesdialog.h similarity index 57% rename from companion/src/fwpreferencesdialog.h rename to companion/src/updates/updatesdialog.h index 16ab5823f..31f0e2702 100644 --- a/companion/src/fwpreferencesdialog.h +++ b/companion/src/updates/updatesdialog.h @@ -1,7 +1,8 @@ /* - * Copyright (C) OpenTX + * Copyright (C) EdgeTX * * Based on code named + * opentx - https://github.com/opentx/opentx * th9x - http://code.google.com/p/th9x * er9x - http://code.google.com/p/er9x * gruvin9x - http://code.google.com/p/gruvin9x @@ -18,35 +19,39 @@ * GNU General Public License for more details. */ -#ifndef _FWPREFERENCESDIALOG_H_ -#define _FWPREFERENCESDIALOG_H_ +#pragma once + +#include "updateinterface.h" +#include "appdata.h" #include #include -#include "eeprominterface.h" +#include +#include namespace Ui { - class FirmwarePreferencesDialog; + class UpdatesDialog; } -class Joystick; - -class FirmwarePreferencesDialog : public QDialog +class UpdatesDialog : public QDialog { Q_OBJECT public: - explicit FirmwarePreferencesDialog(QWidget *parent = 0); - ~FirmwarePreferencesDialog(); + UpdatesDialog(QWidget * parent, UpdateFactories * factories); + virtual ~UpdatesDialog(); - private: - Ui::FirmwarePreferencesDialog *ui; - void initSettings(); + signals: private slots: - void on_fw_dnld_clicked(); - void on_sd_dnld_clicked(); - void on_checkFWUpdates_clicked(); + + private: + Ui::UpdatesDialog *ui; + const UpdateFactories *factories; + + QCheckBox *chkUpdate[MAX_COMPONENTS]; + QLabel *lblCurrentRel[MAX_COMPONENTS]; + QLabel *lblUpdateRel[MAX_COMPONENTS]; + QPushButton *btnOptions[MAX_COMPONENTS]; }; -#endif // _FWPREFERENCESDIALOG_H_ diff --git a/companion/src/updates/updatesdialog.ui b/companion/src/updates/updatesdialog.ui new file mode 100644 index 000000000..37b89da68 --- /dev/null +++ b/companion/src/updates/updatesdialog.ui @@ -0,0 +1,216 @@ + + + UpdatesDialog + + + true + + + + 0 + 0 + 525 + 289 + + + + Process Updates + + + true + + + true + + + + + + Folders + + + + + + Download + + + + + + + + + + Update + + + + + + + Create sub-folders in Download folder + + + true + + + + + + + false + + + + + + + Decompress + + + + + + + Use Radio Profile SD structure + + + true + + + + + + + false + + + + + + + false + + + Select folder + + + + + + + false + + + Select folder + + + + + + + Select folder + + + + + + + + + + + 0 + 0 + + + + Components + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + UpdatesDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + UpdatesDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/companion/src/updates/updatesounds.cpp b/companion/src/updates/updatesounds.cpp new file mode 100644 index 000000000..6f5838b3c --- /dev/null +++ b/companion/src/updates/updatesounds.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "updatesounds.h" + +UpdateSounds::UpdateSounds(QWidget * parent) : + UpdateInterface(parent) +{ + setName("Sounds"); + setRepo(QString(GH_REPOS_EDGETX).append("/edgetx-sdcard-sounds")); + + /* + file: sounds.json + [ + { + "language": "cs_CZ", + "name": "Czech Female", + "description": "Czech Female Voice (cs-CZ-Vlasta)", + "directory": "cz" + }, + + TODO support multiple language sub variants + // eg edgetx-sdcard-sounds-en_gb-libby-2.7.0.zip + ap.filter = QString("^edgetx-sdcard-sounds-%1").arg(language); //return all variants of each language + */ + + UpdateParameters::AssetParams &ap = dfltParams->addAsset(); + // only supports default for each language + // eg edgetx-sdcard-sounds-en-2.7.0.zip + ap.filterType = UpdateParameters::UFT_Startswith; + ap.filter = QString("edgetx-sdcard-sounds-%1-").arg(language); + // if user changes filter then more than one could be returned + //ap.maxExpected = 1; + ap.flags = dfltParams->data.flags | UPDFLG_CopyStructure; +} diff --git a/companion/src/updates/updatesounds.h b/companion/src/updates/updatesounds.h new file mode 100644 index 000000000..9131e5206 --- /dev/null +++ b/companion/src/updates/updatesounds.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include "updateinterface.h" + +class UpdateSounds : public UpdateInterface +{ + Q_DECLARE_TR_FUNCTIONS(UpdateSounds) + + public: + + explicit UpdateSounds(QWidget * parent); + virtual ~UpdateSounds() {} + +}; diff --git a/companion/src/releasenotesfirmwaredialog.h b/companion/src/updates/updatethemes.cpp similarity index 55% rename from companion/src/releasenotesfirmwaredialog.h rename to companion/src/updates/updatethemes.cpp index 97d850767..899d44a30 100644 --- a/companion/src/releasenotesfirmwaredialog.h +++ b/companion/src/updates/updatethemes.cpp @@ -1,7 +1,8 @@ /* - * Copyright (C) OpenTX + * Copyright (C) EdgeTX * * Based on code named + * opentx - https://github.com/opentx/opentx * th9x - http://code.google.com/p/th9x * er9x - http://code.google.com/p/er9x * gruvin9x - http://code.google.com/p/gruvin9x @@ -18,32 +19,18 @@ * GNU General Public License for more details. */ -#ifndef _RELEASENOTESFIRMWAREDIALOG_H_ -#define _RELEASENOTESFIRMWAREDIALOG_H_ - -#include +#include "updatethemes.h" #include "eeprominterface.h" -#include "downloaddialog.h" -namespace Ui { - class HtmlDialog; -} - -class ReleaseNotesFirmwareDialog : public QDialog +UpdateThemes::UpdateThemes(QWidget * parent) : + UpdateInterface(parent) { - Q_OBJECT + setName("Themes"); + setRepo(QString(GH_REPOS_EDGETX).append("/themes")); - public: - explicit ReleaseNotesFirmwareDialog(QWidget *parent, const QString & rnurl); - ~ReleaseNotesFirmwareDialog(); - - private: - Ui::HtmlDialog *ui; - QNetworkAccessManager *manager; - - private slots: - void replyFinished(QNetworkReply * reply); - -}; - -#endif // _RELEASENOTESFIRMWAREDIALOG_H_ + UpdateParameters::AssetParams &ap = dfltParams->addAsset(); + ap.filterType = UpdateParameters::UFT_Startswith; + ap.filter = "edgetx-themes"; + ap.maxExpected = 1; + ap.flags = dfltParams->data.flags | UPDFLG_CopyStructure; +} diff --git a/companion/src/updates/updatethemes.h b/companion/src/updates/updatethemes.h new file mode 100644 index 000000000..f09e78d98 --- /dev/null +++ b/companion/src/updates/updatethemes.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include "updateinterface.h" + +class UpdateThemes : public UpdateInterface +{ + Q_DECLARE_TR_FUNCTIONS(UpdateThemes) + + public: + + explicit UpdateThemes(QWidget * parent); + virtual ~UpdateThemes() {} + +};