Audacity 3.2.0
ShareAudioDialog.cpp
Go to the documentation of this file.
1/* SPDX-License-Identifier: GPL-2.0-or-later */
2/*!********************************************************************
3
4 Audacity: A Digital Audio Editor
5
6 ShareAudioDialog.cpp
7
8 Dmitry Vedenko
9
10**********************************************************************/
11#include "ShareAudioDialog.h"
12
13#include <cassert>
14#include <rapidjson/document.h>
15
16#include <wx/bmpbuttn.h>
17#include <wx/button.h>
18#include <wx/clipbrd.h>
19#include <wx/gauge.h>
20#include <wx/frame.h>
21#include <wx/stattext.h>
22#include <wx/statline.h>
23#include <wx/textctrl.h>
24#include <wx/radiobut.h>
25
26#include "AllThemeResources.h"
27#include "BasicUI.h"
28#include "MemoryX.h"
29#include "Project.h"
30#include "ShuttleGui.h"
31#include "Theme.h"
32#include "Track.h"
33#include "WaveTrack.h"
34
35#include "ServiceConfig.h"
36#include "OAuthService.h"
37#include "UploadService.h"
38#include "UserService.h"
39
41#include "../UserPanel.h"
42
43#include "CodeConversions.h"
44
45#include "Export.h"
46#include "ExportProgressUI.h"
47#include "ExportUtils.h"
50
51#include "WindowAccessible.h"
52#include "HelpSystem.h"
53#include "ProjectRate.h"
54#include "ProjectWindows.h"
55
56#include "CloudLocationDialog.h"
57
59{
60namespace
61{
63{
64 const auto tempPath = GetUploadTempPath();
65
66 wxFileName fileName(
67 tempPath,
68 wxString::Format(
69 "%lld", std::chrono::system_clock::now().time_since_epoch().count()),
70 extension);
71
72 fileName.Mkdir(0700, wxPATH_MKDIR_FULL);
73
74 if (fileName.Exists())
75 {
76 if (!wxRemoveFile(fileName.GetFullPath()))
77 return {};
78 }
79
80 return fileName.GetFullPath();
81}
82
83const auto publicLabelText = XO("Public");
85 XO("Anyone will be able to listen to this audio.");
86
87const auto unlistedLabelText = XO("Unlisted");
89 "Only you and people you share a link with will be able to listen to this audio.");
90
91}
92
93// A helper structures holds UploadService and UploadPromise
95{
97
99
102 {
103 }
104};
105
107{
108public:
110 : mParent(parent)
111 {
112
113 }
114
116
117 void Cancel()
118 {
119 mCancelled.store(true, std::memory_order_release);
120 }
121
123 {
124 return mResult;
125 }
126
128 {
129 mResult = result;
130 }
131
133 {
134 }
135
136 bool IsCancelled() const override
137 {
138 return mCancelled.load(std::memory_order_acquire);
139 }
140
141 bool IsStopped() const override
142 {
143 return false;
144 }
145
146 void OnProgress(double value) override
147 {
148 mProgress.store(value, std::memory_order_release);
149 }
150
151 void UpdateUI()
152 {
153 constexpr auto ProgressSteps = 1000ull;
154
155 mParent.UpdateProgress(mProgress.load(std::memory_order_acquire) * ProgressSteps, ProgressSteps);
156 }
157
158private:
159
161
162 std::atomic<bool> mCancelled{false};
163 std::atomic<double> mProgress;
165};
166
169 parent, wxID_ANY, XO("Share Audio"), wxDefaultPosition, { 480, 250 },
170 wxDEFAULT_DIALOG_STYLE)
171 , mProject(project)
172 , mInitialStatePanel(*this)
173 , mServices(std::make_unique<Services>())
174{
176
177 ShuttleGui s(this, eIsCreating);
178
179 s.StartVerticalLay();
180 {
181 Populate(s);
182 }
183 s.EndVerticalLay();
184
185 Layout();
186 Fit();
187 Centre();
188
189 const auto size = GetSize();
190
191 SetMinSize({ size.x, std::min(250, size.y) });
192 SetMaxSize({ size.x, -1 });
193
194 mContinueAction = [this]()
195 {
196 if (mInitialStatePanel.root->IsShown())
197 StartUploadProcess();
198 };
199
200 Bind(
201 wxEVT_CHAR_HOOK,
202 [this](auto& evt)
203 {
204 if (!IsEscapeKey(evt))
205 {
206 evt.Skip();
207 return;
208 }
209
210 OnCancel();
211 });
212}
213
215{
217 // Clean up the temp file when the dialog is closed
218 if (!mFilePath.empty() && wxFileExists(mFilePath))
219 wxRemoveFile(mFilePath);
220}
221
223{
226
227 s.StartHorizontalLay(wxEXPAND, 0);
228 {
230 {
231 s.SetBorder(2);
232 s.StartHorizontalLay(wxEXPAND, 0);
233 {
234 s.AddSpace(0, 0, 1);
235
236 mCancelButton = s.AddButton(XXO("&Cancel"));
237 mCancelButton->Bind(wxEVT_BUTTON, [this](auto) { OnCancel(); });
238
239 s.AddSpace(4, 0, 0);
240
241 mContinueButton = s.AddButton(XXO("C&ontinue"));
242 mContinueButton->Bind(wxEVT_BUTTON, [this](auto) { OnContinue(); });
243 }
245 }
247 }
249
250 const auto title = mProject.GetProjectName();
251
252 if (!title.empty())
253 {
255 mInitialStatePanel.trackTitle->SetInsertionPoint(title.length());
256 }
257
259
261 wxEVT_TEXT,
262 [this](auto&) {
263 mContinueButton->Enable(
265 });
266}
267
269{
270 if (mInProgress)
271 {
272 AudacityMessageDialog dlgMessage(
273 this, XO("Are you sure you want to cancel?"), XO("Cancel upload to Audio.com"),
274 wxYES_NO | wxICON_QUESTION | wxNO_DEFAULT | wxSTAY_ON_TOP);
275
276 const auto result = dlgMessage.ShowModal();
277
278 if (result != wxID_YES)
279 return;
280
281 // If export has started, notify it that it should be canceled
283 mExportProgressUpdater->Cancel();
284 }
285
286
287 // If upload was started - ask it to discard the result.
288 // The result should be discarded even after the upload has finished
289 if (mServices->uploadPromise)
290 mServices->uploadPromise->DiscardResult();
291
292 EndModal(wxID_CANCEL);
293}
294
296{
298}
299
300namespace
301{
302int CalculateChannels(const TrackList& trackList)
303{
304 auto range = trackList.Any<const WaveTrack>();
305 return std::all_of(range.begin(), range.end(), [](const WaveTrack *track){
306 return IsMono(*track) && track->GetPan() == 0;
307 }) ? 1 : 2;
308}
309}
310
312{
314
315 const double t0 = 0.0;
316 const double t1 = tracks.GetEndTime();
317
318 const int nChannels = CalculateChannels(tracks);
319
320 auto hasMimeType = [](const auto&& mimeTypes, const std::string& mimeType)
321 {
322 return std::find(mimeTypes.begin(), mimeTypes.end(), mimeType) != mimeTypes.end();
323 };
324
325 const auto& registry = ExportPluginRegistry::Get();
326
327 for(const auto& preferredMimeType : GetServiceConfig().GetPreferredAudioFormats())
328 {
329 auto config = GetServiceConfig().GetExportConfig(preferredMimeType);
331 auto pluginIt = std::find_if(registry.begin(), registry.end(), [&](auto t)
332 {
333 auto [plugin, formatIndex] = t;
334 parameters.clear();
335 return hasMimeType(plugin->GetMimeTypes(formatIndex), preferredMimeType) &&
336 plugin->ParseConfig(formatIndex, config, parameters);
337 });
338
339 if(pluginIt == registry.end())
340 continue;
341
342 const auto [plugin, formatIndex] = *pluginIt;
343
344 const auto formatInfo = plugin->GetFormatInfo(formatIndex);
345 const auto path = GenerateTempPath(formatInfo.extensions[0]);
346
347 if(path.empty())
348 continue;
349
350 mExportProgressUpdater = std::make_unique<ExportProgressUpdater>(*this);
351
352 auto builder = ExportTaskBuilder{}
353 .SetParameters(parameters)
354 .SetNumChannels(nChannels)
356 .SetPlugin(plugin)
357 .SetFileName(path)
358 .SetRange(t0, t1, false);
359
360 auto result = ExportResult::Error;
362 {
363 auto exportTask = builder.Build(mProject);
364
365 auto f = exportTask.get_future();
366 std::thread(std::move(exportTask), std::ref(*mExportProgressUpdater)).detach();
367
369 {
370 while(f.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready)
371 mExportProgressUpdater->UpdateUI();
372 result = f.get();
373 });
374 });
375
376 mExportProgressUpdater->SetResult(result);
377 const auto success = result == ExportResult::Success;
378 if(!success && wxFileExists(path))
379 wxRemoveFile(path);
380 if(success)
381 return path;
382 }
383 return {};
384}
385
387{
388 mInProgress = true;
389
390 mInitialStatePanel.root->Hide();
391 mProgressPanel.root->Show();
392
393 mProgressPanel.info->Hide();
394
395 mContinueButton->Hide();
396
397 Layout();
398 Fit();
399
401
403
404 if(mFilePath.empty())
405 {
408 {
410 }
411
412 return;
413 }
414
415 mProgressPanel.title->SetLabel(XO("Uploading audio...").Translation());
417
418 mServices->uploadPromise = mServices->uploadService.Upload(
419 mFilePath,
421 false,
422 [this](const auto& result)
423 {
424 CallAfter(
425 [this, result]()
426 {
427 mInProgress = false;
428
429 if (result.result == UploadOperationCompleted::Result::Success)
430 {
431 // Success indicates that UploadSuccessfulPayload is in the payload
432 assert(std::holds_alternative<UploadSuccessfulPayload>(result.payload));
433
434 if (
435 auto payload =
436 std::get_if<UploadSuccessfulPayload>(&result.payload))
437 HandleUploadSucceeded(*payload);
438 else
439 HandleUploadSucceeded({});
440
441 }
442 else if (
443 result.result != UploadOperationCompleted::Result::Aborted)
444 {
445 if (
446 auto payload =
447 std::get_if<UploadFailedPayload>(&result.payload))
448 HandleUploadFailed(*payload);
449 else
450 HandleUploadFailed({});
451 }
452 });
453 },
454 [this](auto current, auto total)
455 {
456 CallAfter(
457 [this, current, total]()
458 {
459 UpdateProgress(current, total);
460 });
461 });
462}
463
464void ShareAudioDialog::HandleUploadSucceeded(
465 const UploadSuccessfulPayload& payload)
466{
467 EndModal(wxID_CLOSE);
468 OpenInDefaultBrowser(wxString { payload.audioUrl });
469}
470
471void ShareAudioDialog::HandleUploadFailed(const UploadFailedPayload& payload)
472{
473 EndModal(wxID_ABORT);
474
475 TranslatableString message;
476
477 if (!payload.message.empty())
478 {
479 auto details = payload.message;
480
481 for (auto& err : payload.additionalErrors)
482 details += " " + err.second;
483
484 message = XO("Error: %s").Format(details);
485 }
486 else
487 {
488 message = XO(
489 "We are unable to upload this file. Please try again and make sure to link to your audio.com account before uploading.");
490 }
491
493 {}, XO("Upload error"),
494 message,
495 {},
497
498}
499
500void ShareAudioDialog::HandleExportFailure()
501{
502 EndModal(wxID_ABORT);
503
505 {}, XO("Export error"),
506 XO("We are unable to prepare this file for uploading."), {},
508}
509
510void ShareAudioDialog::ResetProgress()
511{
512 mStageStartTime = Clock::now();
513 mLastUIUpdateTime = mStageStartTime;
514
515 mProgressPanel.elapsedTime->SetLabel(" 00:00:00");
516 mProgressPanel.remainingTime->SetLabel(" 00:00:00");
517 mProgressPanel.progress->SetValue(0);
518
519 mLastProgressValue = 0;
520
521 mExportProgressUpdater.reset();
522
524}
525
526namespace
527{
528void SetTimeLabel(wxStaticText* label, std::chrono::milliseconds time)
529{
530 wxTimeSpan tsElapsed(0, 0, 0, time.count());
531
532 label->SetLabel(tsElapsed.Format(wxT(" %H:%M:%S")));
533 label->SetName(label->GetLabel());
534 label->Update();
535}
536}
537
538void ShareAudioDialog::UpdateProgress(uint64_t current, uint64_t total)
539{
540 using namespace std::chrono;
541
542 const auto now = Clock::now();
543
544 if (current == 0)
545 return;
546
547 if (current > total)
548 current = total;
549
550 if (mLastProgressValue != current)
551 {
552 constexpr int scale = 10000;
553
554 mLastProgressValue = static_cast<int>(current);
555
556 mProgressPanel.progress->SetRange(scale);
557 mProgressPanel.progress->SetValue((current * scale) / total);
558
559 if (current == total && mServices->uploadPromise)
560 {
561 mProgressPanel.timePanel->Hide();
562 mProgressPanel.title->SetLabel(XO("Finalizing upload...").Translation());
563 }
564 }
565
566 const auto elapsedSinceUIUpdate = now - mLastUIUpdateTime;
567
568 constexpr auto uiUpdateTimeout = 500ms;
569
570 if (elapsedSinceUIUpdate < uiUpdateTimeout && current < total)
571 return;
572
573 mLastUIUpdateTime = now;
574
575 const auto elapsed = duration_cast<milliseconds>(now - mStageStartTime);
576
577 SetTimeLabel(mProgressPanel.elapsedTime, elapsed);
578
579 const auto estimate = elapsed * total / current;
580 const auto remains = estimate - elapsed;
581
583 mProgressPanel.remainingTime,
584 std::chrono::duration_cast<std::chrono::milliseconds>(remains));
585}
586
587ShareAudioDialog::InitialStatePanel::InitialStatePanel(ShareAudioDialog& parent)
588 : parent { parent }
589{
590}
591
593 ShuttleGui& s)
594{
595 root = s.StartInvisiblePanel();
596 s.StartVerticalLay(wxEXPAND, 1);
597 {
598 s.SetBorder(16);
599
601 GetUserService(), true, s.GetParent() };
602
603 mUserDataChangedSubscription = userPanel->Subscribe(
604 [this](auto message) { UpdateUserData(message.IsAuthorized); });
605
606 s.Prop(0).AddWindow(userPanel, wxEXPAND);
607
608 s.SetBorder(0);
609
610 s.AddWindow(safenew wxStaticLine { s.GetParent() }, wxEXPAND);
611
613 {
615 {
616 s.AddFixedText(XO("Track Title"));
617 s.AddSpace(8);
618 trackTitle = s.AddTextBox({}, {}, 60);
619 trackTitle->SetName(XO("Track Title").Translation());
620 trackTitle->SetFocus();
621 trackTitle->SetMaxLength(100);
622 s.AddSpace(16);
623
624 anonInfoPanel = s.StartInvisiblePanel();
625 {
626 AccessibleLinksFormatter privacyPolicy(XO(
627 /*i18n-hint: %s substitutes for audio.com. %% creates a linebreak in this context. */
628 "Sharing audio requires a free %s account linked to Audacity. %%Press \"Link account\" above to proceed."));
629
630 privacyPolicy.FormatLink(
631 L"%s", XO("audio.com"), "https://audio.com");
632
633 privacyPolicy.FormatLink(
634 L"%%", TranslatableString {},
636
637 privacyPolicy.Populate(s);
638 }
640
641 authorizedInfoPanel = s.StartInvisiblePanel();
642 s.StartHorizontalLay(wxEXPAND, 1);
643 {
644 s.AddFixedText(XO("Press \"Continue\" to upload to audio.com"));
645 }
648 }
650 }
652 }
653 s.EndVerticalLay();
655
656 UpdateUserData(
657 GetOAuthService().HasRefreshToken() &&
658 !GetUserService().GetUserSlug().empty());
659}
660
662{
663 parent.mIsAuthorised = authorized;
664
665 anonInfoPanel->Show(!authorized);
666 authorizedInfoPanel->Show(authorized);
667
668 if (parent.mContinueButton != nullptr)
669 parent.mContinueButton->Enable(authorized && !GetTrackTitle().empty());
670
671 root->GetParent()->Layout();
672}
673
675{
676 wxString ret { trackTitle->GetValue() };
677 ret.Trim(true).Trim(false);
678 return ret;
679}
680
682{
683 return !GetTrackTitle().empty();
684}
685
687{
688 root = s.StartInvisiblePanel(16);
689 root->Hide();
690 s.StartVerticalLay(wxEXPAND, 1);
691 {
692 s.SetBorder(0);
693
694 title = s.AddVariableText(XO("Preparing audio..."));
695 s.AddSpace(0, 16, 0);
696
697 progress = safenew wxGauge { s.GetParent(), wxID_ANY, 100 };
698 s.AddWindow(progress, wxEXPAND);
699
700 timePanel = s.StartInvisiblePanel();
701 {
702 s.AddSpace(0, 16, 0);
703
704 s.StartWrapLay();
705 {
706 s.AddFixedText(XO("Elapsed Time:"));
707 elapsedTime = s.AddVariableText(Verbatim(" 00:00:00"));
708 }
709 s.EndWrapLay();
710
711 s.StartWrapLay();
712 {
713 s.AddFixedText(XO("Remaining Time:"));
714 remainingTime = s.AddVariableText(Verbatim(" 00:00:00"));
715 }
716 s.EndWrapLay();
717 }
719
720 s.AddSpace(0, 16, 0);
721
723 }
724
725 s.EndVerticalLay();
727
728 wxFont font = elapsedTime->GetFont();
729 font.MakeBold();
730
731 elapsedTime->SetFont(font);
732 remainingTime->SetFont(font);
733}
734
735namespace
736{
737auto hooked = []
738{
741 {
742 const auto window = &GetProjectFrame(project);
743
744 sync::CloudLocationDialog locationDialog {
746 };
747
748 const auto result = locationDialog.ShowDialog();
749
752
755
756 ShareAudioDialog shareDialog { project, window };
757 shareDialog.ShowModal();
758
760 },
761 1000);
762 return true;
763}();
764} // namespace
765} // namespace audacity::cloud::audiocom
wxT("CloseDown"))
Toolkit-neutral facade for basic user interface services.
Declare functions to perform UTF-8 to std::wstring conversions.
int min(int a, int b)
#define str(a)
ExportResult
Definition: ExportTypes.h:24
XO("Cut/Copy/Paste")
XXO("&Cut/Copy/Paste Toolbar")
wxString FileExtension
File extension, not including any leading dot.
Definition: Identifier.h:224
#define safenew
Definition: MemoryX.h:9
static const auto title
an object holding per-project preferred sample rate
AUDACITY_DLL_API wxFrame & GetProjectFrame(AudacityProject &project)
Get the top-level window associated with the project (as a wxFrame only, when you do not need to use ...
accessors for certain important windows associated with each project
@ eIsCreating
Definition: ShuttleGui.h:37
TranslatableString label
Definition: TagsEditor.cpp:165
const auto tracks
const auto project
declares abstract base class Track, TrackList, and iterators over TrackList
TranslatableString Verbatim(wxString str)
Require calls to the one-argument constructor to go through this distinct global function name.
Wrap wxMessageDialog so that caption IS translatable.
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
const wxString & GetProjectName() const
Definition: Project.cpp:100
static ExportPluginRegistry & Get()
std::vector< std::tuple< ExportOptionID, ExportValue > > Parameters
Definition: ExportPlugin.h:93
ExportTaskBuilder & SetPlugin(const ExportPlugin *plugin, int format=0) noexcept
Definition: Export.cpp:59
ExportTaskBuilder & SetParameters(ExportProcessor::Parameters parameters) noexcept
Definition: Export.cpp:47
ExportTaskBuilder & SetNumChannels(unsigned numChannels) noexcept
Definition: Export.cpp:53
ExportTaskBuilder & SetSampleRate(double sampleRate) noexcept
Definition: Export.cpp:72
ExportTaskBuilder & SetFileName(const wxFileName &filename)
Definition: Export.cpp:33
ExportTaskBuilder & SetRange(double t0, double t1, bool selectedOnly=false) noexcept
Definition: Export.cpp:39
static void RegisterExportHook(ExportHook hook, Priority=DEFAULT_EXPORT_HOOK_PRIORITY)
Definition: ExportUtils.cpp:60
static ProjectRate & Get(AudacityProject &project)
Definition: ProjectRate.cpp:28
void SetBorder(int Border)
Definition: ShuttleGui.h:495
void EndVerticalLay()
void EndInvisiblePanel()
wxPanel * StartInvisiblePanel(int border=0)
wxWindow * GetParent()
Definition: ShuttleGui.h:502
wxTextCtrl * AddTextBox(const TranslatableString &Caption, const wxString &Value, const int nChars)
Definition: ShuttleGui.cpp:659
void StartVerticalLay(int iProp=1)
wxButton * AddButton(const TranslatableString &Text, int PositionFlags=wxALIGN_CENTRE, bool setDefault=false)
Definition: ShuttleGui.cpp:362
void EndHorizontalLay()
void StartWrapLay(int PositionFlags=wxEXPAND, int iProp=0)
void StartHorizontalLay(int PositionFlags=wxALIGN_CENTRE, int iProp=1)
wxWindow * AddWindow(wxWindow *pWindow, int PositionFlags=wxALIGN_CENTRE)
Definition: ShuttleGui.cpp:301
void AddFixedText(const TranslatableString &Str, bool bCenter=false, int wrapWidth=0)
Definition: ShuttleGui.cpp:442
wxStaticText * AddVariableText(const TranslatableString &Str, bool bCenter=false, int PositionFlags=0, int wrapWidth=0)
Definition: ShuttleGui.cpp:465
Derived from ShuttleGuiBase, an Audacity specific class for shuttling data to and from GUI.
Definition: ShuttleGui.h:640
wxSizerItem * AddSpace(int width, int height, int prop=0)
ShuttleGui & Prop(int iProp)
Definition: ShuttleGui.h:733
A flat linked list of tracks supporting Add, Remove, Clear, and Contains, serialization of the list o...
Definition: Track.h:850
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:950
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:314
Holds a msgid for the translation catalog; may also bind format arguments.
A Track that contains audio waveform data.
Definition: WaveTrack.h:203
rapidjson::Document GetExportConfig(const std::string &exporterName) const
Export configuration suitable for the mime type provided.
void SetStatusString(const TranslatableString &str) override
ShareAudioDialog(AudacityProject &project, wxWindow *parent=nullptr)
void UpdateProgress(uint64_t current, uint64_t total)
struct audacity::cloud::audiocom::ShareAudioDialog::InitialStatePanel mInitialStatePanel
struct audacity::cloud::audiocom::ShareAudioDialog::ProgressPanel mProgressPanel
std::unique_ptr< ExportProgressUpdater > mExportProgressUpdater
A unique_ptr like class that holds a pointer to UploadOperation.
Service, responsible for uploading audio files to audio.com.
bool OpenInDefaultBrowser(const wxString &url)
Open an URL in default browser.
Definition: BasicUI.cpp:240
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:208
void ShowErrorDialog(const WindowPlacement &placement, const TranslatableString &dlogTitle, const TranslatableString &message, const ManualPageID &helpPage, const ErrorDialogOptions &options={})
Show an error dialog with a link to the manual for further help.
Definition: BasicUI.h:264
void Yield()
Dispatch waiting events, including actions enqueued by CallAfter.
Definition: BasicUI.cpp:219
void ExceptionWrappedCall(Callable callable)
double GetRate(const Track &track)
Definition: TimeTrack.cpp:182
void SetTimeLabel(wxStaticText *label, std::chrono::milliseconds time)
AuthorizationHandler & GetAuthorizationHandler()
UserService & GetUserService()
OAuthService & GetOAuthService()
Returns the instance of the OAuthService.
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
Options for variations of error dialogs; the default is for modal dialogs.
Definition: BasicUI.h:52
This structure represents an upload error as returned by the server.
Definition: UploadService.h:28
std::vector< AdditionalError > additionalErrors
Definition: UploadService.h:36
This structure represents the payload associated with successful upload.
Definition: UploadService.h:41
std::string audioUrl
URL to the uploaded audio.
Definition: UploadService.h:49