mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-11-26 22:28:41 -05: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
0ab5a28067
commit
924e20efa8
@ -51,9 +51,11 @@ set (PROJECT_NAME "WSJT-X")
|
|||||||
set (PROJECT_VENDOR "Joe Taylor, K1JT")
|
set (PROJECT_VENDOR "Joe Taylor, K1JT")
|
||||||
set (PROJECT_CONTACT "Joe Taylor <k1jt@arrl.net>")
|
set (PROJECT_CONTACT "Joe Taylor <k1jt@arrl.net>")
|
||||||
set (PROJECT_COPYRIGHT "Copyright (C) 2001-2015 by Joe Taylor, K1JT")
|
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 wsjtx-main-${wsjtx_VERSION}.html)
|
||||||
set (PROJECT_MANUAL_DIRECTORY_URL http://www.physics.princeton.edu/pulsar/K1JT/wsjtx-doc)
|
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_SUMMARY_DESCRIPTION "${PROJECT_NAME} - JT9 and JT65 Modes for LF, MF and HF Amateur Radio.")
|
||||||
set (PROJECT_DESCRIPTION "${PROJECT_SUMMARY_DESCRIPTION}
|
set (PROJECT_DESCRIPTION "${PROJECT_SUMMARY_DESCRIPTION}
|
||||||
${PROJECT_NAME} implements JT9, a new mode designed especially for the LF, MF,
|
${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_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_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
|
||||||
option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.")
|
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)
|
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)
|
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
|
MessageClient.cpp
|
||||||
LettersSpinBox.cpp
|
LettersSpinBox.cpp
|
||||||
HelpTextWindow.cpp
|
HelpTextWindow.cpp
|
||||||
|
SampleDownloader.cpp
|
||||||
|
SampleDownloader/DirectoryDelegate.cpp
|
||||||
|
SampleDownloader/Directory.cpp
|
||||||
|
SampleDownloader/FileNode.cpp
|
||||||
|
SampleDownloader/RemoteFile.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set (jt9_CXXSRCS
|
set (jt9_CXXSRCS
|
||||||
@ -533,11 +539,6 @@ set (PALETTE_FILES
|
|||||||
Palettes/ZL1FZ.pal
|
Palettes/ZL1FZ.pal
|
||||||
)
|
)
|
||||||
|
|
||||||
set (SAMPLE_FILES
|
|
||||||
samples/130418_1742.wav
|
|
||||||
samples/130610_2343.wav
|
|
||||||
)
|
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
set (WSJTX_ICON_FILE ${CMAKE_PROJECT_NAME}.icns)
|
set (WSJTX_ICON_FILE ${CMAKE_PROJECT_NAME}.icns)
|
||||||
set (ICONSRCS
|
set (ICONSRCS
|
||||||
@ -618,6 +619,11 @@ endif (APPLE)
|
|||||||
#
|
#
|
||||||
find_program(CTAGS ctags)
|
find_program(CTAGS ctags)
|
||||||
find_program(ETAGS etags)
|
find_program(ETAGS etags)
|
||||||
|
|
||||||
|
#
|
||||||
|
# sub-directories
|
||||||
|
#
|
||||||
|
add_subdirectory (samples)
|
||||||
if (WSJT_GENERATE_DOCS)
|
if (WSJT_GENERATE_DOCS)
|
||||||
add_subdirectory (doc)
|
add_subdirectory (doc)
|
||||||
endif (WSJT_GENERATE_DOCS)
|
endif (WSJT_GENERATE_DOCS)
|
||||||
@ -855,9 +861,6 @@ endfunction (add_resources resources path)
|
|||||||
|
|
||||||
add_resources (wsjtx_RESOURCES "" ${TOP_LEVEL_RESOURCES})
|
add_resources (wsjtx_RESOURCES "" ${TOP_LEVEL_RESOURCES})
|
||||||
add_resources (wsjtx_RESOURCES /Palettes ${PALETTE_FILES})
|
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)
|
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 <QDateTime>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
@ -206,7 +207,7 @@ int main(int argc, char *argv[])
|
|||||||
).toBool () ? 1u : 4u;
|
).toBool () ? 1u : 4u;
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor);
|
MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor, new QNetworkAccessManager {&a});
|
||||||
w.show();
|
w.show();
|
||||||
|
|
||||||
QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit()));
|
QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit()));
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
#include "wsprnet.h"
|
#include "wsprnet.h"
|
||||||
#include "signalmeter.h"
|
#include "signalmeter.h"
|
||||||
#include "HelpTextWindow.hpp"
|
#include "HelpTextWindow.hpp"
|
||||||
|
#include "SampleDownloader.hpp"
|
||||||
|
|
||||||
#include "ui_mainwindow.h"
|
#include "ui_mainwindow.h"
|
||||||
#include "moc_mainwindow.cpp"
|
#include "moc_mainwindow.cpp"
|
||||||
@ -130,7 +131,8 @@ namespace
|
|||||||
|
|
||||||
//--------------------------------------------------- MainWindow constructor
|
//--------------------------------------------------- MainWindow constructor
|
||||||
MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdmem,
|
MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdmem,
|
||||||
unsigned downSampleFactor, QWidget *parent) :
|
unsigned downSampleFactor, QNetworkAccessManager * network_manager,
|
||||||
|
QWidget *parent) :
|
||||||
QMainWindow(parent),
|
QMainWindow(parent),
|
||||||
m_dataDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)},
|
m_dataDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)},
|
||||||
m_revision {revision ()},
|
m_revision {revision ()},
|
||||||
@ -327,6 +329,14 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
|
|||||||
ui->actionInclude_averaging->setActionGroup(DepthGroup);
|
ui->actionInclude_averaging->setActionGroup(DepthGroup);
|
||||||
ui->actionInclude_correlation->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;
|
QButtonGroup* txMsgButtonGroup = new QButtonGroup;
|
||||||
txMsgButtonGroup->addButton(ui->txrb1,1);
|
txMsgButtonGroup->addButton(ui->txrb1,1);
|
||||||
txMsgButtonGroup->addButton(ui->txrb2,2);
|
txMsgButtonGroup->addButton(ui->txrb2,2);
|
||||||
@ -669,7 +679,7 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
|
|||||||
progressBar->setMaximum(m_TRperiod);
|
progressBar->setMaximum(m_TRperiod);
|
||||||
m_modulator->setPeriod(m_TRperiod); // TODO - not thread safe
|
m_modulator->setPeriod(m_TRperiod); // TODO - not thread safe
|
||||||
m_dialFreqRxWSPR=0;
|
m_dialFreqRxWSPR=0;
|
||||||
wsprNet = new WSPRNet(this);
|
wsprNet = new WSPRNet(network_manager, this);
|
||||||
connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString)));
|
connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString)));
|
||||||
if(m_bFastMode) {
|
if(m_bFastMode) {
|
||||||
int ntr[]={5,10,15,30};
|
int ntr[]={5,10,15,30};
|
||||||
|
@ -49,6 +49,7 @@ namespace Ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class QSettings;
|
class QSettings;
|
||||||
|
class QNetworkAccessManager;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
class QFont;
|
class QFont;
|
||||||
class QHostInfo;
|
class QHostInfo;
|
||||||
@ -68,6 +69,7 @@ class SoundOutput;
|
|||||||
class Modulator;
|
class Modulator;
|
||||||
class SoundInput;
|
class SoundInput;
|
||||||
class Detector;
|
class Detector;
|
||||||
|
class SampleDownloader;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
{
|
{
|
||||||
@ -79,7 +81,8 @@ public:
|
|||||||
|
|
||||||
// Multiple instances: call MainWindow() with *thekey
|
// Multiple instances: call MainWindow() with *thekey
|
||||||
explicit MainWindow(bool multiple, QSettings *, QSharedMemory *shdmem,
|
explicit MainWindow(bool multiple, QSettings *, QSharedMemory *shdmem,
|
||||||
unsigned downSampleFactor, QWidget *parent = 0);
|
unsigned downSampleFactor, QNetworkAccessManager * network_manager,
|
||||||
|
QWidget *parent = 0);
|
||||||
~MainWindow();
|
~MainWindow();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@ -283,6 +286,7 @@ private:
|
|||||||
WSPRBandHopping m_WSPR_band_hopping;
|
WSPRBandHopping m_WSPR_band_hopping;
|
||||||
bool m_WSPR_tx_next;
|
bool m_WSPR_tx_next;
|
||||||
QMessageBox m_rigErrorMessageBox;
|
QMessageBox m_rigErrorMessageBox;
|
||||||
|
QScopedPointer<SampleDownloader> m_sampleDownloader;
|
||||||
|
|
||||||
QScopedPointer<WideGraph> m_wideGraph;
|
QScopedPointer<WideGraph> m_wideGraph;
|
||||||
QScopedPointer<EchoGraph> m_echoGraph;
|
QScopedPointer<EchoGraph> m_echoGraph;
|
||||||
|
@ -2351,6 +2351,7 @@ QPushButton[state="ok"] {
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="actionOnline_User_Guide"/>
|
<addaction name="actionOnline_User_Guide"/>
|
||||||
<addaction name="actionLocal_User_Guide"/>
|
<addaction name="actionLocal_User_Guide"/>
|
||||||
|
<addaction name="download_samples_action"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionKeyboard_shortcuts"/>
|
<addaction name="actionKeyboard_shortcuts"/>
|
||||||
<addaction name="actionSpecial_mouse_commands"/>
|
<addaction name="actionSpecial_mouse_commands"/>
|
||||||
@ -2816,6 +2817,14 @@ QPushButton[state="ok"] {
|
|||||||
<string>JTMSK</string>
|
<string>JTMSK</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</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>
|
</widget>
|
||||||
<layoutdefault spacing="6" margin="11"/>
|
<layoutdefault spacing="6" margin="11"/>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
@ -76,6 +76,21 @@ QString font_as_stylesheet (QFont const&);
|
|||||||
// conditional style sheet updates
|
// conditional style sheet updates
|
||||||
void update_dynamic_property (QWidget *, char const * property, QVariant const& value);
|
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
|
// Register some useful Qt types with QMetaType
|
||||||
Q_DECLARE_METATYPE (QHostAddress);
|
Q_DECLARE_METATYPE (QHostAddress);
|
||||||
|
|
||||||
|
@ -63,13 +63,18 @@ QString revision (QString const& svn_rev_string)
|
|||||||
return result.trimmed ();
|
return result.trimmed ();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString version ()
|
QString version (bool include_patch)
|
||||||
{
|
{
|
||||||
#if defined (CMAKE_BUILD)
|
#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)
|
# if defined (WSJTX_RC)
|
||||||
v += "-rc" WSJTX_STRINGIZE (WSJTX_RC);
|
+ "-rc" WSJTX_STRINGIZE (WSJTX_RC)
|
||||||
# endif
|
# endif
|
||||||
|
;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
QString v {"Not for Release"};
|
QString v {"Not for Release"};
|
||||||
#endif
|
#endif
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
QString revision (QString const& svn_rev_string = QString {});
|
QString revision (QString const& svn_rev_string = QString {});
|
||||||
QString version ();
|
QString version (bool include_patch = true);
|
||||||
QString program_title (QString const& revision = QString {});
|
QString program_title (QString const& revision = QString {});
|
||||||
|
|
||||||
#endif
|
#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 WSJT_DATA_DESTINATION "@WSJT_DATA_DESTINATION@"
|
||||||
#cmakedefine PROJECT_MANUAL "@PROJECT_MANUAL@"
|
#cmakedefine PROJECT_MANUAL "@PROJECT_MANUAL@"
|
||||||
#cmakedefine PROJECT_MANUAL_DIRECTORY_URL "@PROJECT_MANUAL_DIRECTORY_URL@"
|
#cmakedefine PROJECT_MANUAL_DIRECTORY_URL "@PROJECT_MANUAL_DIRECTORY_URL@"
|
||||||
|
#cmakedefine PROJECT_SAMPLES_URL "@PROJECT_SAMPLES_URL@"
|
||||||
|
|
||||||
#cmakedefine01 WSJT_SHARED_RUNTIME
|
#cmakedefine01 WSJT_SHARED_RUNTIME
|
||||||
#cmakedefine01 WSJT_QDEBUG_TO_FILE
|
#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?";
|
// char const * const wsprNetUrl = "http://127.0.0.1/post?";
|
||||||
};
|
};
|
||||||
|
|
||||||
WSPRNet::WSPRNet(QObject *parent)
|
WSPRNet::WSPRNet(QNetworkAccessManager * manager, QObject *parent)
|
||||||
: QObject{parent}
|
: QObject{parent}
|
||||||
, networkManager {new QNetworkAccessManager {this}}
|
, networkManager {manager}
|
||||||
, uploadTimer {new QTimer {this}}
|
, uploadTimer {new QTimer {this}}
|
||||||
, m_urlQueueSize {0}
|
, m_urlQueueSize {0}
|
||||||
{
|
{
|
||||||
@ -74,33 +74,35 @@ void WSPRNet::upload(QString const& call, QString const& grid, QString const& rf
|
|||||||
|
|
||||||
void WSPRNet::networkReply(QNetworkReply *reply)
|
void WSPRNet::networkReply(QNetworkReply *reply)
|
||||||
{
|
{
|
||||||
if (QNetworkReply::NoError != reply->error ()) {
|
// check if request was ours
|
||||||
Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ()));
|
if (m_outstandingRequests.removeOne (reply)) {
|
||||||
// not clearing queue or halting queuing as it may be a transient
|
if (QNetworkReply::NoError != reply->error ()) {
|
||||||
// one off request error
|
Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ()));
|
||||||
}
|
// not clearing queue or halting queuing as it may be a transient
|
||||||
else {
|
// one off request error
|
||||||
QString serverResponse = reply->readAll();
|
}
|
||||||
if( m_uploadType == 2) {
|
else {
|
||||||
if (!serverResponse.contains(QRegExp("spot\\(s\\) added"))) {
|
QString serverResponse = reply->readAll();
|
||||||
emit uploadStatus("Upload Failed");
|
if( m_uploadType == 2) {
|
||||||
urlQueue.clear();
|
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();
|
uploadTimer->stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urlQueue.isEmpty()) {
|
qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ());
|
||||||
emit uploadStatus("done");
|
|
||||||
QFile::remove(m_file);
|
// delete request object instance on return to the event loop otherwise it is leaked
|
||||||
uploadTimer->stop();
|
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)
|
bool WSPRNet::decodeLine(QString const& line, QHash<QString,QString> &query)
|
||||||
|
@ -16,7 +16,7 @@ class WSPRNet : public QObject
|
|||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
|
|
||||||
public:
|
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,
|
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& mode, QString const& tpct, QString const& dbm, QString const& version,
|
||||||
QString const& fileName);
|
QString const& fileName);
|
||||||
|
Loading…
Reference in New Issue
Block a user