summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2025-06-04 01:11:33 +0200
committerCarl Hetherington <cth@carlh.net>2025-07-09 00:35:10 +0200
commit86aece3fc0368fdbe58b3501c0abfb4b1d4c564b (patch)
tree7f52ae85b5ab0b36d05a0e75b8fd148ed07ba3b5
parent81fea8d53c5e92be9b43d97005e88cd92fe9e85d (diff)
Change TMS upload feature to generic upload.
-rw-r--r--src/lib/config.cc8
-rw-r--r--src/lib/config.h9
-rw-r--r--src/tools/dcpomatic.cc17
-rw-r--r--src/wx/upload_destination_dialog.cc140
-rw-r--r--src/wx/upload_destination_dialog.h55
-rw-r--r--src/wx/upload_destination_panel.cc189
-rw-r--r--src/wx/upload_destination_panel.h61
-rw-r--r--src/wx/upload_dialog.cc70
-rw-r--r--src/wx/upload_dialog.h42
-rw-r--r--src/wx/wscript3
10 files changed, 589 insertions, 5 deletions
diff --git a/src/lib/config.cc b/src/lib/config.cc
index 233c7fed7..42fd5d75a 100644
--- a/src/lib/config.cc
+++ b/src/lib/config.cc
@@ -362,6 +362,10 @@ try
_tms = UploadDestination(node);
}
+ for (auto i: f.node_children("UploadDestination")) {
+ _upload_destinations.push_back(UploadDestination(i));
+ }
+
_language = f.optional_string_child("Language");
_default_dcp_content_type = DCPContentType::from_isdcf_name(f.optional_string_child("DefaultDCPContentType").get_value_or("FTR"));
@@ -789,6 +793,10 @@ Config::write_config() const
_tms->as_xml(cxml::add_child(root, "TMS"));
}
+ for (auto i: _upload_destinations) {
+ i.as_xml(cxml::add_child(root, "UploadDestination"));
+ }
+
if (_language) {
/* [XML:opt] Language Language to use in the GUI e.g. <code>fr_FR</code>. */
cxml::add_text_child(root, "Language", _language.get());
diff --git a/src/lib/config.h b/src/lib/config.h
index f65edda20..e33bfe3f3 100644
--- a/src/lib/config.h
+++ b/src/lib/config.h
@@ -155,6 +155,10 @@ public:
return _only_servers_encode;
}
+ std::vector<UploadDestination> upload_destinations() const {
+ return _upload_destinations;
+ }
+
boost::optional<UploadDestination> tms() const {
return _tms;
}
@@ -697,6 +701,10 @@ public:
maybe_set(_only_servers_encode, o);
}
+ void set_upload_destinations(std::vector<UploadDestination> const& destinations) {
+ maybe_set(_upload_destinations, destinations);
+ }
+
void set_tms(UploadDestination tms) {
_tms = tms;
changed();
@@ -1319,6 +1327,7 @@ private:
/** J2K encoding servers that should definitely be used */
std::vector<std::string> _servers;
bool _only_servers_encode;
+ std::vector<UploadDestination> _upload_destinations;
boost::optional<UploadDestination> _tms;
/** The list of possible DCP frame rates that DCP-o-matic will use */
std::list<int> _allowed_dcp_frame_rates;
diff --git a/src/tools/dcpomatic.cc b/src/tools/dcpomatic.cc
index d8e79d667..fc7dd5e40 100644
--- a/src/tools/dcpomatic.cc
+++ b/src/tools/dcpomatic.cc
@@ -54,6 +54,7 @@
#include "wx/system_information_dialog.h"
#include "wx/templates_dialog.h"
#include "wx/update_dialog.h"
+#include "wx/upload_dialog.h"
#include "wx/video_waveform_dialog.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
@@ -97,6 +98,7 @@
#include "lib/transcode_job.h"
#include "lib/unzipper.h"
#include "lib/update_checker.h"
+#include "lib/upload_job.h"
#include "lib/variant.h"
#include "lib/version.h"
#include "lib/video_content.h"
@@ -241,7 +243,7 @@ enum {
ID_jobs_make_self_dkdm,
ID_jobs_export_video_file,
ID_jobs_export_subtitles,
- ID_jobs_send_dcp_to_tms,
+ ID_jobs_upload_dcp,
ID_jobs_show_dcp,
ID_jobs_open_dcp_in_player,
ID_view_closed_captions,
@@ -352,7 +354,7 @@ public:
Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_self_dkdm, this), ID_jobs_make_self_dkdm);
Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_export_video_file, this), ID_jobs_export_video_file);
Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_export_subtitles, this), ID_jobs_export_subtitles);
- Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_send_dcp_to_tms, this), ID_jobs_send_dcp_to_tms);
+ Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_upload_dcp, this), ID_jobs_upload_dcp);
Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_show_dcp, this), ID_jobs_show_dcp);
Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_open_dcp_in_player, this), ID_jobs_open_dcp_in_player);
Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_closed_captions, this), ID_view_closed_captions);
@@ -1068,9 +1070,14 @@ private:
}
- void jobs_send_dcp_to_tms ()
+ void jobs_upload_dcp()
{
- _film->send_dcp_to_tms ();
+ UploadDialog dialog(this);
+ if (dialog.ShowModal() == wxID_OK) {
+ for (auto const& destination: dialog.destinations()) {
+ JobManager::instance()->add(make_shared<UploadJob>(_film, destination));
+ }
+ }
}
void jobs_show_dcp ()
@@ -1403,7 +1410,7 @@ private:
add_item (jobs_menu, _("Export video file...\tCtrl-E"), ID_jobs_export_video_file, NEEDS_FILM);
add_item (jobs_menu, _("Export subtitles..."), ID_jobs_export_subtitles, NEEDS_FILM);
jobs_menu->AppendSeparator ();
- add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
+ add_item (jobs_menu, _("&Upload DCP..."), ID_jobs_upload_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
#if defined(DCPOMATIC_OSX)
add_item (jobs_menu, _("S&how DCP in Finder"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
diff --git a/src/wx/upload_destination_dialog.cc b/src/wx/upload_destination_dialog.cc
new file mode 100644
index 000000000..a32055c60
--- /dev/null
+++ b/src/wx/upload_destination_dialog.cc
@@ -0,0 +1,140 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "check_box.h"
+#include "dcpomatic_choice.h"
+#include "dcpomatic_spin_ctrl.h"
+#include "password_entry.h"
+#include "upload_destination_dialog.h"
+#include "wx_util.h"
+#include "lib/upload_destination.h"
+
+
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+UploadDestinationDialog::UploadDestinationDialog(wxWindow* parent)
+ : TableDialog(parent, _("Upload destination"), 2, 1, true)
+{
+ add(_("Name"), true);
+ _name = add(new wxTextCtrl(this, wxID_ANY, {}, wxDefaultPosition, wxSize(480, -1)));
+ add(_("Protocol"), true);
+ _protocol = add(new Choice(this));
+ _passive = add(new CheckBox(this, _("Passive")));
+ add_spacer();
+ _limit_connections = add(new CheckBox(this, _("Limit connections")));
+ _maximum_connections = add(new SpinCtrl(this, DCPOMATIC_SPIN_CTRL_WIDTH), 0, wxALIGN_CENTER_VERTICAL);
+ _maximum_connections->SetRange(1, 100);
+ add(_("Host"), true);
+ _host = add(new wxTextCtrl(this, wxID_ANY));
+ add(_("Path"), true);
+ _path = add(new wxTextCtrl(this, wxID_ANY));
+ add(_("User"), true);
+ _user = add(new wxTextCtrl(this, wxID_ANY));
+ add(_("Password"), true);
+ _password = new PasswordEntry(this);
+ add(_password->get_panel());
+
+ _protocol->add_entry(_("SCP"), file_transfer_protocol_to_string(FileTransferProtocol::SCP));
+ _protocol->add_entry(_("FTP"), file_transfer_protocol_to_string(FileTransferProtocol::FTP));
+
+ _protocol->bind(&UploadDestinationDialog::protocol_changed, this);
+ _limit_connections->bind(&UploadDestinationDialog::limit_connections_changed, this);
+
+ layout();
+
+ _name->SetFocus();
+
+ _protocol->set(0);
+ protocol_changed();
+ _limit_connections->set(false);
+ _maximum_connections->set(5);
+ limit_connections_changed();
+}
+
+
+void
+UploadDestinationDialog::protocol_changed()
+{
+ auto const ftp = _protocol->get_data() == string("ftp") || _protocol->get_data() == string("ftps");
+ _passive->Enable(ftp);
+ _passive->SetValue(ftp);
+}
+
+
+void
+UploadDestinationDialog::limit_connections_changed()
+{
+ _maximum_connections->Enable(_limit_connections->get());
+}
+
+
+void
+UploadDestinationDialog::set(UploadDestination const& destination)
+{
+ checked_set(_name, destination.name);
+ checked_set(_protocol, static_cast<int>(destination.protocol));
+ checked_set(_passive, destination.passive_ftp);
+ checked_set(_limit_connections, static_cast<bool>(destination.maximum_connections));
+ if (_limit_connections->get()) {
+ checked_set(_maximum_connections, *destination.maximum_connections);
+ } else {
+ checked_set(_maximum_connections, 5);
+ }
+ checked_set(_host, destination.host);
+ checked_set(_path, destination.path);
+ checked_set(_user, destination.user);
+ checked_set(_password, destination.password);
+
+ protocol_changed();
+ limit_connections_changed();
+}
+
+
+vector<UploadDestination>
+UploadDestinationDialog::get () const
+{
+ auto protocol = FileTransferProtocol::SCP;
+ if (auto protocol_name = _protocol->get_data()) {
+ protocol = file_transfer_protocol_from_string(*protocol_name);
+ }
+
+ optional<int> max;
+ if (_limit_connections->get()) {
+ max = _maximum_connections->get();
+ }
+
+ return {
+ UploadDestination(
+ wx_to_std(_name->GetValue()),
+ protocol,
+ _passive->get(),
+ max,
+ wx_to_std(_host->GetValue()),
+ wx_to_std(_path->GetValue()),
+ wx_to_std(_user->GetValue()),
+ _password->get()
+ )
+ };
+}
diff --git a/src/wx/upload_destination_dialog.h b/src/wx/upload_destination_dialog.h
new file mode 100644
index 000000000..1ebe13552
--- /dev/null
+++ b/src/wx/upload_destination_dialog.h
@@ -0,0 +1,55 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "table_dialog.h"
+#include "lib/upload_destination.h"
+#include <boost/optional.hpp>
+
+
+class CheckBox;
+class Choice;
+class PasswordEntry;
+class SpinCtrl;
+class wxTextCtrl;
+
+
+class UploadDestinationDialog : public TableDialog
+{
+public:
+ explicit UploadDestinationDialog(wxWindow *);
+
+ void set(UploadDestination const& destination);
+ std::vector<UploadDestination> get() const;
+
+private:
+ void protocol_changed();
+ void limit_connections_changed();
+
+ wxTextCtrl* _name;
+ Choice* _protocol;
+ CheckBox* _passive;
+ CheckBox* _limit_connections;
+ SpinCtrl* _maximum_connections;
+ wxTextCtrl* _host;
+ wxTextCtrl* _path;
+ wxTextCtrl* _user;
+ PasswordEntry* _password;
+};
diff --git a/src/wx/upload_destination_panel.cc b/src/wx/upload_destination_panel.cc
new file mode 100644
index 000000000..6465400d9
--- /dev/null
+++ b/src/wx/upload_destination_panel.cc
@@ -0,0 +1,189 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcpomatic_button.h"
+#include "upload_destination_dialog.h"
+#include "upload_destination_panel.h"
+#include "wx_util.h"
+#include "lib/config.h"
+#include <wx/treelist.h>
+
+
+using std::string;
+using std::vector;
+using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+UploadDestinationPanel::UploadDestinationPanel(wxWindow* parent)
+ : wxPanel(parent, wxID_ANY)
+{
+ _list = new wxTreeListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTL_NO_HEADER | wxTL_3STATE);
+ _list->AppendColumn(char_to_wx("foo"), 640);
+
+ for (auto destination: Config::instance()->upload_destinations()) {
+ add_destination(destination);
+ }
+
+ auto buttons = new wxBoxSizer(wxVERTICAL);
+
+ _add = new Button(this, _("Add..."));
+ buttons->Add(_add, 0, wxEXPAND | wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+ _edit = new Button(this, _("Edit..."));
+ buttons->Add(_edit, 0, wxEXPAND | wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+ _remove = new Button(this, _("Remove"));
+ buttons->Add(_remove, 0, wxEXPAND | wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+
+ _add->bind(&UploadDestinationPanel::add, this);
+ _edit->bind(&UploadDestinationPanel::edit, this);
+ _remove->bind(&UploadDestinationPanel::remove, this);
+
+ _list->Bind(wxEVT_TREELIST_SELECTION_CHANGED, boost::bind(&UploadDestinationPanel::selection_changed, this));
+ _list->Bind(wxEVT_TREELIST_ITEM_CHECKED, boost::bind(&UploadDestinationPanel::checkbox_changed, this));
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(_list, 1, wxEXPAND);
+ sizer->Add(buttons, 0, wxEXPAND | wxLEFT, DCPOMATIC_SIZER_X_GAP);
+ SetSizerAndFit(sizer);
+
+ setup_sensitivity();
+}
+
+
+void
+UploadDestinationPanel::checkbox_changed()
+{
+ DestinationsChanged();
+}
+
+
+void
+UploadDestinationPanel::selection_changed()
+{
+ setup_sensitivity();
+}
+
+
+void
+UploadDestinationPanel::setup_sensitivity()
+{
+ auto const selection = static_cast<bool>(selected());
+ _edit->Enable(selection);
+ _remove->Enable(selection);
+}
+
+
+void
+UploadDestinationPanel::add_destination(UploadDestination destination)
+{
+ auto const id = _list->AppendItem(_list->GetRootItem(), std_to_wx(destination.name));
+ _destinations.push_back(std::make_pair(id, destination));
+}
+
+
+void
+UploadDestinationPanel::add()
+{
+ UploadDestinationDialog dialog(this);
+ if (dialog.ShowModal() == wxID_OK) {
+ add_destination(dialog.get()[0]);
+ update_config();
+ }
+}
+
+
+void
+UploadDestinationPanel::update_config()
+{
+ vector<UploadDestination> dest;
+ for (auto const& i: _destinations) {
+ dest.push_back(i.second);
+ }
+ Config::instance()->set_upload_destinations(dest);
+}
+
+
+optional<wxTreeListItem>
+UploadDestinationPanel::selected() const
+{
+ wxTreeListItems selection;
+ _list->GetSelections(selection);
+ DCPOMATIC_ASSERT(selection.size() <= 1);
+ if (selection.empty()) {
+ return {};
+ }
+
+ return selection[0];
+}
+
+void
+UploadDestinationPanel::edit()
+{
+ if (auto sel = selected()) {
+ for (auto& i: _destinations) {
+ if (i.first == *sel) {
+ UploadDestinationDialog dialog(this);
+ dialog.set(i.second);
+ if (dialog.ShowModal() == wxID_OK) {
+ i.second = dialog.get()[0];
+ update_config();
+ _list->SetItemText(*sel, std_to_wx(i.second.name));
+ }
+ }
+ }
+ }
+}
+
+
+void
+UploadDestinationPanel::remove()
+{
+ if (auto sel = selected()) {
+ for (auto iter = _destinations.begin(); iter != _destinations.end(); ) {
+ if (iter->first == *sel) {
+ iter = _destinations.erase(iter);
+ _list->DeleteItem(iter->first);
+ } else {
+ ++iter;
+ }
+ }
+ update_config();
+ }
+
+}
+
+
+vector<UploadDestination>
+UploadDestinationPanel::destinations() const
+{
+ vector<UploadDestination> checked;
+
+ for (auto const& dest: _destinations) {
+ if (_list->GetCheckedState(dest.first) == wxCHK_CHECKED) {
+ checked.push_back(dest.second);
+ }
+ }
+
+ return checked;
+}
+
diff --git a/src/wx/upload_destination_panel.h b/src/wx/upload_destination_panel.h
new file mode 100644
index 000000000..fb2fee99e
--- /dev/null
+++ b/src/wx/upload_destination_panel.h
@@ -0,0 +1,61 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/upload_destination.h"
+#include <wx/treelist.h>
+#include <wx/wx.h>
+#include <boost/signals2.hpp>
+#include <map>
+
+
+class Button;
+
+
+class UploadDestinationPanel : public wxPanel
+{
+public:
+ UploadDestinationPanel(wxWindow* parent);
+
+ /** @return ticked, i.e. "enabled" destinations */
+ std::vector<UploadDestination> destinations() const;
+
+ boost::signals2::signal<void ()> DestinationsChanged;
+
+private:
+ void add();
+ void edit();
+ void remove();
+
+ void add_destination(UploadDestination destination);
+ boost::optional<wxTreeListItem> selected() const;
+ void selection_changed();
+ void setup_sensitivity();
+ void update_config();
+ void destinations_changed();
+ void checkbox_changed();
+
+ wxTreeListCtrl* _list;
+ Button* _add;
+ Button* _edit;
+ Button* _remove;
+ std::vector<std::pair<wxTreeListItem, UploadDestination>> _destinations;
+};
+
diff --git a/src/wx/upload_dialog.cc b/src/wx/upload_dialog.cc
new file mode 100644
index 000000000..606ba5ee3
--- /dev/null
+++ b/src/wx/upload_dialog.cc
@@ -0,0 +1,70 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "static_text.h"
+#include "upload_destination_panel.h"
+#include "upload_dialog.h"
+#include "wx_util.h"
+#include <wx/wx.h>
+
+
+using std::vector;
+
+
+UploadDialog::UploadDialog(wxWindow* parent)
+ : wxDialog(parent, wxID_ANY, _("Upload DCP"))
+ , _upload_destination_panel(new UploadDestinationPanel(this))
+{
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+
+ wxFont subheading_font(*wxNORMAL_FONT);
+ subheading_font.SetWeight(wxFONTWEIGHT_BOLD);
+
+ sizer->Add(_upload_destination_panel, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
+
+ auto buttons = CreateStdDialogButtonSizer(0);
+ sizer->Add(CreateSeparatedSizer(buttons), wxSizerFlags().Expand().DoubleBorder());
+ _start_upload = new wxButton(this, wxID_OK, _("Start upload"));
+ buttons->SetAffirmativeButton(_start_upload);
+ buttons->SetNegativeButton(new wxButton(this, wxID_CANCEL, _("Cancel")));
+ buttons->Realize();
+
+ _upload_destination_panel->DestinationsChanged.connect(boost::bind(&UploadDialog::setup_sensitivity, this));
+ setup_sensitivity();
+
+ SetSizer(sizer);
+ SetSize({640, 480});
+}
+
+
+vector<UploadDestination>
+UploadDialog::destinations() const
+{
+ return _upload_destination_panel->destinations();
+}
+
+
+void
+UploadDialog::setup_sensitivity()
+{
+ _start_upload->Enable(!_upload_destination_panel->destinations().empty());
+}
+
diff --git a/src/wx/upload_dialog.h b/src/wx/upload_dialog.h
new file mode 100644
index 000000000..20f395254
--- /dev/null
+++ b/src/wx/upload_dialog.h
@@ -0,0 +1,42 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/upload_destination.h"
+#include <wx/wx.h>
+
+
+class UploadDestinationPanel;
+
+
+class UploadDialog : public wxDialog
+{
+public:
+ UploadDialog(wxWindow* parent);
+
+ std::vector<UploadDestination> destinations() const;
+
+private:
+ void setup_sensitivity();
+
+ UploadDestinationPanel* _upload_destination_panel;
+ wxButton* _start_upload;
+};
+
diff --git a/src/wx/wscript b/src/wx/wscript
index 3dfc157e2..ae754f0a9 100644
--- a/src/wx/wscript
+++ b/src/wx/wscript
@@ -185,7 +185,10 @@ sources = """
timeline_time_axis_view.cc
timing_panel.cc
try_unmount_dialog.cc
+ upload_destination_dialog.cc
+ upload_destination_panel.cc
update_dialog.cc
+ upload_dialog.cc
verify_dcp_dialog.cc
verify_dcp_progress_panel.cc
verify_dcp_result_panel.cc