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 <wx/bmpbuttn.h>
14#include <wx/button.h>
15#include <wx/clipbrd.h>
16#include <wx/gauge.h>
17#include <wx/stattext.h>
18#include <wx/statline.h>
19#include <wx/textctrl.h>
20
21#include "AllThemeResources.h"
22#include "BasicUI.h"
23#include "MemoryX.h"
24#include "Project.h"
25#include "ShuttleGui.h"
26#include "Theme.h"
27#include "Track.h"
28
29#include "ServiceConfig.h"
30#include "OAuthService.h"
31#include "UploadService.h"
32#include "UserService.h"
33
35#include "CloudExporterPlugin.h"
36
38#include "LinkAccountDialog.h"
39#include "UserImage.h"
40
41#include "CodeConversions.h"
42
43#include "export/Export.h"
45
46#include "widgets/HelpSystem.h"
47
48#ifdef HAS_CUSTOM_URL_HANDLING
49#include "URLSchemesRegistry.h"
50#endif
51
52namespace cloud::audiocom
53{
54namespace
55{
56BoolSetting wasOpened { L"/cloud/audiocom/wasOpened", false };
57
58const wxSize avatarSize = { 32, 32 };
59
61{
62 const auto tempPath = GetUploadTempPath();
63
64 wxFileName fileName(
65 tempPath,
66 wxString::Format(
67 "%lld", std::chrono::system_clock::now().time_since_epoch().count()),
68 extension);
69
70 fileName.Mkdir(0700, wxPATH_MKDIR_FULL);
71
72 if (fileName.Exists())
73 {
74 if (!wxRemoveFile(fileName.GetFullPath()))
75 return {};
76 }
77
78 return fileName.GetFullPath();
79}
80
81}
82
83// A helper structures holds UploadService and UploadPromise
85{
87
89
92 {
93 }
94};
95
96// Implementation of the ProgressDialog, which is not a dialog.
97// Instead, progress is forwarded to the parent
100{
102 : mParent(parent)
103 {
104 }
105
106 void Cancel()
107 {
108 mCancelled = true;
109 }
110
112 {
113 return mCancelled;
114 }
115
117 unsigned long long numerator, unsigned long long denominator,
118 const TranslatableString&) override
119 {
120 mParent.UpdateProgress(numerator, denominator);
121
122 const auto now = Clock::now();
123
124 // Exporter polls in the main thread. To make the dialog responsive
125 // periodic yielding is required
126 if ((now - mLastYield > std::chrono::milliseconds(50)) || (numerator == denominator))
127 {
129 mLastYield = now;
130 }
131
134 }
135
136 void SetMessage(const TranslatableString&) override
137 {
138 }
139
140 void SetDialogTitle(const TranslatableString&) override
141 {
142 }
143
144 void Reinit() override
145 {
146 }
147
149
150 using Clock = std::chrono::steady_clock;
151 Clock::time_point mLastYield;
152
153 bool mCancelled { false };
154};
155
158 parent, wxID_ANY, XO("Share Audio"), wxDefaultPosition, { 480, 250 },
159 wxDEFAULT_DIALOG_STYLE)
160 , mProject(project)
161 , mServices(std::make_unique<Services>())
162{
164
165 ShuttleGui s(this, eIsCreating);
166
167 s.StartVerticalLay();
168 {
169 Populate(s);
170 }
171 s.EndVerticalLay();
172
173 Layout();
174 Fit();
175 Centre();
176
177 SetMinSize(GetSize());
178 SetMaxSize({ GetSize().x, -1 });
179
180 mContinueAction = [this]()
181 {
182 if (mInitialStatePanel.root->IsShown())
183 StartUploadProcess();
184 };
185
186 Bind(
187 wxEVT_CHAR_HOOK,
188 [this](auto& evt)
189 {
190 if (!IsEscapeKey(evt))
191 {
192 evt.Skip();
193 return;
194 }
195
196 if (mCancelButton->IsShown())
197 OnCancel();
198 else
199 OnClose();
200 });
201}
202
204{
206 // Clean up the temp file when the dialog is closed
207 if (!mFilePath.empty() && wxFileExists(mFilePath))
208 wxRemoveFile(mFilePath);
209}
210
212{
215
216 s.StartHorizontalLay(wxEXPAND, 0);
217 {
219 {
220 s.SetBorder(2);
221 s.StartHorizontalLay(wxEXPAND, 0);
222 {
223 s.AddSpace(0, 0, 1);
224
225 mCancelButton = s.AddButton(XXO("&Cancel"));
226 mCancelButton->Bind(wxEVT_BUTTON, [this](auto) { OnCancel(); });
227
228 mCloseButton = s.AddButton(XXO("&Close"));
229 mCloseButton->Bind(wxEVT_BUTTON, [this](auto) { OnClose(); });
230
231 s.AddSpace(4, 0, 0);
232
233 mContinueButton = s.AddButton(XXO("C&ontinue"));
234 mContinueButton->Bind(wxEVT_BUTTON, [this](auto) { OnContinue(); });
235
236 mGotoButton = s.AddButton(XXO("&Go to my file"));
237 }
239 }
241 }
243
244 // This two buttons are only used in the end of
245 // authorised upload flow
246 mGotoButton->Hide();
247 mCloseButton->Hide();
248}
249
251{
252 const auto hasExportStarted = mExportProgressHelper != nullptr;
253 const auto hasUploadStarted = !!mServices->uploadPromise;
254
255 if (mInProgress)
256 {
257 AudacityMessageDialog dlgMessage(
258 this, XO("Are you sure you want to cancel?"), XO("Cancel upload to Audio.com"),
259 wxYES_NO | wxICON_QUESTION | wxNO_DEFAULT | wxSTAY_ON_TOP);
260
261 const auto result = dlgMessage.ShowModal();
262
263 if (result != wxID_YES)
264 return;
265
266 // If export has started, notify it that it should be canceled
267 if (mExportProgressHelper != nullptr)
268 static_cast<ExportProgressHelper&>(*mExportProgressHelper).Cancel();
269 }
270
271
272 // If upload was started - ask it to discard the result.
273 // The result should be discarded even after the upload has finished
274 if (mServices->uploadPromise)
275 mServices->uploadPromise->DiscardResult();
276
277 EndModal(wxID_CANCEL);
278}
279
281{
283}
284
286{
287 EndModal(wxID_CLOSE);
288}
289
290
292{
293 mExportProgressHelper = std::make_unique<ExportProgressHelper>(*this);
294
295 auto exporter = CreatePreferredExporter(GetServiceConfig().GetPreferredAudioFormats(), mProject);
296
297 if (!exporter)
298 return {};
299
300 const auto path = GenerateTempPath(exporter->GetFileExtension());
301
302 if (path.empty())
303 return {};
304
305
307 exporter->OnBeforeExport();
308
309 auto cleanupExporter = finally([&]() { exporter->OnAfterExport(); });
310
311 Exporter e { const_cast<AudacityProject&>(mProject) };
312
313 auto& tracks = TrackList::Get(mProject);
314
315 const double t0 = 0.0;
316 const double t1 = tracks.GetEndTime();
317
318 const int nChannels = (tracks.Any() - &Track::IsLeader).empty() ? 1 : 2;
319
320 const bool success = e.Process(
321 nChannels, // numChannels,
322 exporter->GetExporterID(), // type,
323 path, // full path,
324 false, // selectedOnly,
325 t0, // t0
326 t1, // t1
327 mExportProgressHelper // progress dialog
328 );
329
330 if (!success && wxFileExists(path))
331 // Try to remove the file if exporting has failed (or was canceled)
332 wxRemoveFile(path);
333
334 return success ? path : wxString {};
335}
336
338{
339 mInProgress = true;
340
341 mInitialStatePanel.root->Hide();
342 mProgressPanel.root->Show();
343
345 mProgressPanel.info->Hide();
346
347 mContinueButton->Hide();
348
349 Layout();
350 Fit();
351
353
355
356 if (mFilePath.empty())
357 {
359 .WasCancelled())
360 {
362 }
363
364 return;
365 }
366
367 mProgressPanel.title->SetLabel(XO("Uploading audio...").Translation());
369
370 mServices->uploadPromise = mServices->uploadService.Upload(
371 mFilePath,
373 [this](const auto& result)
374 {
375 CallAfter(
376 [this, result]()
377 {
378 mInProgress = false;
379
380 if (result.result == UploadOperationCompleted::Result::Success)
381 HandleUploadSucceeded(result.finishUploadURL, result.audioSlug);
382 else if (result.result != UploadOperationCompleted::Result::Aborted)
383 HandleUploadFailed(result.errorMessage);
384 });
385 },
386 [this](auto current, auto total)
387 {
388 CallAfter(
389 [this, current, total]()
390 {
391 UpdateProgress(current, total);
392 });
393 });
394}
395
396void ShareAudioDialog::HandleUploadSucceeded(
397 std::string_view finishUploadURL, std::string_view audioSlug)
398{
399 mProgressPanel.timePanel->Hide();
400 mProgressPanel.title->SetLabel(XO("Upload complete!").Translation());
401 mProgressPanel.info->Show();
402
403 if (!GetOAuthService().HasAccessToken())
404 {
405 mProgressPanel.info->SetLabel(
406 "By pressing continue, you will be taken to audio.com and given a shareable link.");
407 mProgressPanel.info->Wrap(mProgressPanel.root->GetSize().GetWidth());
408
409 mContinueAction = [this, url = std::string(finishUploadURL)]()
410 {
411 EndModal(wxID_CLOSE);
412 OpenInDefaultBrowser({ url });
413 };
414
415 mContinueButton->Show();
416 }
417 else
418 {
419 auto shareableLink = wxString::Format(
420 "https://audio.com/%s/%s", GetUserService().GetUserSlug(),
421 audacity::ToWXString(audioSlug));
422
423 mGotoButton->Show();
424 mCloseButton->Show();
425 mCancelButton->Hide();
426
427 mGotoButton->Bind(
428 wxEVT_BUTTON,
429 [this, url = shareableLink](auto)
430 {
431 EndModal(wxID_CLOSE);
432 OpenInDefaultBrowser({ url });
433 });
434
435 mProgressPanel.link->SetValue(shareableLink);
436 mProgressPanel.linkPanel->Show();
437 }
438
439 Layout();
440 Fit();
441}
442
443void ShareAudioDialog::HandleUploadFailed(std::string_view errorMessage)
444{
445 EndModal(wxID_ABORT);
446
448 {}, XO("Upload error"),
449 XO("We are unable to upload this file. Please try again and make sure to link to your audio.com account before uploading."),
450 {},
452 audacity::ToWString(errorMessage)));
453}
454
455void ShareAudioDialog::HandleExportFailure()
456{
457 EndModal(wxID_ABORT);
458
460 {}, XO("Export error"),
461 XO("We are unable to prepare this file for uploading."), {},
463}
464
465void ShareAudioDialog::ResetProgress()
466{
467 mStageStartTime = Clock::now();
468 mLastUIUpdateTime = mStageStartTime;
469
470 mProgressPanel.elapsedTime->SetLabel(" 00:00:00");
471 mProgressPanel.remainingTime->SetLabel(" 00:00:00");
472 mProgressPanel.progress->SetValue(0);
473
474 mLastProgressValue = 0;
475
477}
478
479namespace
480{
481void SetTimeLabel(wxStaticText* label, std::chrono::milliseconds time)
482{
483 wxTimeSpan tsElapsed(0, 0, 0, time.count());
484
485 label->SetLabel(tsElapsed.Format(wxT(" %H:%M:%S")));
486 label->SetName(label->GetLabel());
487 label->Update();
488}
489}
490
491void ShareAudioDialog::UpdateProgress(uint64_t current, uint64_t total)
492{
493 using namespace std::chrono;
494
495 const auto now = Clock::now();
496
497 if (current == 0)
498 return;
499
500 if (current > total)
501 current = total;
502
503 if (mLastProgressValue != current)
504 {
505 constexpr int scale = 10000;
506
507 mLastProgressValue = static_cast<int>(current);
508
509 mProgressPanel.progress->SetRange(scale);
510 mProgressPanel.progress->SetValue((current * scale) / total);
511
512 if (current == total && mServices->uploadPromise)
513 {
514 mProgressPanel.timePanel->Hide();
515 mProgressPanel.title->SetLabel(XO("Finalizing upload...").Translation());
516 }
517 }
518
519 const auto elapsedSinceUIUpdate = now - mLastUIUpdateTime;
520
521 constexpr auto uiUpdateTimeout = 500ms;
522
523 if (elapsedSinceUIUpdate < uiUpdateTimeout && current < total)
524 return;
525
526 mLastUIUpdateTime = now;
527
528 const auto elapsed = duration_cast<milliseconds>(now - mStageStartTime);
529
530 SetTimeLabel(mProgressPanel.elapsedTime, elapsed);
531
532 const auto estimate = elapsed * total / current;
533 const auto remains = estimate - elapsed;
534
536 mProgressPanel.remainingTime,
537 std::chrono::duration_cast<std::chrono::milliseconds>(remains));
538}
539
540ShareAudioDialog::InitialStatePanel::InitialStatePanel()
541 : mUserDataChangedSubscription(
542 GetUserService().Subscribe([this](const auto&) { UpdateUserData(); }))
543{
544}
545
547 ShuttleGui& s)
548{
549 root = s.StartInvisiblePanel();
550 s.StartVerticalLay(wxEXPAND, 1);
551 {
552 s.SetBorder(16);
553
554 s.StartHorizontalLay(wxEXPAND, 0);
555 {
556 avatar = safenew UserImage(s.GetParent(), avatarSize);
557
558 s.AddWindow(avatar);
559
560 s.StartVerticalLay(wxEXPAND, 1);
561 {
562 s.SetBorder(0);
563 s.AddSpace(0, 0, 1);
564 name = s.AddVariableText(XO("Anonymous"));
565 s.AddSpace(0, 0, 1);
566 }
567 s.EndVerticalLay();
568
569 s.AddSpace(0, 0, 1);
570
571 s.StartVerticalLay(wxEXPAND, 1);
572 {
573 s.AddSpace(0, 0, 1);
574
575 s.SetBorder(16);
576 oauthButton = s.AddButton(XXO("&Link Account"));
577 oauthButton->Bind(
578 wxEVT_BUTTON, [this](auto) { OnLinkButtonPressed(); });
579 s.AddSpace(0, 0, 1);
580 }
581 s.EndVerticalLay();
582 }
584
585 s.SetBorder(0);
586
587 s.AddWindow(safenew wxStaticLine { s.GetParent() }, wxEXPAND);
588
589 if (!wasOpened.Read())
590 PopulateFirstTimeNotice(s);
591 else
592 {
593 s.AddSpace(16);
594 s.StartHorizontalLay(wxEXPAND, 0);
595 {
596 s.AddSpace(30, 0, 0);
597 s.AddFixedText(XO("Press \"Continue\" to upload to audio.com"));
598 }
600 }
601
602 }
603 s.EndVerticalLay();
605
606 UpdateUserData();
607}
608
610{
611 s.AddSpace(16);
613 s.SetBorder(30);
614 {
615 AccessibleLinksFormatter privacyPolicy(XO(
616 "Your audio will be uploaded to our sharing service: %s,%%which requires a free account to use.\n\nIf you have problems uploading, try the Link Account button."));
617
618 privacyPolicy.FormatLink(
619 L"%s", XO("audio.com"),
620 "https://audio.com");
621
622 privacyPolicy.FormatLink(
623 L"%%", TranslatableString {},
625
626 privacyPolicy.Populate(s);
627 }
629
630 wasOpened.Write(true);
631 gPrefs->Flush();
632}
633
635{
636 auto parent = root->GetParent();
637 parent->Freeze();
638
639 auto layoutUpdater = finally(
640 [parent = root->GetParent(), this]()
641 {
642 oauthButton->Fit();
643 parent->Layout();
644
645 parent->Thaw();
646 });
647
648 auto& oauthService = GetOAuthService();
649
650 if (!oauthService.HasRefreshToken())
651 {
652 name->SetLabel(XO("Anonymous").Translation());
653 avatar->SetBitmap(theTheme.Bitmap(bmpAnonymousUser));
654 oauthButton->SetLabel(XXO("&Link Account").Translation());
655
656 return;
657 }
658
659 if (!oauthService.HasAccessToken())
660 oauthService.ValidateAuth({});
661
662 oauthButton->SetLabel(XXO("&Unlink Account").Translation());
663
664 auto& userService = GetUserService();
665
666 const auto displayName = userService.GetDisplayName();
667
668 if (!displayName.empty())
669 name->SetLabel(displayName);
670
671 const auto avatarPath = userService.GetAvatarPath();
672
673 if (!avatarPath.empty())
674 avatar->SetBitmap(avatarPath);
675 else
676 avatar->SetBitmap(theTheme.Bitmap(bmpAnonymousUser));
677}
678
680{
681 auto& oauthService = GetOAuthService();
682
683 if (oauthService.HasAccessToken())
684 oauthService.UnlinkAccount();
685 else
686 {
688 { audacity::ToWXString(GetServiceConfig().GetOAuthLoginPage()) });
689
690#ifdef HAS_CUSTOM_URL_HANDLING
692#endif
693 {
694 LinkAccountDialog dlg(root);
695 dlg.ShowModal();
696 }
697 }
698}
699
701{
702 root = s.StartInvisiblePanel(16);
703 root->Hide();
704 s.StartVerticalLay(wxEXPAND, 1);
705 {
706 s.SetBorder(0);
707
708 title = s.AddVariableText(XO("Preparing audio..."));
709 s.AddSpace(0, 16, 0);
710
711 progress = safenew wxGauge { s.GetParent(), wxID_ANY, 100 };
712 s.AddWindow(progress, wxEXPAND);
713
714 timePanel = s.StartInvisiblePanel();
715 {
716 s.AddSpace(0, 16, 0);
717
718 s.StartWrapLay();
719 {
720 s.AddFixedText(XO("Elapsed Time:"));
721 elapsedTime = s.AddVariableText(Verbatim(" 00:00:00"));
722 }
723 s.EndWrapLay();
724
725 s.StartWrapLay();
726 {
727 s.AddFixedText(XO("Remaining Time:"));
728 remainingTime = s.AddVariableText(Verbatim(" 00:00:00"));
729 }
730 s.EndWrapLay();
731 }
733
734 linkPanel = s.StartInvisiblePanel();
735 {
736 s.AddSpace(0, 16, 0);
737
738 s.AddFixedText(XO("Shareable link"));
739
740 s.StartHorizontalLay(wxEXPAND, 0);
741 {
742 link = s.AddTextBox(TranslatableString {}, "https://audio.com", 0);
743 link->SetName(XO("Shareable link").Translation());
744 link->SetEditable(false);
745 link->SetMinSize({ 360, -1 });
746
747 s.AddSpace(1, 0, 1);
748
749 copyButton = s.AddButton(XO("Copy"));
750 copyButton->Bind(
751 wxEVT_BUTTON,
752 [this](auto)
753 {
754 if (wxTheClipboard->Open())
755 {
756 wxTheClipboard->SetData(
757 safenew wxTextDataObject(link->GetValue()));
758 wxTheClipboard->Close();
759 }
760 });
761 }
763 }
765
766 s.AddSpace(0, 16, 0);
767 info = s.AddVariableText(XO("Only people you share this link with can access your audio"));
768 }
769 s.EndVerticalLay();
771
772 wxFont font = elapsedTime->GetFont();
773 font.MakeBold();
774
775 elapsedTime->SetFont(font);
776 remainingTime->SetFont(font);
777}
778} // namespace cloud::audiocom
wxT("CloseDown"))
Toolkit-neutral facade for basic user interface services.
Declare functions to perform UTF-8 to std::wstring conversions.
static TransactionScope::Factory::Scope scope
const TranslatableString name
Definition: Distortion.cpp:82
void OpenInDefaultBrowser(const URLString &link)
Definition: HelpSystem.cpp:532
wxString FileExtension
File extension, not including any leading dot.
Definition: Identifier.h:224
#define XXO(s)
Definition: Internat.h:44
#define XO(s)
Definition: Internat.h:31
#define safenew
Definition: MemoryX.h:10
static const auto title
FileConfig * gPrefs
Definition: Prefs.cpp:71
@ eIsCreating
Definition: ShuttleGui.h:39
TranslatableString label
Definition: TagsEditor.cpp:163
THEME_API Theme theTheme
Definition: Theme.cpp:82
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:89
const wxString & GetProjectName() const
Definition: Project.cpp:100
Abstraction of a progress dialog with well defined time-to-completion estimate.
Definition: BasicUI.h:154
This specialization of Setting for bool adds a Toggle method to negate the saved value.
Definition: Prefs.h:339
virtual bool Flush(bool bCurrentOnly=false) wxOVERRIDE
Definition: FileConfig.cpp:143
bool Write(const T &value)
Write value to config and return true if successful.
Definition: Prefs.h:252
bool Read(T *pVar) const
overload of Read returning a boolean that is true if the value was previously defined *‍/
Definition: Prefs.h:200
Makes temporary changes to preferences, then rolls them back at destruction.
Definition: Prefs.h:115
void SetBorder(int Border)
Definition: ShuttleGui.h:486
void EndVerticalLay()
void EndInvisiblePanel()
wxPanel * StartInvisiblePanel(int border=0)
wxWindow * GetParent()
Definition: ShuttleGui.h:493
wxTextCtrl * AddTextBox(const TranslatableString &Caption, const wxString &Value, const int nChars)
Definition: ShuttleGui.cpp:639
void StartVerticalLay(int iProp=1)
wxButton * AddButton(const TranslatableString &Text, int PositionFlags=wxALIGN_CENTRE, bool setDefault=false)
Definition: ShuttleGui.cpp:361
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:300
void AddFixedText(const TranslatableString &Str, bool bCenter=false, int wrapWidth=0)
Definition: ShuttleGui.cpp:441
wxStaticText * AddVariableText(const TranslatableString &Str, bool bCenter=false, int PositionFlags=0, int wrapWidth=0)
Definition: ShuttleGui.cpp:464
Derived from ShuttleGuiBase, an Audacity specific class for shuttling data to and from GUI.
Definition: ShuttleGui.h:628
wxSizerItem * AddSpace(int width, int height, int prop=0)
wxBitmap & Bitmap(int iIndex)
bool IsLeader() const
Definition: Track.cpp:405
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:486
Holds a msgid for the translation catalog; may also bind format arguments.
bool IsURLHandlingSupported() const noexcept
Returns true, if Audacity can handle custom URLs.
static URLSchemesRegistry & Get()
Retrieves the registry instance.
void UpdateProgress(uint64_t current, uint64_t total)
struct cloud::audiocom::ShareAudioDialog::InitialStatePanel mInitialStatePanel
ShareAudioDialog(AudacityProject &project, wxWindow *parent=nullptr)
struct cloud::audiocom::ShareAudioDialog::ProgressPanel mProgressPanel
std::function< void()> mContinueAction
std::unique_ptr< BasicUI::ProgressDialog > mExportProgressHelper
std::unique_ptr< Services > mServices
A unique_ptr like class that holds a pointer to UploadOperation.
Definition: UploadService.h:75
Service, responsible for uploading audio files to audio.com.
Definition: UploadService.h:97
ProgressResult
Definition: BasicUI.h:145
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:206
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:254
void Yield()
Dispatch waiting events, including actions enqueued by CallAfter.
Definition: BasicUI.cpp:217
void OnClose(wxCommandEvent &e)
std::wstring ToWString(const std::string &str)
wxString ToWXString(const std::string &str)
void SetTimeLabel(wxStaticText *label, std::chrono::milliseconds time)
AuthorizationHandler & GetAuthorizationHandler()
wxString GetUploadTempPath()
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
OAuthService & GetOAuthService()
Returns the instance of the OAuthService.
UserService & GetUserService()
std::unique_ptr< cloud::CloudExporterPlugin > CreatePreferredExporter(const MimeTypesList &mimeTypes, const AudacityProject &project)
Options for variations of error dialogs; the default is for modal dialogs.
Definition: BasicUI.h:49
void SetDialogTitle(const TranslatableString &) override
Change the dialog's title.
void SetMessage(const TranslatableString &) override
Change an existing dialog's message.
BasicUI::ProgressResult Poll(unsigned long long numerator, unsigned long long denominator, const TranslatableString &) override
Update the bar and poll for clicks. Call only on the main thread.