mirror of
				https://github.com/saitohirga/WSJT-X.git
				synced 2025-10-30 20:40:28 -04:00 
			
		
		
		
	Add a sample download dialog and upload sub-system
Samples are downloaded from a web server, currently the SF download server. The samples are stored in the source controlled samples directory and the CMake script there builds a suitable directory tree for upload to the web server under samples/web containing the samples hierarchy and the generated JSON contents database file. The samples CMake script also defines an 'upload-samples' target that uses rsync to efficiently upload the samples and the accompanying contents JSON database file. Any directory structure under the samples directory may be created, to add a new sample file simply add the file to source control and amend the list of sample files (SAMPLE_FILES) in samples/CMakeLists.txt. git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@6308 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
This commit is contained in:
		
							parent
							
								
									0775acf236
								
							
						
					
					
						commit
						0efe9231bb
					
				| @ -51,9 +51,11 @@ set (PROJECT_NAME "WSJT-X") | ||||
| set (PROJECT_VENDOR "Joe Taylor, K1JT") | ||||
| set (PROJECT_CONTACT "Joe Taylor <k1jt@arrl.net>") | ||||
| set (PROJECT_COPYRIGHT "Copyright (C) 2001-2015 by Joe Taylor, K1JT") | ||||
| set (PROJECT_HOMEPAGE "http://www.physics.princeton.edu/pulsar/K1JT/wsjtx.html") | ||||
| set (PROJECT_HOMEPAGE http://www.physics.princeton.edu/pulsar/K1JT/wsjtx.html) | ||||
| set (PROJECT_MANUAL wsjtx-main-${wsjtx_VERSION}.html) | ||||
| set (PROJECT_MANUAL_DIRECTORY_URL http://www.physics.princeton.edu/pulsar/K1JT/wsjtx-doc) | ||||
| set (PROJECT_SAMPLES_URL http://downloads.sourceforge.net/project/wsjt/) | ||||
| set (PROJECT_SAMPLES_UPLOAD_DEST frs.sourceforge.net:/home/frs/project/wsjt/) | ||||
| set (PROJECT_SUMMARY_DESCRIPTION "${PROJECT_NAME} - JT9 and JT65 Modes for LF, MF and HF Amateur Radio.") | ||||
| set (PROJECT_DESCRIPTION "${PROJECT_SUMMARY_DESCRIPTION} | ||||
|  ${PROJECT_NAME} implements JT9, a new mode designed especially for the LF, MF, | ||||
| @ -111,7 +113,6 @@ option (WSJT_TRACE_CAT_POLLS "Debugging option that turns on CAT diagnostics dur | ||||
| option (WSJT_HAMLIB_TRACE "Debugging option that turns on minimal Hamlib internal diagnostics.") | ||||
| option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON) | ||||
| option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.") | ||||
| option (WSJT_EMBED_SAMPLES "Embed sample files into WSJT-X resources." ON) | ||||
| option (WSJT_GENERATE_DOCS "Generate documentation files." ON) | ||||
| 
 | ||||
| CMAKE_DEPENDENT_OPTION (WSJT_HAMLIB_VERBOSE_TRACE "Debugging option that turns on full Hamlib internal diagnostics." OFF WSJT_HAMLIB_TRACE OFF) | ||||
| @ -209,6 +210,11 @@ set (wsjt_qt_CXXSRCS | ||||
|   MessageClient.cpp | ||||
|   LettersSpinBox.cpp | ||||
|   HelpTextWindow.cpp | ||||
|   SampleDownloader.cpp | ||||
|   SampleDownloader/DirectoryDelegate.cpp | ||||
|   SampleDownloader/Directory.cpp | ||||
|   SampleDownloader/FileNode.cpp | ||||
|   SampleDownloader/RemoteFile.cpp | ||||
|   ) | ||||
| 
 | ||||
| set (jt9_CXXSRCS | ||||
| @ -533,11 +539,6 @@ set (PALETTE_FILES | ||||
|   Palettes/ZL1FZ.pal | ||||
| ) | ||||
| 
 | ||||
| set (SAMPLE_FILES | ||||
|   samples/130418_1742.wav | ||||
|   samples/130610_2343.wav | ||||
|   ) | ||||
| 
 | ||||
| if (APPLE) | ||||
|   set (WSJTX_ICON_FILE ${CMAKE_PROJECT_NAME}.icns) | ||||
|   set (ICONSRCS | ||||
| @ -618,6 +619,11 @@ endif (APPLE) | ||||
| # | ||||
| find_program(CTAGS ctags) | ||||
| find_program(ETAGS etags) | ||||
| 
 | ||||
| # | ||||
| # sub-directories | ||||
| # | ||||
| add_subdirectory (samples) | ||||
| if (WSJT_GENERATE_DOCS) | ||||
|   add_subdirectory (doc) | ||||
| endif (WSJT_GENERATE_DOCS) | ||||
| @ -855,9 +861,6 @@ endfunction (add_resources resources path) | ||||
| 
 | ||||
| add_resources (wsjtx_RESOURCES "" ${TOP_LEVEL_RESOURCES}) | ||||
| add_resources (wsjtx_RESOURCES /Palettes ${PALETTE_FILES}) | ||||
| if (WSJT_EMBED_SAMPLES) | ||||
|   add_resources (wsjtx_RESOURCES /samples ${SAMPLE_FILES}) | ||||
| endif (WSJT_EMBED_SAMPLES) | ||||
| 
 | ||||
| configure_file (wsjtx.qrc.in wsjtx.qrc @ONLY) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										151
									
								
								SampleDownloader.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								SampleDownloader.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,151 @@ | ||||
| #include "SampleDownloader.hpp" | ||||
| 
 | ||||
| #include <QString> | ||||
| #include <QSettings> | ||||
| #include <QtWidgets> | ||||
| 
 | ||||
| #include "SettingsGroup.hpp" | ||||
| #include "SampleDownloader/Directory.hpp" | ||||
| 
 | ||||
| #include "pimpl_impl.hpp" | ||||
| 
 | ||||
| #include "moc_SampleDownloader.cpp" | ||||
| 
 | ||||
| namespace | ||||
| { | ||||
|   char const * const title = "Download Samples"; | ||||
| } | ||||
| 
 | ||||
| class SampleDownloader::impl final | ||||
|   : public QDialog | ||||
| { | ||||
|   Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|   explicit impl (QSettings * settings, Configuration const *, QNetworkAccessManager *, QWidget * parent); | ||||
|   ~impl () {save_window_state ();} | ||||
| 
 | ||||
|   void refresh () | ||||
|   { | ||||
|     show (); | ||||
|     raise (); | ||||
|     activateWindow (); | ||||
|     directory_.refresh (); | ||||
|   } | ||||
| 
 | ||||
| protected: | ||||
|   void closeEvent (QCloseEvent * e) override | ||||
|   { | ||||
|     save_window_state (); | ||||
|     QDialog::closeEvent (e); | ||||
|   } | ||||
| 
 | ||||
| private: | ||||
|   void save_window_state () | ||||
|   { | ||||
|     SettingsGroup g (settings_, title); | ||||
|     settings_->setValue ("geometry", saveGeometry ()); | ||||
|     settings_->setValue ("SamplesURL", url_line_edit_.text ()); | ||||
|   } | ||||
| 
 | ||||
|   Q_SLOT void button_clicked (QAbstractButton *); | ||||
| 
 | ||||
|   QSettings * settings_; | ||||
|   Directory directory_; | ||||
|   QGridLayout main_layout_; | ||||
|   QVBoxLayout left_layout_; | ||||
|   QDialogButtonBox button_box_; | ||||
|   QWidget details_widget_; | ||||
|   QFormLayout details_layout_; | ||||
|   QLineEdit url_line_edit_; | ||||
| }; | ||||
| 
 | ||||
| #include "SampleDownloader.moc" | ||||
| 
 | ||||
| SampleDownloader::SampleDownloader (QSettings * settings, Configuration const * configuration | ||||
|                                     , QNetworkAccessManager * network_manager, QWidget * parent) | ||||
|   : m_ {settings, configuration, network_manager, parent} | ||||
| { | ||||
| } | ||||
| 
 | ||||
| SampleDownloader::~SampleDownloader () | ||||
| { | ||||
| } | ||||
| 
 | ||||
| void SampleDownloader::show () | ||||
| { | ||||
|   m_->refresh (); | ||||
| } | ||||
| 
 | ||||
| SampleDownloader::impl::impl (QSettings * settings | ||||
|                               , Configuration const * configuration | ||||
|                               , QNetworkAccessManager * network_manager | ||||
|                               , QWidget * parent) | ||||
|   : QDialog {parent, Qt::Window | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint} | ||||
|   , settings_ {settings} | ||||
|   , directory_ {configuration, network_manager} | ||||
|   , button_box_ {QDialogButtonBox::Close, Qt::Vertical} | ||||
| { | ||||
|   setWindowTitle (windowTitle () + ' ' + tr (title)); | ||||
|   resize (500, 600); | ||||
|   { | ||||
|     SettingsGroup g {settings_, title}; | ||||
|     restoreGeometry (settings_->value ("geometry", saveGeometry ()).toByteArray ()); | ||||
|     url_line_edit_.setText (settings_->value ("SamplesURL", PROJECT_SAMPLES_URL).toString ()); | ||||
|     directory_.url_root (url_line_edit_.text ()); | ||||
|   } | ||||
| 
 | ||||
|   setWindowTitle (QApplication::applicationName () + " - " + tr ("Download Samples")); | ||||
| 
 | ||||
|   button_box_.button (QDialogButtonBox::Close)->setDefault (true); | ||||
|   button_box_.addButton ("&Abort", QDialogButtonBox::DestructiveRole); | ||||
|   button_box_.addButton ("&Refresh", QDialogButtonBox::ResetRole); | ||||
|   left_layout_.addWidget (&directory_); | ||||
| 
 | ||||
|   auto details_button = button_box_.addButton ("&Details", QDialogButtonBox::HelpRole); | ||||
|   details_button->setCheckable (true); | ||||
|   details_widget_.hide (); | ||||
|   details_layout_.setMargin (0); | ||||
|   details_layout_.addRow ("Base URL for samples:", &url_line_edit_); | ||||
|   details_widget_.setLayout (&details_layout_); | ||||
| 
 | ||||
|   main_layout_.addLayout (&left_layout_, 0, 0); | ||||
|   main_layout_.addWidget (&button_box_, 0, 1); | ||||
|   main_layout_.addWidget (&details_widget_, 1, 0, 1, 2); | ||||
|   main_layout_.setRowStretch (1, 2); | ||||
|   setLayout (&main_layout_); | ||||
| 
 | ||||
|   connect (&button_box_, &QDialogButtonBox::clicked, this, &SampleDownloader::impl::button_clicked); | ||||
|   connect (details_button, &QAbstractButton::clicked, &details_widget_, &QWidget::setVisible); | ||||
|   connect (&url_line_edit_, &QLineEdit::editingFinished, [this] () { | ||||
|       if (directory_.url_root (url_line_edit_.text ())) | ||||
|         { | ||||
|           directory_.refresh (); | ||||
|         } | ||||
|       else | ||||
|         { | ||||
|           QMessageBox::warning (this, "Input Error", "Invalid URL format"); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| void SampleDownloader::impl::button_clicked (QAbstractButton * button) | ||||
| { | ||||
|   switch (button_box_.buttonRole (button)) | ||||
|     { | ||||
|     case QDialogButtonBox::RejectRole: | ||||
|       hide (); | ||||
|       break; | ||||
| 
 | ||||
|     case QDialogButtonBox::DestructiveRole: | ||||
|       directory_.abort (); | ||||
|       break; | ||||
| 
 | ||||
|     case QDialogButtonBox::ResetRole: | ||||
|       directory_.refresh (); | ||||
|       break; | ||||
| 
 | ||||
|     default: | ||||
|       break; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								SampleDownloader.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								SampleDownloader.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| #ifndef SAMPLE_DOWNLOADER_HPP__ | ||||
| #define SAMPLE_DOWNLOADER_HPP__ | ||||
| 
 | ||||
| #include <QObject> | ||||
| 
 | ||||
| #include "pimpl_h.hpp" | ||||
| 
 | ||||
| class QSettings; | ||||
| class QWidget; | ||||
| class QNetworkAccessManager; | ||||
| class Configuration; | ||||
| 
 | ||||
| //
 | ||||
| // SampleDownloader - A Dialog to maintain sample files
 | ||||
| //
 | ||||
| // This  uses a  Qt Dialog  window that  contains a  tree view  of the
 | ||||
| // available sample  files on a  web or ftp  server. The files  can be
 | ||||
| // installed locally by ticking a check  box or removed from the local
 | ||||
| // machine by un-ticking the check boxes.
 | ||||
| //
 | ||||
| // The class requires a pointer to an open QSettings instance where it
 | ||||
| // will save its persistent state, a pointer to a WSJT-X Configuration
 | ||||
| // instance that is used to  obtain configuration information like the
 | ||||
| // current    file   save    location    and,   a    pointer   to    a
 | ||||
| // QNetworkAccessManager instance which is used for network requests.
 | ||||
| //
 | ||||
| // An instance  of SampleDownloader need  not be destroyed  after use,
 | ||||
| // just  call  SampleDownloader::show()  to make  the  dialog  visible
 | ||||
| // again.
 | ||||
| //
 | ||||
| class SampleDownloader final | ||||
|   : public QObject | ||||
| { | ||||
|   Q_OBJECT; | ||||
| 
 | ||||
| public: | ||||
|   SampleDownloader (QSettings * settings | ||||
|                     , Configuration const * | ||||
|                     , QNetworkAccessManager * | ||||
|                     , QWidget * parent = nullptr); | ||||
|   ~SampleDownloader (); | ||||
| 
 | ||||
|   Q_SLOT void show (); | ||||
| 
 | ||||
| private: | ||||
|   class impl; | ||||
|   pimpl<impl> m_; | ||||
| }; | ||||
| 
 | ||||
| #endif | ||||
							
								
								
									
										299
									
								
								SampleDownloader/Directory.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								SampleDownloader/Directory.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,299 @@ | ||||
| #include "Directory.hpp" | ||||
| 
 | ||||
| #include <QVariant> | ||||
| #include <QString> | ||||
| #include <QHeaderView> | ||||
| #include <QStringList> | ||||
| #include <QFileInfo> | ||||
| #include <QDir> | ||||
| #include <QNetworkAccessManager> | ||||
| #include <QAuthenticator> | ||||
| #include <QNetworkReply> | ||||
| #include <QTreeWidgetItem> | ||||
| #include <QTreeWidgetItemIterator> | ||||
| #include <QMessageBox> | ||||
| #include <QJsonDocument> | ||||
| #include <QJsonParseError> | ||||
| #include <QJsonArray> | ||||
| #include <QJsonObject> | ||||
| #include <QRegularExpression> | ||||
| 
 | ||||
| #include "Configuration.hpp" | ||||
| #include "DirectoryNode.hpp" | ||||
| #include "FileNode.hpp" | ||||
| #include "revision_utils.hpp" | ||||
| 
 | ||||
| #include "moc_Directory.cpp" | ||||
| 
 | ||||
| namespace | ||||
| { | ||||
|   char const * samples_dir_name = "samples"; | ||||
|   QString const contents_file_name = "contents_" + version (false) + ".json"; | ||||
| } | ||||
| 
 | ||||
| Directory::Directory (Configuration const * configuration | ||||
|                       , QNetworkAccessManager * network_manager | ||||
|                       , QWidget * parent) | ||||
|   : QTreeWidget {parent} | ||||
|   , configuration_ {configuration} | ||||
|   , network_manager_ {network_manager} | ||||
|   , root_dir_ {configuration_->save_directory ()} | ||||
|   , contents_ {this | ||||
|         , network_manager_ | ||||
|         , QDir {root_dir_.absoluteFilePath (samples_dir_name)}.absoluteFilePath (contents_file_name)} | ||||
| { | ||||
|   dir_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_DirClosedIcon), QIcon::Normal, QIcon::Off); | ||||
|   dir_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_DirOpenIcon), QIcon::Normal, QIcon::On); | ||||
|   file_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_FileIcon)); | ||||
| 
 | ||||
|   setColumnCount (2); | ||||
|   setHeaderLabels ({"File", "Progress"}); | ||||
|   header ()->setSectionResizeMode (QHeaderView::ResizeToContents); | ||||
|   setItemDelegate (&item_delegate_); | ||||
| 
 | ||||
|   connect (network_manager_, &QNetworkAccessManager::authenticationRequired | ||||
|            , this, &Directory::authentication); | ||||
|   connect (this, &Directory::itemChanged, [this] (QTreeWidgetItem * item) { | ||||
|       switch (item->type ()) | ||||
|         { | ||||
|         case FileNode::Type: | ||||
|           { | ||||
|             FileNode * node = static_cast<FileNode *> (item); | ||||
|             if (!node->sync (node->checkState (0) == Qt::Checked)) | ||||
|               { | ||||
|                 FileNode::sync_blocker b {node}; | ||||
|                 node->setCheckState (0, node->checkState (0) == Qt::Checked ? Qt::Unchecked : Qt::Checked); | ||||
|               } | ||||
|           } | ||||
|           break; | ||||
| 
 | ||||
|         default: | ||||
|           break; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| bool Directory::url_root (QUrl root) | ||||
| { | ||||
|   if (!root.path ().endsWith ('/')) | ||||
|     { | ||||
|       root.setPath (root.path () + '/'); | ||||
|     } | ||||
|   if (root.isValid ()) | ||||
|     { | ||||
|       url_root_ = root; | ||||
|       refresh (); | ||||
|     } | ||||
|   return root.isValid (); | ||||
| } | ||||
| 
 | ||||
| void Directory::error (QString const& title, QString const& message) | ||||
| { | ||||
|   QMessageBox::warning (this, title, message); | ||||
| } | ||||
| 
 | ||||
| bool Directory::refresh () | ||||
| { | ||||
|   abort (); | ||||
|   clear (); | ||||
|   // update locations
 | ||||
|   root_dir_ = configuration_->save_directory (); | ||||
|   QDir contents_dir {root_dir_.absoluteFilePath (samples_dir_name)}; | ||||
|   contents_.local_file_path (contents_dir.absoluteFilePath (contents_file_name)); | ||||
|   QUrl url {url_root_.resolved (QDir {root_dir_.relativeFilePath (samples_dir_name)}.filePath (contents_file_name))}; | ||||
|   if (url.isValid ()) | ||||
|     { | ||||
|       return contents_.sync (url, true, true); // attempt to fetch contents
 | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       QMessageBox::warning (this | ||||
|                             , tr ("URL Error") | ||||
|                             , tr ("Invalid URL:\n\"%1\"") | ||||
|                             .arg (url.toDisplayString ())); | ||||
|     } | ||||
|   return false; | ||||
| } | ||||
| 
 | ||||
| void Directory::download_finished (bool success) | ||||
| { | ||||
|   if (success) | ||||
|     { | ||||
|       QFile contents {contents_.local_file_path ()}; | ||||
|       if (contents.open (QFile::ReadOnly | QFile::Text)) | ||||
|         { | ||||
|           QJsonParseError json_status; | ||||
|           auto content = QJsonDocument::fromJson (contents.readAll (), &json_status); | ||||
|           if (json_status.error) | ||||
|             { | ||||
|               QMessageBox::warning (this | ||||
|                                     , tr ("JSON Error") | ||||
|                                     , tr ("Contents file syntax error %1 at character offset %2") | ||||
|                                     .arg (json_status.errorString ()).arg (json_status.offset)); | ||||
|               return; | ||||
|             } | ||||
|           if (!content.isArray ()) | ||||
|             { | ||||
|               QMessageBox::warning (this, tr ("JSON Error") | ||||
|                                     , tr ("Contents file top level must be a JSON array")); | ||||
|               return; | ||||
|             } | ||||
|           QTreeWidgetItem * parent {invisibleRootItem ()}; | ||||
|           parent = new DirectoryNode {parent, samples_dir_name}; | ||||
|           parent->setIcon (0, dir_icon_); | ||||
|           parent->setExpanded (true); | ||||
|           parse_entries (content.array (), root_dir_.relativeFilePath (samples_dir_name), parent); | ||||
|         } | ||||
|       else | ||||
|         { | ||||
|           QMessageBox::warning (this, tr ("File System Error") | ||||
|                                 , tr ("Failed to open \"%1\"\nError: %2 - %3") | ||||
|                                 .arg (contents.fileName ()) | ||||
|                                 .arg (contents.error ()) | ||||
|                                 .arg (contents.errorString ())); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void Directory::parse_entries (QJsonArray const& entries, QDir const& dir, QTreeWidgetItem * parent) | ||||
| { | ||||
|   if (dir.isRelative () && !dir.path ().startsWith ('.')) | ||||
|     { | ||||
|       for (auto const& value: entries) | ||||
|         { | ||||
|           if (value.isObject ()) | ||||
|             { | ||||
|               auto const& entry = value.toObject (); | ||||
|               auto const& name = entry["name"].toString (); | ||||
|               if (name.size () && !name.contains (QRegularExpression {R"([/:;])"})) | ||||
|                 { | ||||
|                   auto const& type = entry["type"].toString (); | ||||
|                   if ("file" == type) | ||||
|                     { | ||||
|                       QUrl url {url_root_.resolved (dir.filePath (name))}; | ||||
|                       if (url.isValid ()) | ||||
|                         { | ||||
|                           auto node = new FileNode {parent, network_manager_ | ||||
|                                                     , QDir {root_dir_.filePath (dir.path ())}.absoluteFilePath (name) | ||||
|                                                     , url}; | ||||
|                           FileNode::sync_blocker b {node}; | ||||
|                           node->setIcon (0, file_icon_); | ||||
|                           node->setCheckState (0, node->local () ? Qt::Checked : Qt::Unchecked); | ||||
|                           update (parent); | ||||
|                         } | ||||
|                       else | ||||
|                         { | ||||
|                           QMessageBox::warning (this | ||||
|                                                 , tr ("URL Error") | ||||
|                                                 , tr ("Invalid URL:\n\"%1\"") | ||||
|                                                 .arg (url.toDisplayString ())); | ||||
|                         } | ||||
|                     } | ||||
|                   else if ("directory" == type) | ||||
|                     { | ||||
|                           auto node = new DirectoryNode {parent, name}; | ||||
|                           node->setIcon (0, dir_icon_); | ||||
|                           auto const& entries = entry["entries"]; | ||||
|                           if (entries.isArray ()) | ||||
|                             { | ||||
|                               parse_entries (entries.toArray () | ||||
|                                              , QDir {root_dir_.relativeFilePath (dir.path ())}.filePath (name) | ||||
|                                              , node); | ||||
|                             } | ||||
|                           else | ||||
|                             { | ||||
|                               QMessageBox::warning (this, tr ("JSON Error") | ||||
|                                                     , tr ("Contents entries must be a JSON array")); | ||||
|                             } | ||||
|                     } | ||||
|                   else | ||||
|                     { | ||||
|                       QMessageBox::warning (this, tr ("JSON Error") | ||||
|                                             , tr ("Contents entries must have a valid type")); | ||||
|                     } | ||||
|                 } | ||||
|               else | ||||
|                 { | ||||
|                   QMessageBox::warning (this, tr ("JSON Error") | ||||
|                                         , tr ("Contents entries must have a valid name")); | ||||
|                 } | ||||
|             } | ||||
|           else | ||||
|             { | ||||
|               QMessageBox::warning (this, tr ("JSON Error") | ||||
|                                     , tr ("Contents entries must be JSON objects")); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       QMessageBox::warning (this, tr ("JSON Error") | ||||
|                             , tr ("Contents directories must be relative and within \"%1\"") | ||||
|                             .arg (samples_dir_name)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void Directory::abort () | ||||
| { | ||||
|   QTreeWidgetItemIterator iter {this}; | ||||
|   while (*iter) | ||||
|     { | ||||
|       if ((*iter)->type () == FileNode::Type) | ||||
|         { | ||||
|           auto * node = static_cast<FileNode *> (*iter); | ||||
|           node->abort (); | ||||
|         } | ||||
|       ++iter; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void Directory::update (QTreeWidgetItem * item) | ||||
| { | ||||
|   // iterate the tree under item and accumulate the progress
 | ||||
|   if (item) | ||||
|     { | ||||
|       Q_ASSERT (item->type () == DirectoryNode::Type); | ||||
|       qint64 max {0}; | ||||
|       qint64 bytes {0}; | ||||
| 
 | ||||
|       // reset progress
 | ||||
|       item->setData (1, Qt::UserRole, max); | ||||
|       item->setData (1, Qt::DisplayRole, bytes); | ||||
|       int items {0}; | ||||
|       int counted {0}; | ||||
|       QTreeWidgetItemIterator iter {item}; | ||||
|       // iterate sub tree only
 | ||||
|       while (*iter && (*iter == item || (*iter)->parent () != item->parent ())) | ||||
|         { | ||||
|           if ((*iter)->type () == FileNode::Type)  // only count files
 | ||||
|             { | ||||
|               ++items; | ||||
|               if (auto size = (*iter)->data (1, Qt::UserRole).toLongLong ()) | ||||
|                 { | ||||
|                   max += size; | ||||
|                   ++counted; | ||||
|                 } | ||||
|               bytes += (*iter)->data (1, Qt::DisplayRole).toLongLong (); | ||||
|             } | ||||
|           ++iter; | ||||
|         } | ||||
|       // estimate size of items not yet downloaded as average of
 | ||||
|       // those actually present
 | ||||
|       if (counted) | ||||
|         { | ||||
|           max += (items - counted) * max / counted; | ||||
|         } | ||||
|       // save as our progress
 | ||||
|       item->setData (1, Qt::UserRole, max); | ||||
|       item->setData (1, Qt::DisplayRole, bytes); | ||||
| 
 | ||||
|       // recurse up to top
 | ||||
|       update (item->parent ()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void Directory::authentication (QNetworkReply * /* reply */ | ||||
|                                 , QAuthenticator * /* authenticator */) | ||||
| { | ||||
|   QMessageBox::warning (this, "Network Error", "Authentication required"); | ||||
| } | ||||
							
								
								
									
										59
									
								
								SampleDownloader/Directory.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								SampleDownloader/Directory.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| #ifndef SAMPLE_DOWNLOADER_DIRECTORY_HPP__ | ||||
| #define SAMPLE_DOWNLOADER_DIRECTORY_HPP__ | ||||
| 
 | ||||
| #include <QObject> | ||||
| #include <QString> | ||||
| #include <QTreeWidget> | ||||
| #include <QIcon> | ||||
| #include <QSize> | ||||
| #include <QDir> | ||||
| #include <QUrl> | ||||
| 
 | ||||
| #include "DirectoryDelegate.hpp" | ||||
| #include "RemoteFile.hpp" | ||||
| 
 | ||||
| class Configuration; | ||||
| class QNetworkAccessManager; | ||||
| class QTreeWidgetItem; | ||||
| class QNetworkReply; | ||||
| class QAuthenticator; | ||||
| class QJsonArray; | ||||
| 
 | ||||
| class Directory final | ||||
|   : public QTreeWidget | ||||
|   , protected RemoteFile::ListenerInterface | ||||
| { | ||||
|   Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|   explicit Directory (Configuration const * configuration | ||||
|                       , QNetworkAccessManager * network_manager | ||||
|                       , QWidget * parent = nullptr); | ||||
| 
 | ||||
|   QSize sizeHint () const override {return {400, 500};} | ||||
| 
 | ||||
|   bool url_root (QUrl); | ||||
|   bool refresh (); | ||||
|   void abort (); | ||||
|   void update (QTreeWidgetItem * item); | ||||
| 
 | ||||
| protected: | ||||
|   void error (QString const& title, QString const& message) override; | ||||
|   bool redirect_request (QUrl const&) override {return true;} // allow
 | ||||
|   void download_finished (bool success) override; | ||||
| 
 | ||||
| private: | ||||
|   Q_SLOT void authentication (QNetworkReply *, QAuthenticator *); | ||||
|   void parse_entries (QJsonArray const& entries, QDir const& dir, QTreeWidgetItem * parent); | ||||
| 
 | ||||
|   Configuration const * configuration_; | ||||
|   QNetworkAccessManager * network_manager_; | ||||
|   QDir root_dir_; | ||||
|   QUrl url_root_; | ||||
|   RemoteFile contents_; | ||||
|   DirectoryDelegate item_delegate_; | ||||
|   QIcon dir_icon_; | ||||
|   QIcon file_icon_; | ||||
| }; | ||||
| 
 | ||||
| #endif | ||||
							
								
								
									
										44
									
								
								SampleDownloader/DirectoryDelegate.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								SampleDownloader/DirectoryDelegate.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| #include "DirectoryDelegate.hpp" | ||||
| 
 | ||||
| #include <QApplication> | ||||
| #include <QVariant> | ||||
| #include <QString> | ||||
| #include <QStyle> | ||||
| #include <QModelIndex> | ||||
| #include <QPainter> | ||||
| #include <QStyleOptionViewItem> | ||||
| #include <QStyleOptionProgressBar> | ||||
| 
 | ||||
| void DirectoryDelegate::paint (QPainter * painter, QStyleOptionViewItem const& option | ||||
|                                , QModelIndex const& index) const | ||||
| { | ||||
|   if (1 == index.column ()) | ||||
|     { | ||||
|       QStyleOptionProgressBar progress_bar_option; | ||||
|       progress_bar_option.rect = option.rect; | ||||
|       progress_bar_option.state = QStyle::State_Enabled; | ||||
|       progress_bar_option.direction = QApplication::layoutDirection (); | ||||
|       progress_bar_option.fontMetrics = QApplication::fontMetrics (); | ||||
|       progress_bar_option.minimum = 0; | ||||
|       progress_bar_option.maximum = 100; | ||||
|       auto progress = index.data ().toLongLong (); | ||||
|       if (progress > 0) | ||||
|         { | ||||
|           auto percent = int (progress * 100 / index.data (Qt::UserRole).toLongLong ()); | ||||
|           progress_bar_option.progress = percent; | ||||
|           progress_bar_option.text = QString::number (percent) + '%'; | ||||
|           progress_bar_option.textVisible = true; | ||||
|           progress_bar_option.textAlignment = Qt::AlignCenter; | ||||
|         } | ||||
|       else | ||||
|         { | ||||
|           // not started
 | ||||
|           progress_bar_option.progress = -1; | ||||
|         } | ||||
|       QApplication::style ()->drawControl (QStyle::CE_ProgressBar, &progress_bar_option, painter); | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       QStyledItemDelegate::paint (painter, option, index); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										30
									
								
								SampleDownloader/DirectoryDelegate.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								SampleDownloader/DirectoryDelegate.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| #ifndef DIRECTORY_DELEGATE_HPP__ | ||||
| #define DIRECTORY_DELEGATE_HPP__ | ||||
| 
 | ||||
| #include <QStyledItemDelegate> | ||||
| 
 | ||||
| class QObject; | ||||
| class QStyleOptionVoew; | ||||
| class QModelIndex; | ||||
| class QPainter; | ||||
| 
 | ||||
| //
 | ||||
| // Styled item delegate that renders a progress bar in column #1
 | ||||
| //
 | ||||
| // model column #1 DisplayRole is the progress in bytes
 | ||||
| // model column #1 UserRole is the expected number of bytes
 | ||||
| //
 | ||||
| class DirectoryDelegate final | ||||
|   : public QStyledItemDelegate | ||||
| { | ||||
| public: | ||||
|   explicit DirectoryDelegate (QObject * parent = nullptr) | ||||
|     : QStyledItemDelegate {parent} | ||||
|   { | ||||
|   } | ||||
| 
 | ||||
|   void paint (QPainter * painter, QStyleOptionViewItem const& option | ||||
|               , QModelIndex const& index) const override; | ||||
| }; | ||||
| 
 | ||||
| #endif | ||||
							
								
								
									
										51
									
								
								SampleDownloader/DirectoryNode.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								SampleDownloader/DirectoryNode.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| #ifndef DIRECTORY_NODE_HPP__ | ||||
| #define DIRECTORY_NODE_HPP__ | ||||
| 
 | ||||
| #include <QTreeWidgetItem> | ||||
| #include <QString> | ||||
| 
 | ||||
| //
 | ||||
| // Tree widget item representing a file system directory.
 | ||||
| //
 | ||||
| // It  renders the  directory name  in the  first column  and progress
 | ||||
| // information in the 2nd column. The progress information consists of
 | ||||
| // two 64 bit integer values, the 1st in the DisplayRole is the number
 | ||||
| // of  bytes received  and the  2nd in  the UserRole  the total  bytes
 | ||||
| // expected.    The   progress   information  is   not   automatically
 | ||||
| // maintained,  see the  Directory  class  for an  example  of how  to
 | ||||
| // dynamically maintain  the DirectoryNode  progress values.   The 1st
 | ||||
| // column also  renders a tristate  check box that controls  the first
 | ||||
| // column check boxes of child items.
 | ||||
| //
 | ||||
| class DirectoryNode final | ||||
|   : public QTreeWidgetItem | ||||
| { | ||||
| public: | ||||
|   explicit DirectoryNode (QTreeWidgetItem * parent, QString const& name) | ||||
|     : QTreeWidgetItem {parent, Type} | ||||
|   { | ||||
|     setFlags (flags () | Qt::ItemIsUserCheckable | Qt::ItemIsTristate); | ||||
|     setText (0, name); | ||||
|     setCheckState (0, Qt::Unchecked); | ||||
| 
 | ||||
|     // initialize as empty, the owning QTreeWidget must maintain these
 | ||||
|     // progress values
 | ||||
|     setData (1, Qt::DisplayRole, 0ll); // progress in bytes
 | ||||
|     setData (1, Qt::UserRole, 0ll);    // expected bytes
 | ||||
|   } | ||||
| 
 | ||||
|   bool operator == (QString const& name) const | ||||
|   { | ||||
|     return name == text (0); | ||||
|   } | ||||
| 
 | ||||
|   static int const Type {UserType}; | ||||
| }; | ||||
| 
 | ||||
| inline | ||||
| bool operator == (QString const& lhs, DirectoryNode const& rhs) | ||||
| { | ||||
|   return rhs == lhs; | ||||
| } | ||||
| 
 | ||||
| #endif | ||||
							
								
								
									
										73
									
								
								SampleDownloader/FileNode.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								SampleDownloader/FileNode.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| #include "FileNode.hpp" | ||||
| 
 | ||||
| #include <QVariant> | ||||
| #include <QUrl> | ||||
| #include <QDir> | ||||
| #include <QFileInfo> | ||||
| #include <QMessageBox> | ||||
| 
 | ||||
| #include "Directory.hpp" | ||||
| 
 | ||||
| FileNode::FileNode (QTreeWidgetItem * parent | ||||
|                     , QNetworkAccessManager * network_manager | ||||
|                     , QString const& local_file_path | ||||
|                     , QUrl const& url) | ||||
|   : QTreeWidgetItem {parent, Type} | ||||
|   , remote_file_ {this, network_manager, local_file_path} | ||||
|   , block_sync_ {false} | ||||
| { | ||||
|   sync_blocker b {this}; | ||||
|   setFlags (flags () | Qt::ItemIsUserCheckable); | ||||
|   setText (0, QFileInfo {local_file_path}.fileName ()); // display
 | ||||
|   setData (0, Qt::UserRole, url); | ||||
|   setData (0, Qt::UserRole + 1, local_file_path); // local absolute path
 | ||||
|   setCheckState (0, Qt::Unchecked); | ||||
| } | ||||
| 
 | ||||
| void FileNode::error (QString const& title, QString const& message) | ||||
| { | ||||
|   QMessageBox::warning (treeWidget (), title, message); | ||||
| } | ||||
| 
 | ||||
| bool FileNode::sync (bool local) | ||||
| { | ||||
|   if (block_sync_) | ||||
|     { | ||||
|       return true; | ||||
|     } | ||||
|   return remote_file_.sync (data (0, Qt::UserRole).toUrl (), local); | ||||
| } | ||||
| 
 | ||||
| void FileNode::download_progress (qint64 bytes_received, qint64 total_bytes) | ||||
| { | ||||
|   sync_blocker b {this}; | ||||
|   setData (1, Qt::UserRole, total_bytes); | ||||
|   if (bytes_received < 0) | ||||
|     { | ||||
|       setData (1, Qt::DisplayRole, 0ll); | ||||
|       setCheckState (0, Qt::Unchecked); | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       setData (1, Qt::DisplayRole, bytes_received); | ||||
|     } | ||||
|   static_cast<Directory *> (treeWidget ())->update (parent ()); | ||||
| } | ||||
| 
 | ||||
| void FileNode::download_finished (bool success) | ||||
| { | ||||
|   sync_blocker b {this}; | ||||
|   if (!success) | ||||
|     { | ||||
|       setData (1, Qt::UserRole, 0ll); | ||||
|       setData (1, Qt::DisplayRole, 0ll); | ||||
|     } | ||||
|   setCheckState (0, success ? Qt::Checked : Qt::Unchecked); | ||||
|   static_cast<Directory *> (treeWidget ())->update (parent ()); | ||||
| } | ||||
| 
 | ||||
| void FileNode::abort () | ||||
| { | ||||
|   sync_blocker b {this}; | ||||
|   remote_file_.abort (); | ||||
| } | ||||
							
								
								
									
										67
									
								
								SampleDownloader/FileNode.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								SampleDownloader/FileNode.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | ||||
| #ifndef FILE_NODE_HPP__ | ||||
| #define FILE_NODE_HPP__ | ||||
| 
 | ||||
| #include <QTreeWidgetItem> | ||||
| 
 | ||||
| #include "RemoteFile.hpp" | ||||
| 
 | ||||
| class QNetworkAccessManager; | ||||
| class QString; | ||||
| class QUrl; | ||||
| 
 | ||||
| //
 | ||||
| // A holder for a RemoteFile object linked to a QTreeWidget row.
 | ||||
| //
 | ||||
| // It  renders  the file  name  in  first  column and  holds  download
 | ||||
| // progress data in  the second column. The progress  information is a
 | ||||
| // 64 bit integer number of bytes in the DisplayRole and a total bytes
 | ||||
| // expected in the UserRole. The first column also renders a check box
 | ||||
| // that downloads  the file  when checked  and removes  the downloaded
 | ||||
| // file  when unchecked.  The URL  and  local absolute  file path  are
 | ||||
| // stored in the UserData and UserData+1 roles of the first column.
 | ||||
| //
 | ||||
| class FileNode final | ||||
|   : public QTreeWidgetItem | ||||
|   , protected RemoteFile::ListenerInterface | ||||
| { | ||||
| public: | ||||
|   explicit FileNode (QTreeWidgetItem * parent | ||||
|                      , QNetworkAccessManager * network_manager | ||||
|                      , QString const& local_path | ||||
|                      , QUrl const& url); | ||||
| 
 | ||||
|   bool local () const {return remote_file_.local ();} | ||||
|   bool sync (bool local); | ||||
|   void abort (); | ||||
| 
 | ||||
|   static int const Type {UserType + 1}; | ||||
| 
 | ||||
|   //
 | ||||
|   // Clients may  use this RAII  class to  block nested calls  to sync
 | ||||
|   // which may be troublesome, e.g. when UI updates cause recursion.
 | ||||
|   //
 | ||||
|   struct sync_blocker | ||||
|   { | ||||
|     sync_blocker (FileNode * node) : node_ {node} {node_->block_sync_ = true;} | ||||
|     sync_blocker (sync_blocker const&) = delete; | ||||
|     sync_blocker& operator = (sync_blocker const&) = delete; | ||||
|     ~sync_blocker () {node_->block_sync_ = false;} | ||||
|   private: | ||||
|     FileNode * node_; | ||||
|   }; | ||||
| 
 | ||||
| protected: | ||||
|   void error (QString const& title, QString const& message) override; | ||||
|   bool redirect_request (QUrl const&) override {return true;} // allow
 | ||||
|   void download_progress (qint64 bytes_received, qint64 total_bytes) override; | ||||
|   void download_finished (bool success) override; | ||||
| 
 | ||||
| private: | ||||
|   QNetworkAccessManager * network_manager_; | ||||
|   RemoteFile remote_file_;      // active download
 | ||||
|   bool block_sync_; | ||||
| 
 | ||||
|   friend struct sync_blocker; | ||||
| }; | ||||
| 
 | ||||
| #endif | ||||
							
								
								
									
										5
									
								
								SampleDownloader/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								SampleDownloader/README
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| A UI for downloading sample files from a web server. | ||||
| 
 | ||||
| Works in concert with  samples/CMakeLists.txt which generates the JSON | ||||
| contents description file  and has a build  target upload-samples that | ||||
| uploads the samples and content file to the project files server. | ||||
							
								
								
									
										269
									
								
								SampleDownloader/RemoteFile.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								SampleDownloader/RemoteFile.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,269 @@ | ||||
| #include "RemoteFile.hpp" | ||||
| 
 | ||||
| #include <utility> | ||||
| 
 | ||||
| #include <QNetworkAccessManager> | ||||
| #include <QNetworkReply> | ||||
| #include <QDir> | ||||
| #include <QByteArray> | ||||
| 
 | ||||
| #include "moc_RemoteFile.cpp" | ||||
| 
 | ||||
| RemoteFile::RemoteFile (ListenerInterface * listener, QNetworkAccessManager * network_manager | ||||
|                         , QString const& local_file_path, QObject * parent) | ||||
|   : QObject {parent} | ||||
|   , listener_ {listener} | ||||
|   , network_manager_ {network_manager} | ||||
|   , local_file_ {local_file_path} | ||||
|   , reply_ {nullptr} | ||||
|   , is_valid_ {false} | ||||
|   , redirect_count_ {0} | ||||
|   , file_ {local_file_path} | ||||
| { | ||||
|   local_file_.setCaching (false); | ||||
| } | ||||
| 
 | ||||
| void RemoteFile::local_file_path (QString const& name) | ||||
| { | ||||
|   QFileInfo new_file {name}; | ||||
|   new_file.setCaching (false); | ||||
|   if (new_file != local_file_) | ||||
|     { | ||||
|       if (local_file_.exists ()) | ||||
|         { | ||||
|           QFile file {local_file_.absoluteFilePath ()}; | ||||
|           if (!file.rename (new_file.absoluteFilePath ())) | ||||
|             { | ||||
|               listener_->error (tr ("File System Error") | ||||
|                                 , tr ("Cannot rename file:\n\"%1\"\nto: \"%2\"\nError(%3): %4") | ||||
|                                 .arg (file.fileName ()) | ||||
|                                 .arg (new_file.absoluteFilePath ()) | ||||
|                                 .arg (file.error ()) | ||||
|                                 .arg (file.errorString ())); | ||||
|             } | ||||
|         } | ||||
|       std::swap (local_file_, new_file); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool RemoteFile::local () const | ||||
| { | ||||
|   auto is_local = (reply_ && !reply_->isFinished ()) || local_file_.exists (); | ||||
|   if (is_local) | ||||
|     { | ||||
|       auto size = local_file_.size (); | ||||
|       listener_->download_progress (size, size); | ||||
|       listener_->download_finished (true); | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       listener_->download_progress (-1, 0); | ||||
|     } | ||||
|   return is_local; | ||||
| } | ||||
| 
 | ||||
| bool RemoteFile::sync (QUrl const& url, bool local, bool force) | ||||
| { | ||||
|   if (local) | ||||
|     { | ||||
|       if (!reply_ || reply_->isFinished ()) // not active download
 | ||||
|         { | ||||
|           if (force || !local_file_.exists () || url != url_) | ||||
|             { | ||||
|               url_ = url; | ||||
|               redirect_count_ = 0; | ||||
|               Q_ASSERT (!is_valid_); | ||||
|               download (url_); | ||||
|             } | ||||
|         } | ||||
|       else | ||||
|         { | ||||
|           return false; | ||||
|         } | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       if (reply_ && reply_->isRunning ()) | ||||
|         { | ||||
|           reply_->abort (); | ||||
|         } | ||||
|       if (local_file_.exists ()) | ||||
|         { | ||||
|           auto path = local_file_.absoluteDir (); | ||||
|           if (path.remove (local_file_.fileName ())) | ||||
|             { | ||||
|               listener_->download_progress (-1, 0); | ||||
|             } | ||||
|           else | ||||
|             { | ||||
|               listener_->error (tr ("File System Error") | ||||
|                                 , tr ("Cannot delete file:\n\"%1\"") | ||||
|                                 .arg (local_file_.absoluteFilePath ())); | ||||
|               return false; | ||||
|             } | ||||
|           path.rmpath ("."); | ||||
|         } | ||||
|     } | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| void RemoteFile::download (QUrl const& url) | ||||
| { | ||||
|   QNetworkRequest request {url}; | ||||
|   request.setRawHeader ("User-Agent", "WSJT Sample Downloader"); | ||||
|   request.setOriginatingObject (this); | ||||
| 
 | ||||
|   // this blocks for a second or two the first time it is used on
 | ||||
|   // Windows - annoying
 | ||||
|   if (!is_valid_) | ||||
|     { | ||||
|       reply_ = network_manager_->head (request); | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       reply_ = network_manager_->get (request); | ||||
|     } | ||||
| 
 | ||||
|   connect (reply_, &QNetworkReply::finished, this, &RemoteFile::reply_finished); | ||||
|   connect (reply_, &QNetworkReply::readyRead, this, &RemoteFile::store); | ||||
|   connect (reply_, &QNetworkReply::downloadProgress | ||||
|            , [this] (qint64 bytes_received, qint64 total_bytes) { | ||||
|              // report progress of wanted file
 | ||||
|              if (is_valid_) | ||||
|                { | ||||
|                  listener_->download_progress (bytes_received, total_bytes); | ||||
|                } | ||||
|            }); | ||||
|   connect (reply_, &QNetworkReply::sslErrors, [this] (QList<QSslError> const& errors) { | ||||
|       QString message; | ||||
|       for (auto const& error: errors) | ||||
|         { | ||||
|           message += '\n' + reply_->request ().url ().toDisplayString () + ": " | ||||
|             + error.errorString (); | ||||
|         } | ||||
|       listener_->error ("Network SSL Errors", message); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| void RemoteFile::abort () | ||||
| { | ||||
|   if (reply_ && reply_->isRunning ()) | ||||
|     { | ||||
|       reply_->abort (); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void RemoteFile::reply_finished () | ||||
| { | ||||
|   auto saved_reply = reply_; | ||||
|   auto redirect_url = reply_->attribute (QNetworkRequest::RedirectionTargetAttribute).toUrl (); | ||||
|   if (!redirect_url.isEmpty ()) | ||||
|     { | ||||
|       if (listener_->redirect_request (redirect_url)) | ||||
|         { | ||||
|           if (++redirect_count_ < 10) // maintain sanity
 | ||||
|             { | ||||
|               // follow redirect
 | ||||
|               download (reply_->url ().resolved (redirect_url)); | ||||
|             } | ||||
|           else | ||||
|             { | ||||
|               listener_->download_finished (false); | ||||
|               listener_->error (tr ("Network Error") | ||||
|                                 , tr ("Too many redirects: %1") | ||||
|                                 .arg (redirect_url.toDisplayString ())); | ||||
|               is_valid_ = false; // reset
 | ||||
|             } | ||||
|         } | ||||
|       else | ||||
|         { | ||||
|           listener_->download_finished (false); | ||||
|           listener_->error (tr ("Network Error") | ||||
|                             , tr ("Redirect not followed: %1") | ||||
|                             .arg (redirect_url.toDisplayString ())); | ||||
|           is_valid_ = false;    // reset
 | ||||
|         } | ||||
|     } | ||||
|   else if (reply_->error () != QNetworkReply::NoError) | ||||
|     { | ||||
|       file_.cancelWriting (); | ||||
|       file_.commit (); | ||||
|       listener_->download_finished (false); | ||||
|       is_valid_ = false;        // reset
 | ||||
|       // report errors that are not due to abort
 | ||||
|       if (QNetworkReply::OperationCanceledError != reply_->error ()) | ||||
|         { | ||||
|           listener_->error (tr ("Network Error"), reply_->errorString ()); | ||||
|         } | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       auto path = QFileInfo {file_.fileName ()}.absoluteDir (); | ||||
|       if (is_valid_ && !file_.commit ()) | ||||
|         { | ||||
|           listener_->error (tr ("File System Error") | ||||
|                             , tr ("Cannot commit changes to:\n\"%1\"") | ||||
|                             .arg (file_.fileName ())); | ||||
|           path.rmpath (".");    // tidy empty directories
 | ||||
|           listener_->download_finished (false); | ||||
|           is_valid_ = false;    // reset
 | ||||
|         } | ||||
|       else | ||||
|         { | ||||
|           if (!is_valid_) | ||||
|             { | ||||
|               // now get the body content
 | ||||
|               is_valid_ = true; | ||||
|               download (reply_->url () .resolved (redirect_url)); | ||||
|             } | ||||
|           else | ||||
|             { | ||||
|               listener_->download_finished (true); | ||||
|               is_valid_ = false; // reset
 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   if (reply_->isFinished ()) reply_ = nullptr; | ||||
|   disconnect (saved_reply); | ||||
|   saved_reply->deleteLater ();  // finished with QNetworkReply
 | ||||
| } | ||||
| 
 | ||||
| void RemoteFile::store () | ||||
| { | ||||
|   if (is_valid_) | ||||
|     { | ||||
|       if (!file_.isOpen ()) | ||||
|         { | ||||
|           // create temporary file in the final location
 | ||||
|           auto path = QFileInfo {file_.fileName ()}.absoluteDir (); | ||||
|           if (path.mkpath (".")) | ||||
|             { | ||||
|               if (!file_.open (QSaveFile::WriteOnly)) | ||||
|                 { | ||||
|                   abort (); | ||||
|                   listener_->error (tr ("File System Error") | ||||
|                                     , tr ("Cannot open file:\n\"%1\"\nError(%2): %3") | ||||
|                                     .arg (path.path ()) | ||||
|                                     .arg (file_.error ()) | ||||
|                                     .arg (file_.errorString ())); | ||||
|                 } | ||||
|             } | ||||
|           else | ||||
|             { | ||||
|               abort (); | ||||
|               listener_->error (tr ("File System Error") | ||||
|                                 , tr ("Cannot make path:\n\"%1\"") | ||||
|                                 .arg (path.path ())); | ||||
|             } | ||||
|         } | ||||
|       if (file_.write (reply_->read (reply_->bytesAvailable ())) < 0) | ||||
|         { | ||||
|           abort (); | ||||
|           listener_->error (tr ("File System Error") | ||||
|                             , tr ("Cannot write to file:\n\"%1\"\nError(%2): %3") | ||||
|                             .arg (file_.fileName ()) | ||||
|                             .arg (file_.error ()) | ||||
|                             .arg (file_.errorString ())); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										78
									
								
								SampleDownloader/RemoteFile.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								SampleDownloader/RemoteFile.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| #ifndef REMOTE_FILE_HPP__ | ||||
| #define REMOTE_FILE_HPP__ | ||||
| 
 | ||||
| #include <QObject> | ||||
| #include <QString> | ||||
| #include <QUrl> | ||||
| #include <QFileInfo> | ||||
| #include <QSaveFile> | ||||
| 
 | ||||
| class QNetworkAccessManager; | ||||
| class QNetworkReply; | ||||
| 
 | ||||
| //
 | ||||
| // Synchronize an individual file specified by a URL to the local file
 | ||||
| // system
 | ||||
| //
 | ||||
| class RemoteFile final | ||||
|   : public QObject | ||||
| { | ||||
|   Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|   //
 | ||||
|   // Clients  of   RemoteFile  must   provide  an  instance   of  this
 | ||||
|   // interface. It  may be  used to  receive information  and requests
 | ||||
|   // from the RemoteFile instance as it does its work.
 | ||||
|   //
 | ||||
|   class ListenerInterface | ||||
|   { | ||||
|   protected: | ||||
|     ListenerInterface () {} | ||||
| 
 | ||||
|   public: | ||||
|     virtual void error (QString const& title, QString const& message) = 0; | ||||
|     virtual bool redirect_request (QUrl const&) {return false;} // disallow
 | ||||
|     virtual void download_progress (qint64 /* bytes_received */, qint64 /* total_bytes */) {} | ||||
|     virtual void download_finished (bool /* success */) {} | ||||
|   }; | ||||
| 
 | ||||
|   explicit RemoteFile (ListenerInterface * listener, QNetworkAccessManager * network_manager | ||||
|                        , QString const& local_file_path, QObject * parent = nullptr); | ||||
| 
 | ||||
|   // true if local file exists or will do very soon
 | ||||
|   bool local () const; | ||||
| 
 | ||||
|   // download/remove the local file
 | ||||
|   bool sync (QUrl const& url, bool local = true, bool force = false); | ||||
| 
 | ||||
|   // abort an active download
 | ||||
|   void abort (); | ||||
| 
 | ||||
|   // change the local location, this will rename if the file exists locally
 | ||||
|   void local_file_path (QString const&); | ||||
| 
 | ||||
|   QString local_file_path () const {return local_file_.absoluteFilePath ();} | ||||
|   QUrl url () const {return url_;} | ||||
| 
 | ||||
| private: | ||||
|   void download (QUrl const& url); | ||||
|   void reply_finished (); | ||||
| 
 | ||||
|   Q_SLOT void store (); | ||||
| 
 | ||||
|   Q_SIGNAL void redirect (QUrl const&, unsigned redirect_count); | ||||
|   Q_SIGNAL void downloadProgress (qint64 bytes_received, qint64 total_bytes); | ||||
|   Q_SIGNAL void finished (); | ||||
| 
 | ||||
|   ListenerInterface * listener_; | ||||
|   QNetworkAccessManager * network_manager_; | ||||
|   QFileInfo local_file_; | ||||
|   QUrl url_; | ||||
|   QNetworkReply * reply_; | ||||
|   bool is_valid_; | ||||
|   unsigned redirect_count_; | ||||
|   QSaveFile file_; | ||||
| }; | ||||
| 
 | ||||
| #endif | ||||
							
								
								
									
										3
									
								
								main.cpp
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								main.cpp
									
									
									
									
									
								
							| @ -7,6 +7,7 @@ | ||||
| 
 | ||||
| #include <QDateTime> | ||||
| #include <QApplication> | ||||
| #include <QNetworkAccessManager> | ||||
| #include <QRegularExpression> | ||||
| #include <QObject> | ||||
| #include <QSettings> | ||||
| @ -206,7 +207,7 @@ int main(int argc, char *argv[]) | ||||
|                                            ).toBool () ? 1u : 4u; | ||||
|       } | ||||
| 
 | ||||
|       MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor); | ||||
|       MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor, new QNetworkAccessManager {&a}); | ||||
|       w.show(); | ||||
| 
 | ||||
|       QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit())); | ||||
|  | ||||
| @ -47,6 +47,7 @@ | ||||
| #include "wsprnet.h" | ||||
| #include "signalmeter.h" | ||||
| #include "HelpTextWindow.hpp" | ||||
| #include "SampleDownloader.hpp" | ||||
| 
 | ||||
| #include "ui_mainwindow.h" | ||||
| #include "moc_mainwindow.cpp" | ||||
| @ -130,7 +131,8 @@ namespace | ||||
| 
 | ||||
| //--------------------------------------------------- MainWindow constructor
 | ||||
| MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdmem, | ||||
|                        unsigned downSampleFactor, QWidget *parent) : | ||||
|                        unsigned downSampleFactor, QNetworkAccessManager * network_manager, | ||||
|                        QWidget *parent) : | ||||
|   QMainWindow(parent), | ||||
|   m_dataDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}, | ||||
|   m_revision {revision ()}, | ||||
| @ -327,6 +329,14 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme | ||||
|   ui->actionInclude_averaging->setActionGroup(DepthGroup); | ||||
|   ui->actionInclude_correlation->setActionGroup(DepthGroup); | ||||
| 
 | ||||
|   connect (ui->download_samples_action, &QAction::triggered, [this, network_manager] () { | ||||
|       if (!m_sampleDownloader) | ||||
|         { | ||||
|           m_sampleDownloader.reset (new SampleDownloader {m_settings, &m_config, network_manager, this}); | ||||
|         } | ||||
|       m_sampleDownloader->show (); | ||||
|     }); | ||||
| 
 | ||||
|   QButtonGroup* txMsgButtonGroup = new QButtonGroup; | ||||
|   txMsgButtonGroup->addButton(ui->txrb1,1); | ||||
|   txMsgButtonGroup->addButton(ui->txrb2,2); | ||||
| @ -669,7 +679,7 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme | ||||
|   progressBar->setMaximum(m_TRperiod); | ||||
|   m_modulator->setPeriod(m_TRperiod); // TODO - not thread safe
 | ||||
|   m_dialFreqRxWSPR=0; | ||||
|   wsprNet = new WSPRNet(this); | ||||
|   wsprNet = new WSPRNet(network_manager, this); | ||||
|   connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString))); | ||||
|   if(m_bFastMode) { | ||||
|     int ntr[]={5,10,15,30}; | ||||
|  | ||||
| @ -49,6 +49,7 @@ namespace Ui { | ||||
| } | ||||
| 
 | ||||
| class QSettings; | ||||
| class QNetworkAccessManager; | ||||
| class QLineEdit; | ||||
| class QFont; | ||||
| class QHostInfo; | ||||
| @ -68,6 +69,7 @@ class SoundOutput; | ||||
| class Modulator; | ||||
| class SoundInput; | ||||
| class Detector; | ||||
| class SampleDownloader; | ||||
| 
 | ||||
| class MainWindow : public QMainWindow | ||||
| { | ||||
| @ -79,7 +81,8 @@ public: | ||||
| 
 | ||||
|   // Multiple instances: call MainWindow() with *thekey
 | ||||
|   explicit MainWindow(bool multiple, QSettings *, QSharedMemory *shdmem, | ||||
|                       unsigned downSampleFactor, QWidget *parent = 0); | ||||
|                       unsigned downSampleFactor, QNetworkAccessManager * network_manager, | ||||
|                       QWidget *parent = 0); | ||||
|   ~MainWindow(); | ||||
| 
 | ||||
| public slots: | ||||
| @ -283,6 +286,7 @@ private: | ||||
|   WSPRBandHopping m_WSPR_band_hopping; | ||||
|   bool m_WSPR_tx_next; | ||||
|   QMessageBox m_rigErrorMessageBox; | ||||
|   QScopedPointer<SampleDownloader> m_sampleDownloader; | ||||
| 
 | ||||
|   QScopedPointer<WideGraph> m_wideGraph; | ||||
|   QScopedPointer<EchoGraph> m_echoGraph; | ||||
|  | ||||
| @ -2351,6 +2351,7 @@ QPushButton[state="ok"] { | ||||
|     </property> | ||||
|     <addaction name="actionOnline_User_Guide"/> | ||||
|     <addaction name="actionLocal_User_Guide"/> | ||||
|     <addaction name="download_samples_action"/> | ||||
|     <addaction name="separator"/> | ||||
|     <addaction name="actionKeyboard_shortcuts"/> | ||||
|     <addaction name="actionSpecial_mouse_commands"/> | ||||
| @ -2816,6 +2817,14 @@ QPushButton[state="ok"] { | ||||
|     <string>JTMSK</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="download_samples_action"> | ||||
|    <property name="text"> | ||||
|     <string>&Download Samples ...</string> | ||||
|    </property> | ||||
|    <property name="whatsThis"> | ||||
|     <string><html><head/><body><p>Download sample audio files demonstrating the various modes.</p></body></html></string> | ||||
|    </property> | ||||
|   </action> | ||||
|  </widget> | ||||
|  <layoutdefault spacing="6" margin="11"/> | ||||
|  <customwidgets> | ||||
|  | ||||
| @ -76,6 +76,21 @@ QString font_as_stylesheet (QFont const&); | ||||
| // conditional style sheet updates
 | ||||
| void update_dynamic_property (QWidget *, char const * property, QVariant const& value); | ||||
| 
 | ||||
| template <class T> | ||||
| class VPtr | ||||
| { | ||||
| public: | ||||
|   static T * asPtr (QVariant v) | ||||
|   { | ||||
|     return reinterpret_cast<T *> (v.value<void *> ()); | ||||
|   } | ||||
| 
 | ||||
|   static QVariant asQVariant(T * ptr) | ||||
|   { | ||||
|     return qVariantFromValue (reinterpret_cast<void *> (ptr)); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // Register some useful Qt types with QMetaType
 | ||||
| Q_DECLARE_METATYPE (QHostAddress); | ||||
| 
 | ||||
|  | ||||
| @ -63,13 +63,18 @@ QString revision (QString const& svn_rev_string) | ||||
|   return result.trimmed (); | ||||
| } | ||||
| 
 | ||||
| QString version () | ||||
| QString version (bool include_patch) | ||||
| { | ||||
| #if defined (CMAKE_BUILD) | ||||
|   QString v {WSJTX_STRINGIZE (WSJTX_VERSION_MAJOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_MINOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_PATCH)}; | ||||
|   QString v {WSJTX_STRINGIZE (WSJTX_VERSION_MAJOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_MINOR)}; | ||||
|   if (include_patch) | ||||
|     { | ||||
|       v += "." WSJTX_STRINGIZE (WSJTX_VERSION_PATCH) | ||||
| # if defined (WSJTX_RC) | ||||
|   v += "-rc" WSJTX_STRINGIZE (WSJTX_RC); | ||||
|         + "-rc" WSJTX_STRINGIZE (WSJTX_RC) | ||||
| # endif | ||||
|         ; | ||||
|     } | ||||
| #else | ||||
|   QString v {"Not for Release"}; | ||||
| #endif | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| #include <QString> | ||||
| 
 | ||||
| QString revision (QString const& svn_rev_string = QString {}); | ||||
| QString version (); | ||||
| QString version (bool include_patch = true); | ||||
| QString program_title (QString const& revision = QString {}); | ||||
| 
 | ||||
| #endif | ||||
|  | ||||
							
								
								
									
										122
									
								
								samples/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								samples/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,122 @@ | ||||
| set (SAMPLE_FILES | ||||
|   JT9+JT65/130610_2343.wav | ||||
|   JT9/130418_1742.wav | ||||
|   ) | ||||
| 
 | ||||
| #set_directory_properties (PROPERTIES EXCLUDE_FROM_ALL ON) | ||||
| 
 | ||||
| set (contents_file_ ${CMAKE_CURRENT_BINARY_DIR}/contents_${WSJTX_VERSION_MAJOR}.${WSJTX_VERSION_MINOR}.json) | ||||
| 
 | ||||
| function (indent_) | ||||
|   foreach (temp_ RANGE ${level_}) | ||||
|     file (APPEND ${contents_file_} "  ") | ||||
|   endforeach () | ||||
| endfunction () | ||||
| 
 | ||||
| function (end_entry_) | ||||
|   file (APPEND ${contents_file_} "\n") | ||||
|   set(first_ 0 PARENT_SCOPE) | ||||
|   math (EXPR level_ "${level_} - 1") | ||||
|   indent_ () | ||||
|   file (APPEND ${contents_file_} "]\n") | ||||
|   math (EXPR level_ "${level_} - 2") | ||||
|   indent_ () | ||||
|   file (APPEND ${contents_file_} "}") | ||||
|   string (FIND "${dirs_}" "${cwd_}" pos_) | ||||
|   set (level_ ${level_} PARENT_SCOPE) | ||||
| endfunction () | ||||
| 
 | ||||
| file (WRITE ${contents_file_} "[") | ||||
| 
 | ||||
| set (cwd_) | ||||
| set (level_ 0) | ||||
| set (first_ 1) | ||||
| list (SORT SAMPLE_FILES) | ||||
| foreach (sample_ IN LISTS SAMPLE_FILES) | ||||
|   string (REGEX MATCHALL "[^/]*/" dirs_ "${sample_}") | ||||
|   string (REPLACE "/" "" dirs_ "${dirs_}") | ||||
|   string (REGEX MATCH "[^/]*$" name_ "${sample_}") | ||||
|   string (FIND "${dirs_}" "${cwd_}" pos_) | ||||
|   list (LENGTH cwd_ cwd_count_) | ||||
|   if (${pos_} EQUAL 0) | ||||
|     # same root | ||||
|     while (${cwd_count_} GREATER 0) | ||||
|       list (REMOVE_AT dirs_ 0) | ||||
|       math (EXPR cwd_count_ "${cwd_count_} - 1") | ||||
|     endwhile () | ||||
|   else () | ||||
|     # reduce cwd_ until matched | ||||
|     while ((NOT ${pos_} EQUAL 0) AND ${cwd_count_} GREATER 0) | ||||
|       math (EXPR cwd_count_ "${cwd_count_} - 1") | ||||
|       list (REMOVE_AT cwd_ ${cwd_count_}) | ||||
|       end_entry_ () | ||||
|     endwhile () | ||||
|     # back to same root | ||||
|     while (${cwd_count_} GREATER 0) | ||||
|       list (REMOVE_AT dirs_ 0) | ||||
|       math (EXPR cwd_count_ "${cwd_count_} - 1") | ||||
|     endwhile () | ||||
|   endif () | ||||
|   list (LENGTH cwd_ cwd_count_) | ||||
|   list (LENGTH dirs_ path_count_) | ||||
|   while (${path_count_} GREATER 0) | ||||
|     list (GET dirs_ 0 dir_) | ||||
|     list (APPEND cwd_ "${dir_}") | ||||
|     list (REMOVE_AT dirs_ 0) | ||||
|     if (${first_}) | ||||
|       file (APPEND ${contents_file_} "\n") | ||||
|       set (first 0) | ||||
|     else () | ||||
|       file (APPEND ${contents_file_} ",\n") | ||||
|     endif () | ||||
|     indent_ () | ||||
|     file (APPEND ${contents_file_} "{\n") | ||||
|     math (EXPR level_ "${level_} + 1") | ||||
|     indent_ () | ||||
|     file (APPEND ${contents_file_} "\"type\": \"directory\",\n") | ||||
|     indent_ () | ||||
|     file (APPEND ${contents_file_} "\"name\":  \"${dir_}\",\n") | ||||
|     indent_ () | ||||
|     file (APPEND ${contents_file_} "\"entries\": [") | ||||
|     set (first_ 1) | ||||
|     math (EXPR level_ "${level_} + 2") | ||||
|     math (EXPR path_count_ "${path_count_} - 1") | ||||
|   endwhile () | ||||
|   file (COPY ${sample_} DESTINATION web/samples/${cwd_}) | ||||
|   if (${first_}) | ||||
|     file (APPEND ${contents_file_} "\n") | ||||
|     set (first 0) | ||||
|   else () | ||||
|     file (APPEND ${contents_file_} ",\n") | ||||
|   endif () | ||||
|   indent_ () | ||||
|   file (APPEND ${contents_file_} "{\n") | ||||
|   math (EXPR level_ "${level_} + 1") | ||||
|   indent_ () | ||||
|   file (APPEND ${contents_file_} "\"type\": \"file\",\n") | ||||
|   indent_ () | ||||
|   file (APPEND ${contents_file_} "\"name\": \"${name_}\"\n") | ||||
|   math (EXPR level_ "${level_} - 1") | ||||
|   indent_ () | ||||
|   file (APPEND ${contents_file_} "}") | ||||
|   set (first_ 0) | ||||
| endforeach () | ||||
| if (${level_} GREATER 1) | ||||
|   end_entry_ () | ||||
| endif () | ||||
| 
 | ||||
| file (APPEND ${contents_file_} "\n]\n") | ||||
| 
 | ||||
| file (COPY ${contents_file_} DESTINATION web/samples) | ||||
| 
 | ||||
| find_program (RSYNC_EXECUTABLE rsync) | ||||
| if (RSYNC_EXECUTABLE) | ||||
|   add_custom_command ( | ||||
|     OUTPUT upload.timestamp | ||||
|     COMMAND ${RSYNC_EXECUTABLE} ARGS -avz ${CMAKE_CURRENT_BINARY_DIR}/web/samples ${PROJECT_SAMPLES_UPLOAD_DEST} | ||||
|     COMMAND ${CMAKE_COMMAND} ARGS touch upload.timestamp | ||||
|     DEPENDS ${contents_file_} ${SAMPLE_FILES} | ||||
|     COMMENT "Uploading WSJT-X samples to web server" | ||||
|     ) | ||||
|   add_custom_target (upload-samples DEPENDS upload.timestamp) | ||||
| endif () | ||||
| @ -16,6 +16,7 @@ | ||||
| #cmakedefine WSJT_DATA_DESTINATION "@WSJT_DATA_DESTINATION@" | ||||
| #cmakedefine PROJECT_MANUAL "@PROJECT_MANUAL@" | ||||
| #cmakedefine PROJECT_MANUAL_DIRECTORY_URL "@PROJECT_MANUAL_DIRECTORY_URL@" | ||||
| #cmakedefine PROJECT_SAMPLES_URL "@PROJECT_SAMPLES_URL@" | ||||
| 
 | ||||
| #cmakedefine01 WSJT_SHARED_RUNTIME | ||||
| #cmakedefine01 WSJT_QDEBUG_TO_FILE | ||||
|  | ||||
							
								
								
									
										50
									
								
								wsprnet.cpp
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								wsprnet.cpp
									
									
									
									
									
								
							| @ -22,9 +22,9 @@ namespace | ||||
|   // char const * const wsprNetUrl = "http://127.0.0.1/post?";
 | ||||
| }; | ||||
| 
 | ||||
| WSPRNet::WSPRNet(QObject *parent) | ||||
| WSPRNet::WSPRNet(QNetworkAccessManager * manager, QObject *parent) | ||||
|   : QObject{parent} | ||||
|   , networkManager {new QNetworkAccessManager {this}} | ||||
|   , networkManager {manager} | ||||
|   , uploadTimer {new QTimer {this}} | ||||
|   , m_urlQueueSize {0} | ||||
| { | ||||
| @ -74,33 +74,35 @@ void WSPRNet::upload(QString const& call, QString const& grid, QString const& rf | ||||
| 
 | ||||
| void WSPRNet::networkReply(QNetworkReply *reply) | ||||
| { | ||||
|   if (QNetworkReply::NoError != reply->error ()) { | ||||
|     Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ())); | ||||
|     // not clearing queue or halting queuing as it may be a transient
 | ||||
|     // one off request error
 | ||||
|   } | ||||
|   else { | ||||
|     QString serverResponse = reply->readAll(); | ||||
|     if( m_uploadType == 2) { | ||||
|       if (!serverResponse.contains(QRegExp("spot\\(s\\) added"))) { | ||||
|         emit uploadStatus("Upload Failed"); | ||||
|         urlQueue.clear(); | ||||
|   // check if request was ours
 | ||||
|   if (m_outstandingRequests.removeOne (reply)) { | ||||
|     if (QNetworkReply::NoError != reply->error ()) { | ||||
|       Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ())); | ||||
|       // not clearing queue or halting queuing as it may be a transient
 | ||||
|       // one off request error
 | ||||
|     } | ||||
|     else { | ||||
|       QString serverResponse = reply->readAll(); | ||||
|       if( m_uploadType == 2) { | ||||
|         if (!serverResponse.contains(QRegExp("spot\\(s\\) added"))) { | ||||
|           emit uploadStatus("Upload Failed"); | ||||
|           urlQueue.clear(); | ||||
|           uploadTimer->stop(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (urlQueue.isEmpty()) { | ||||
|         emit uploadStatus("done"); | ||||
|         QFile::remove(m_file); | ||||
|         uploadTimer->stop(); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (urlQueue.isEmpty()) { | ||||
|       emit uploadStatus("done"); | ||||
|       QFile::remove(m_file); | ||||
|       uploadTimer->stop(); | ||||
|     } | ||||
|     qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ()); | ||||
| 
 | ||||
|     // delete request object instance on return to the event loop otherwise it is leaked
 | ||||
|     reply->deleteLater (); | ||||
|   } | ||||
| 
 | ||||
|   m_outstandingRequests.removeOne (reply); | ||||
|   qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ()); | ||||
| 
 | ||||
|   // delete request object instance on return to the event loop otherwise it is leaked
 | ||||
|   reply->deleteLater (); | ||||
| } | ||||
| 
 | ||||
| bool WSPRNet::decodeLine(QString const& line, QHash<QString,QString> &query) | ||||
|  | ||||
| @ -16,7 +16,7 @@ class WSPRNet : public QObject | ||||
|   Q_OBJECT; | ||||
| 
 | ||||
| public: | ||||
|     explicit WSPRNet(QObject *parent = nullptr); | ||||
|   explicit WSPRNet(QNetworkAccessManager *, QObject *parent = nullptr); | ||||
|     void upload(QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq, | ||||
|                 QString const& mode, QString const& tpct, QString const& dbm, QString const& version, | ||||
|                 QString const& fileName); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user