mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-11-26 14:18:38 -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_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…
Reference in New Issue
Block a user