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 000000000..7574da86a Binary files /dev/null and b/companion/src/images/originals/icons Classical/download.png differ diff --git a/companion/src/images/originals/icons Monochrome/download.png b/companion/src/images/originals/icons Monochrome/download.png new file mode 100644 index 000000000..d6d7e2235 Binary files /dev/null and b/companion/src/images/originals/icons Monochrome/download.png differ 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() {} + +};