Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2026 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "saveprojectscenario.h"
24 :
25 : #include <QBuffer>
26 : #include <QFile>
27 : #include <QFileInfo>
28 : #include <QTemporaryFile>
29 : #include <QUrl>
30 :
31 : #include "defer.h"
32 : #include "translation.h"
33 :
34 : #include "cloud/clouderrors.h"
35 : #include "cloud/qml/Muse/Cloud/enums.h"
36 : #include "engraving/infrastructure/mscio.h"
37 :
38 : #include "notation/imasternotation.h"
39 : #include "notation/inotation.h"
40 : #include "notation/inotationinteraction.h"
41 :
42 : #include "inotationproject.h"
43 : #include "projecterrors.h"
44 : #include "types/projecturis.h"
45 :
46 : #include "log.h"
47 :
48 : using namespace mu::project;
49 : using namespace mu::notation;
50 : using namespace muse;
51 : using muse::async::Promise;
52 :
53 : template<typename T>
54 89 : static Promise<T> resolvedPromise(const T& value)
55 : {
56 267 : return async::make_promise<T>([value](auto resolve) {
57 89 : return resolve(value);
58 178 : });
59 : }
60 :
61 : static constexpr int RETRY_SAVE_BTN_ID = int(IInteractive::Button::CustomButton);
62 : static constexpr int SAVE_AS_BTN_ID = RETRY_SAVE_BTN_ID + 1;
63 :
64 139 : INotationProjectPtr SaveProjectScenario::currentNotationProject() const
65 : {
66 139 : return projectSession()->project();
67 : }
68 :
69 28 : INotationInteractionPtr SaveProjectScenario::currentInteraction() const
70 : {
71 28 : INotationProjectPtr project = currentNotationProject();
72 28 : if (!project || !project->masterNotation() || !project->masterNotation()->notation()) {
73 0 : return nullptr;
74 : }
75 :
76 28 : return project->masterNotation()->notation()->interaction();
77 28 : }
78 :
79 50 : bool SaveProjectScenario::isBusy(BusyStatus status) const
80 : {
81 50 : return m_busyStatuses.contains(status);
82 : }
83 :
84 102 : void SaveProjectScenario::setBusy(BusyStatus status, bool isBusy)
85 : {
86 102 : bool wasBusy = m_busyStatuses.contains(status);
87 102 : if (wasBusy == isBusy) {
88 12 : return;
89 : }
90 :
91 90 : if (isBusy) {
92 45 : m_busyStatuses.insert(status);
93 : } else {
94 45 : m_busyStatuses.erase(status);
95 : }
96 :
97 90 : m_busyChanged.notify();
98 : }
99 :
100 0 : muse::async::Notification SaveProjectScenario::busyChanged() const
101 : {
102 0 : return m_busyChanged;
103 : }
104 :
105 48 : Promise<Ret> SaveProjectScenario::runIfNotBusy(BusyStatus status, const std::function<Promise<Ret>()>& flow)
106 : {
107 48 : if (isBusy(status)) {
108 3 : return resolvedPromise(make_ret(Ret::Code::Busy));
109 : }
110 :
111 45 : setBusy(status, true);
112 :
113 90 : return flow().then<Ret>(this, [this, status](const Ret& ret, auto resolve) {
114 45 : setBusy(status, false);
115 45 : return resolve(ret);
116 45 : });
117 : }
118 :
119 11 : Promise<RetVal<Val> > SaveProjectScenario::openDialog(const UriQuery& query) const
120 : {
121 22 : return async::make_promise<RetVal<Val> >([this, query](auto resolve) {
122 11 : interactive()->open(query)
123 22 : .onResolve(this, [resolve](const Val& val) {
124 9 : (void)resolve(RetVal<Val>::make_ok(val));
125 : })
126 11 : .onReject(this, [resolve](int code, const std::string& err) {
127 2 : (void)resolve(RetVal<Val>(muse::make_ret(code, err)));
128 : });
129 :
130 11 : return Promise<RetVal<Val> >::dummy_result();
131 22 : });
132 : }
133 :
134 1 : Promise<Ret> SaveProjectScenario::shareAudio()
135 : {
136 1 : return shareAudio(AudioFile());
137 : }
138 :
139 31 : Ret SaveProjectScenario::canSaveProject() const
140 : {
141 31 : auto project = currentNotationProject();
142 31 : if (!project) {
143 0 : LOGW() << "no current project";
144 0 : return make_ret(Err::NoProjectError);
145 : }
146 :
147 31 : return project->canSave();
148 31 : }
149 :
150 5 : Promise<Ret> SaveProjectScenario::saveProject(const muse::io::path_t& path)
151 : {
152 5 : if (path.empty()) {
153 1 : return saveProject(SaveMode::Save);
154 : }
155 :
156 8 : return runIfNotBusy(BusyStatus::Saving, [this, path]() {
157 6 : return saveProjectAt(SaveLocation(SaveLocationType::Local, path));
158 4 : });
159 : }
160 :
161 18 : Promise<Ret> SaveProjectScenario::saveProject(SaveMode saveMode, SaveLocationType saveLocationType, bool force)
162 : {
163 36 : return runIfNotBusy(BusyStatus::Saving, [this, saveMode, saveLocationType, force]() -> Promise<Ret> {
164 17 : INotationProjectPtr project = currentNotationProject();
165 17 : if (!project) {
166 1 : LOGW() << "no current project";
167 1 : return resolvedPromise(make_ret(Err::NoProjectError));
168 : }
169 :
170 16 : const bool isExistingSave = saveMode == SaveMode::Save && !project->isNewlyCreated();
171 16 : const bool wantNewCloudSave = saveLocationType == SaveLocationType::Cloud && !project->isCloudProject();
172 16 : if (isExistingSave && !wantNewCloudSave) {
173 : // Under these conditions, we can save without asking...
174 7 : SaveLocation location;
175 7 : if (project->isCloudProject()) {
176 2 : location = SaveLocation(SaveLocationType::Cloud, project->cloudInfo());
177 : } else {
178 5 : location = SaveLocation(SaveLocationType::Local);
179 : }
180 7 : return saveProjectAt(location, saveMode, force);
181 7 : }
182 :
183 18 : return askSaveLocation(project, saveMode, saveLocationType)
184 9 : .then<Ret>(this, [this, saveMode, force](const RetVal<SaveLocation>& response, auto resolve) {
185 9 : if (!response.ret) {
186 7 : LOGE() << response.ret.toString();
187 7 : return resolve(response.ret);
188 : }
189 :
190 2 : saveProjectAt(response.val, saveMode, force).onResolve(this, [resolve](const Ret& ret) {
191 2 : (void)resolve(ret);
192 : });
193 :
194 2 : return Promise<Ret>::dummy_result();
195 9 : });
196 53 : });
197 : }
198 :
199 5 : Promise<Ret> SaveProjectScenario::publish()
200 : {
201 10 : return runIfNotBusy(BusyStatus::Publishing, [this]() -> Promise<Ret> {
202 5 : Ret ret = canSaveProject();
203 5 : if (!ret) {
204 : //! NOTE The answer leads nowhere here, but the flow is not over while the question is up
205 0 : return askIfUserAgreesToSaveProjectWithErrors(ret, SaveLocationType::Cloud)
206 0 : .then<Ret>(this, [ret](bool, auto resolve) {
207 0 : return resolve(ret);
208 0 : });
209 : }
210 :
211 5 : auto project = currentNotationProject();
212 :
213 10 : return askPublishLocation(project)
214 10 : .then<Ret>(this, [this, project](const RetVal<CloudProjectInfo>& info, auto resolve) {
215 5 : if (info.ret.code() == RET_CODE_CHANGE_SAVE_LOCATION_TYPE) {
216 2 : saveProjectLocallyInstead(project, SaveMode::Save).onResolve(this, [resolve](const Ret& saveRet) {
217 2 : (void)resolve(saveRet);
218 : });
219 :
220 2 : return Promise<Ret>::dummy_result();
221 : }
222 :
223 3 : if (!info.ret) {
224 0 : return resolve(info.ret);
225 : }
226 :
227 3 : AudioFile audio = exportMp3(project->masterNotation()->notation());
228 3 : if (!audio.isValid()) {
229 1 : return resolve(make_ret(Ret::Code::BadData));
230 : }
231 :
232 2 : uploadProject(info.val, audio, /*openEditUrl=*/ true, /*publishMode=*/ true)
233 2 : .onResolve(this, [resolve](const Ret& ret) {
234 2 : (void)resolve(ret);
235 : });
236 :
237 2 : return Promise<Ret>::dummy_result();
238 8 : });
239 15 : });
240 : }
241 :
242 1 : Promise<Ret> SaveProjectScenario::shareAudio(const AudioFile& existingAudio)
243 : {
244 2 : return runIfNotBusy(BusyStatus::AudioSharing, [this, existingAudio]() -> Promise<Ret> {
245 1 : auto project = currentNotationProject();
246 1 : if (!project) {
247 1 : LOGW() << "no current project";
248 1 : return resolvedPromise(make_ret(Err::NoProjectError));
249 : }
250 :
251 0 : return askShareAudioLocation(project)
252 0 : .then<Ret>(this, [this, project, existingAudio](const RetVal<CloudAudioInfo>& info, auto resolve) {
253 0 : if (!info.ret) {
254 0 : return resolve(info.ret);
255 : }
256 :
257 0 : AudioFile audio = existingAudio.isValid() ? existingAudio : exportMp3(project->masterNotation()->notation());
258 0 : if (!audio.isValid()) {
259 0 : return resolve(make_ret(Ret::Code::BadData));
260 : }
261 :
262 0 : uploadAudioToAudioCom(audio, project, info.val).onResolve(this, [resolve](const Ret& ret) {
263 0 : (void)resolve(ret);
264 : });
265 :
266 0 : return Promise<Ret>::dummy_result();
267 0 : });
268 3 : });
269 : }
270 :
271 0 : Promise<Ret> SaveProjectScenario::uploadAudioToAudioCom(const AudioFile& audio, const INotationProjectPtr& project,
272 : const CloudAudioInfo& info)
273 : {
274 0 : return async::make_promise<Ret>([this, audio, project, info](auto resolve) {
275 0 : m_uploadingAudioProgress = audioComService()->uploadAudio(audio.device, audio.format, info.name,
276 0 : project->cloudAudioInfo().url, info.visibility,
277 0 : info.replaceExisting);
278 0 : LOGD() << "Uploading audio started";
279 0 : showUploadProgressDialog();
280 :
281 0 : m_uploadingAudioProgress->progressChanged().onReceive(this, [](int64_t current, int64_t total, const std::string&) {
282 0 : if (total > 0) {
283 0 : LOGD() << "Uploading audio progress: " << current << " / " << total << " bytes";
284 : }
285 : });
286 :
287 0 : m_uploadingAudioProgress->finished().onReceive(this, [this, project, info, resolve](const ProgressResult& res) {
288 0 : LOGD() << "Uploading audio finished";
289 :
290 0 : m_uploadingAudioProgress->started().disconnect(this);
291 0 : m_uploadingAudioProgress->progressChanged().disconnect(this);
292 0 : m_uploadingAudioProgress->finished().disconnect(this);
293 :
294 0 : if (!res.ret) {
295 0 : LOGE() << res.ret.toString();
296 :
297 0 : onAudioUploadFailed(res.ret).onResolve(this, [resolve](const Ret& ret) {
298 0 : (void)resolve(ret);
299 : });
300 :
301 0 : return;
302 : }
303 :
304 0 : ValMap resMap = res.val.toMap();
305 0 : onAudioSuccessfullyUploaded(resMap["editUrl"].toQString());
306 :
307 0 : if (!info.replaceExisting) {
308 0 : CloudAudioInfo newInfo = project->cloudAudioInfo();
309 0 : newInfo.url = QUrl(resMap["url"].toQString());
310 0 : project->setCloudAudioInfo(newInfo);
311 0 : }
312 :
313 0 : (void)resolve(res.ret);
314 0 : });
315 :
316 0 : return Promise<Ret>::dummy_result();
317 0 : });
318 : }
319 :
320 2 : Promise<Ret> SaveProjectScenario::saveProjectAt(const muse::rcommand::Params& params)
321 : {
322 2 : const std::string& path = params.at("path").toString();
323 2 : if (path.empty()) {
324 1 : return resolvedPromise(make_ret(Ret::Code::BadArgs));
325 : }
326 :
327 2 : return runIfNotBusy(BusyStatus::Saving, [this, path]() {
328 2 : return saveProjectAt(SaveLocation(muse::io::path_t(path)));
329 1 : });
330 2 : }
331 :
332 28 : Promise<Ret> SaveProjectScenario::saveProjectAt(const SaveLocation& location, SaveMode saveMode, bool force)
333 : {
334 28 : INotationInteractionPtr interaction = currentInteraction();
335 28 : if (interaction && interaction->isTextEditingStarted()) {
336 1 : interaction->endEditText();
337 : }
338 :
339 28 : if (!force) {
340 26 : Ret ret = canSaveProject();
341 26 : if (!ret) {
342 10 : return askIfUserAgreesToSaveProjectWithErrors(ret, location)
343 10 : .then<Ret>(this, [this, location, saveMode, ret](bool agreed, auto resolve) {
344 5 : if (!agreed) {
345 4 : return resolve(ret);
346 : }
347 :
348 1 : doSaveProjectAt(location, saveMode).onResolve(this, [resolve](const Ret& saveRet) {
349 1 : (void)resolve(saveRet);
350 : });
351 :
352 1 : return Promise<Ret>::dummy_result();
353 5 : });
354 : }
355 26 : }
356 :
357 23 : return doSaveProjectAt(location, saveMode);
358 28 : }
359 :
360 24 : Promise<Ret> SaveProjectScenario::doSaveProjectAt(const SaveLocation& location, SaveMode saveMode)
361 : {
362 24 : if (location.isLocal()) {
363 20 : return saveProjectLocally(location.localPath(), saveMode);
364 : }
365 :
366 4 : if (location.isCloud()) {
367 3 : return saveProjectToCloud(location.cloudInfo(), saveMode);
368 : }
369 :
370 1 : return resolvedPromise(make_ret(Err::UnknownError));
371 : }
372 :
373 37 : Promise<Ret> SaveProjectScenario::saveProjectLocally(const muse::io::path_t& filePath, SaveMode saveMode, bool createBackup)
374 : {
375 37 : INotationProjectPtr project = currentNotationProject();
376 37 : if (!project) {
377 0 : return resolvedPromise(make_ret(Err::NoProjectError));
378 : }
379 :
380 37 : Ret ret = project->save(filePath, saveMode, createBackup);
381 :
382 37 : if (ret) {
383 31 : recentFilesController()->prependRecentFile(makeRecentFile(project));
384 31 : return resolvedPromise(make_ok());
385 : }
386 :
387 6 : LOGE() << ret.toString();
388 :
389 6 : if (ret.code() != (int)Err::CorruptionUponSavingError) {
390 3 : return warnScoreCouldnotBeSaved(ret);
391 : }
392 :
393 6 : return warnScoreHasBecomeCorruptedAfterSave(ret).then<Ret>(this, [this, filePath, saveMode, ret](int btn, auto resolve) {
394 3 : switch (btn) {
395 1 : case RETRY_SAVE_BTN_ID: {
396 : // Retry the save. Do not create a backup this time because the target file has been corrupted
397 : // already. Creating a backup file of a corrupted file now makes no sense and will corrupt
398 : // the healthy backup file created on the first save attempt.
399 1 : saveProjectLocally(filePath, saveMode, false /*createBackup*/).onResolve(this, [resolve](const Ret& retryRet) {
400 1 : (void)resolve(retryRet);
401 : });
402 :
403 1 : return Promise<Ret>::dummy_result();
404 : }
405 :
406 1 : case SAVE_AS_BTN_ID: {
407 : //! NOTE The save that started this one still holds the status; the new flow takes it over
408 1 : setBusy(BusyStatus::Saving, false);
409 :
410 1 : saveProject(SaveMode::SaveAs).onResolve(this, [resolve](const Ret& saveAsRet) {
411 1 : (void)resolve(saveAsRet);
412 : });
413 :
414 1 : return Promise<Ret>::dummy_result();
415 : }
416 : }
417 :
418 1 : return resolve(ret);
419 3 : });
420 37 : }
421 :
422 19 : Promise<Ret> SaveProjectScenario::saveProjectToCloud(CloudProjectInfo info, SaveMode saveMode)
423 : {
424 38 : return runIfNotBusy(BusyStatus::Uploading, [this, info, saveMode]() {
425 36 : return museScoreComService()->authorization()->checkCloudIsAvailable()
426 36 : .then<Ret>(this, [this, info, saveMode](const Ret& isCloudAvailable, auto resolve) {
427 18 : if (!isCloudAvailable) {
428 4 : warnCloudIsNotAvailable().onResolve(this, [this, info, saveMode, resolve](const Ret&) {
429 4 : INotationProjectPtr project = currentNotationProject();
430 4 : if (!project) {
431 0 : (void)resolve(make_ret(Err::NoProjectError));
432 0 : return;
433 : }
434 :
435 4 : saveCloudProjectLocally(project, info, saveMode).onResolve(this, [resolve](const Ret& ret) {
436 4 : (void)resolve(ret);
437 : });
438 4 : });
439 :
440 4 : return Promise<Ret>::dummy_result();
441 : }
442 :
443 14 : doSaveProjectToCloud(info, saveMode).onResolve(this, [resolve](const Ret& ret) {
444 14 : (void)resolve(ret);
445 : });
446 :
447 14 : return Promise<Ret>::dummy_result();
448 36 : });
449 38 : });
450 : }
451 :
452 14 : Promise<Ret> SaveProjectScenario::doSaveProjectToCloud(const CloudProjectInfo& info, SaveMode saveMode)
453 : {
454 14 : std::string dialogText = muse::trc("project/save", "Log in to MuseScore.com to save this score to the cloud.");
455 :
456 14 : return ensureAuthorization(muse::cloud::MUSESCORE_COM_CLOUD_CODE, true, dialogText)
457 28 : .then<Ret>(this, [this, info, saveMode](const RetVal<Val>& auth, auto resolve) {
458 14 : if (!auth.ret) {
459 1 : return resolve(auth.ret);
460 : }
461 :
462 13 : INotationProjectPtr project = currentNotationProject();
463 13 : if (!project) {
464 0 : return resolve(make_ret(Err::NoProjectError));
465 : }
466 :
467 : using Response = muse::cloud::SaveToCloudResponse::SaveToCloudResponse;
468 13 : if (static_cast<Response>(auth.val.toInt()) == Response::SaveLocallyInstead) {
469 2 : saveProjectLocallyInstead(project, saveMode).onResolve(this, [resolve](const Ret& ret) {
470 2 : (void)resolve(ret);
471 : });
472 :
473 2 : return Promise<Ret>::dummy_result();
474 : }
475 :
476 11 : saveAndUploadProject(project, info, saveMode).onResolve(this, [resolve](const Ret& ret) {
477 11 : (void)resolve(ret);
478 : });
479 :
480 11 : return Promise<Ret>::dummy_result();
481 41 : });
482 14 : }
483 :
484 11 : Promise<Ret> SaveProjectScenario::saveAndUploadProject(const INotationProjectPtr& project, CloudProjectInfo info, SaveMode saveMode)
485 : {
486 11 : const bool isPublic = info.visibility == muse::cloud::Visibility::Public;
487 :
488 11 : if (saveMode != SaveMode::Save) {
489 7 : return doSaveAndUploadProject(project, info, saveMode, isPublic);
490 : }
491 :
492 : // Get up-to-date visibility information
493 8 : return museScoreComService()->downloadScoreInfo(info.sourceUrl)
494 8 : .then<Ret>(this, [this, project, info, saveMode](const RetVal<muse::cloud::ScoreInfo>& scoreInfo, auto resolve) {
495 4 : CloudProjectInfo actualInfo = info;
496 :
497 4 : if (scoreInfo.ret) {
498 1 : actualInfo.name = scoreInfo.val.title;
499 1 : actualInfo.visibility = scoreInfo.val.visibility;
500 : } else {
501 6 : LOGE() << "Failed to download up-to-date score info for " << actualInfo.sourceUrl
502 3 : << "; falling back to last known name and visibility setting, namely "
503 3 : << actualInfo.name << " and " << static_cast<int>(actualInfo.visibility);
504 : }
505 :
506 4 : const bool isPublic = actualInfo.visibility == muse::cloud::Visibility::Public;
507 4 : if (!isPublic) {
508 3 : doSaveAndUploadProject(project, actualInfo, saveMode, isPublic).onResolve(this, [resolve](const Ret& ret) {
509 3 : (void)resolve(ret);
510 : });
511 :
512 3 : return Promise<Ret>::dummy_result();
513 : }
514 :
515 1 : warnBeforeSavingToExistingPubliclyVisibleCloudProject()
516 1 : .onResolve(this, [this, project, actualInfo, saveMode, isPublic, resolve](bool agreed) {
517 1 : if (!agreed) {
518 1 : (void)resolve(make_ret(Ret::Code::Cancel));
519 1 : return;
520 : }
521 :
522 0 : doSaveAndUploadProject(project, actualInfo, saveMode, isPublic).onResolve(this, [resolve](const Ret& ret) {
523 0 : (void)resolve(ret);
524 : });
525 : });
526 :
527 1 : return Promise<Ret>::dummy_result();
528 8 : });
529 : }
530 :
531 10 : Promise<Ret> SaveProjectScenario::doSaveAndUploadProject(const INotationProjectPtr& project, const CloudProjectInfo& info,
532 : SaveMode saveMode, bool isPublic)
533 : {
534 20 : return needGenerateAudio(isPublic)
535 20 : .then<Ret>(this, [this, project, info, saveMode, isPublic](const RetVal<bool>& need, auto resolve) {
536 10 : if (!need.ret) {
537 0 : return resolve(need.ret);
538 : }
539 :
540 10 : const bool generateAudio = need.val;
541 :
542 10 : saveCloudProjectLocally(project, info, saveMode)
543 10 : .onResolve(this, [this, project, info, isPublic, generateAudio, resolve](const Ret& saveRet) {
544 10 : if (!saveRet) {
545 0 : (void)resolve(saveRet);
546 1 : return;
547 : }
548 :
549 10 : AudioFile audio;
550 10 : if (generateAudio) {
551 1 : audio = exportMp3(project->masterNotation()->notation());
552 1 : if (!audio.isValid()) {
553 1 : (void)resolve(make_ret(Ret::Code::BadData));
554 1 : return;
555 : }
556 : }
557 :
558 9 : uploadProject(info, audio, /*openEditUrl=*/ isPublic, /*publishMode=*/ false)
559 9 : .onResolve(this, [this, resolve](const Ret& ret) {
560 9 : if (ret) {
561 5 : m_numberOfSavesToCloud++;
562 : }
563 9 : (void)resolve(ret);
564 : });
565 10 : });
566 :
567 10 : return Promise<Ret>::dummy_result();
568 20 : });
569 : }
570 :
571 14 : Promise<Ret> SaveProjectScenario::saveCloudProjectLocally(const INotationProjectPtr& project, const CloudProjectInfo& info,
572 : SaveMode saveMode)
573 : {
574 : // TODO(cloud): is this correct for all save modes?
575 14 : project->setCloudInfo(info);
576 :
577 14 : muse::io::path_t savingPath;
578 :
579 14 : if (project->isCloudProject()) {
580 3 : if (saveMode == SaveMode::Save || saveMode == SaveMode::AutoSave) {
581 3 : savingPath = project->path();
582 : }
583 : }
584 :
585 14 : if (savingPath.empty()) {
586 12 : ID scoreId = muse::cloud::idFromCloudUrl(info.sourceUrl);
587 :
588 12 : savingPath = configuration()->cloudProjectSavingPath(scoreId.toUint64());
589 : }
590 :
591 28 : return saveProjectLocally(savingPath, saveMode);
592 14 : }
593 :
594 4 : Promise<Ret> SaveProjectScenario::saveProjectLocallyInstead(const INotationProjectPtr& project, SaveMode saveMode)
595 : {
596 8 : return askLocalPath(project, saveMode)
597 4 : .then<Ret>(this, [this, saveMode](const RetVal<muse::io::path_t>& path, auto resolve) {
598 4 : if (!path.ret) {
599 2 : LOGE() << path.ret.toString();
600 2 : return resolve(path.ret);
601 : }
602 :
603 2 : saveProjectLocally(path.val, saveMode).onResolve(this, [this, resolve](const Ret& ret) {
604 2 : configuration()->setLastUsedSaveLocationType(SaveLocationType::Local);
605 :
606 2 : (void)resolve(ret);
607 : });
608 :
609 2 : return Promise<Ret>::dummy_result();
610 8 : });
611 : }
612 :
613 0 : Promise<Ret> SaveProjectScenario::alsoShareAudioCom(const AudioFile& audio)
614 : {
615 0 : if (!configuration()->showAlsoShareAudioComDialog()) {
616 0 : return shareAudio(audio);
617 : }
618 :
619 0 : UriQuery query("musescore://project/alsoshareaudiocom");
620 0 : query.addParam("rememberChoice", Val(!configuration()->hasAskedAlsoShareAudioCom()));
621 :
622 0 : auto audioHolder = std::make_shared<AudioFile>(audio);
623 :
624 0 : return openDialog(query).then<Ret>(this, [this, audioHolder](const RetVal<Val>& rv, auto resolve) {
625 0 : const AudioFile audio = *audioHolder;
626 0 : *audioHolder = AudioFile();
627 :
628 0 : DEFER {
629 0 : configuration()->setHasAskedAlsoShareAudioCom(true);
630 : };
631 :
632 0 : if (rv.val.isNull()) {
633 0 : return resolve(make_ret(Ret::Code::Cancel));
634 : }
635 :
636 0 : QVariantMap vals = rv.val.toQVariant().toMap();
637 0 : bool shareAudioCom = vals["share"].toBool();
638 0 : bool rememberChoice = vals["remember"].toBool();
639 :
640 0 : configuration()->setShowAlsoShareAudioComDialog(!rememberChoice);
641 0 : configuration()->setAlsoShareAudioCom(shareAudioCom);
642 :
643 0 : if (!shareAudioCom) {
644 0 : return resolve(make_ok());
645 : }
646 :
647 0 : shareAudio(audio).onResolve(this, [resolve](const Ret& ret) {
648 0 : (void)resolve(ret);
649 : });
650 :
651 0 : return Promise<Ret>::dummy_result();
652 0 : });
653 0 : }
654 :
655 1 : Promise<Ret> SaveProjectScenario::askAudioGenerationSettings() const
656 : {
657 3 : return openDialog(UriQuery("musescore://project/audiogenerationsettings"))
658 1 : .then<Ret>(this, [this](const RetVal<Val>& res, auto resolve) {
659 1 : if (!res.ret) {
660 0 : return resolve(res.ret);
661 : }
662 :
663 1 : configuration()->setHasAskedAudioGenerationSettings(true);
664 :
665 1 : return resolve(make_ok());
666 2 : });
667 : }
668 :
669 17 : Promise<RetVal<bool> > SaveProjectScenario::needGenerateAudio(bool isPublicUpload) const
670 : {
671 17 : if (isPublicUpload) {
672 2 : return resolvedPromise(RetVal<bool>::make_ok(true));
673 : }
674 :
675 15 : if (configuration()->hasAskedAudioGenerationSettings()) {
676 14 : return resolvedPromise(RetVal<bool>::make_ok(needGenerateAudioAccordingToSettings()));
677 : }
678 :
679 2 : return askAudioGenerationSettings().then<RetVal<bool> >(this, [this](const Ret& ret, auto resolve) {
680 1 : if (!ret) {
681 0 : return resolve(RetVal<bool>(ret));
682 : }
683 :
684 1 : return resolve(RetVal<bool>::make_ok(needGenerateAudioAccordingToSettings()));
685 1 : });
686 : }
687 :
688 15 : bool SaveProjectScenario::needGenerateAudioAccordingToSettings() const
689 : {
690 15 : switch (configuration()->generateAudioTimePeriodType()) {
691 11 : case GenerateAudioTimePeriodType::Never:
692 11 : return false;
693 1 : case GenerateAudioTimePeriodType::Always:
694 1 : return true;
695 3 : case GenerateAudioTimePeriodType::AfterCertainNumberOfSaves: {
696 3 : int requiredNumberOfSaves = configuration()->numberOfSavesToGenerateAudio();
697 3 : if (requiredNumberOfSaves <= 0) {
698 1 : LOGW() << "invalid number of saves to generate audio: " << requiredNumberOfSaves;
699 1 : return true;
700 : }
701 :
702 2 : return m_numberOfSavesToCloud % requiredNumberOfSaves == 0;
703 : }
704 : }
705 :
706 0 : return false;
707 : }
708 :
709 4 : SaveProjectScenario::AudioFile SaveProjectScenario::exportMp3(const INotationPtr notation) const
710 : {
711 4 : QString mp3Path;
712 : {
713 4 : QTemporaryFile tempFile(configuration()->temporaryMp3FilePathTemplate().toQString());
714 4 : if (!tempFile.open()) {
715 0 : LOGE() << "Could not create a temp file";
716 0 : return AudioFile();
717 : }
718 :
719 4 : mp3Path = QFileInfo(tempFile).absoluteFilePath();
720 4 : }
721 :
722 4 : LOGD() << "mp3 path: " << mp3Path;
723 :
724 4 : if (mp3Path.isEmpty()) {
725 0 : LOGE() << "mp3 path is empty";
726 0 : return AudioFile();
727 : }
728 :
729 : // In the uploaded audio file, the repeats need to be expanded
730 4 : bool wasExpandRepeats = notationConfiguration()->isPlayRepeatsEnabled();
731 4 : if (!wasExpandRepeats) {
732 4 : notationConfiguration()->setIsPlayRepeatsEnabled(true);
733 : }
734 :
735 4 : DEFER {
736 4 : if (!wasExpandRepeats) {
737 4 : notationConfiguration()->setIsPlayRepeatsEnabled(false);
738 : }
739 4 : };
740 :
741 16 : if (!exportProjectScenario()->exportScores({ notation }, mp3Path)) {
742 2 : LOGE() << "Could not export an mp3";
743 2 : fileSystem()->remove(mp3Path);
744 2 : return AudioFile();
745 : }
746 :
747 0 : std::shared_ptr<QFile> exportedFile(new QFile(mp3Path), [this](QFile* file) {
748 2 : file->close();
749 2 : fileSystem()->remove(file->fileName());
750 2 : delete file;
751 4 : });
752 :
753 2 : if (!exportedFile->open(QIODevice::ReadOnly)) {
754 0 : LOGE() << "Could not reopen exported mp3: " << mp3Path;
755 0 : return AudioFile();
756 : }
757 :
758 2 : AudioFile audio;
759 2 : audio.format = "mp3";
760 2 : audio.device = exportedFile;
761 :
762 2 : return audio;
763 8 : }
764 :
765 10 : void SaveProjectScenario::showUploadProgressDialog()
766 : {
767 10 : if (interactive()->isOpened(UPLOAD_PROGRESS_URI).val) {
768 0 : return;
769 : }
770 :
771 10 : interactive()->open(UPLOAD_PROGRESS_URI);
772 : }
773 :
774 14 : void SaveProjectScenario::closeUploadProgressDialog()
775 : {
776 14 : if (interactive()->isOpened(UPLOAD_PROGRESS_URI).val) {
777 0 : interactive()->close(UriQuery(UPLOAD_PROGRESS_URI));
778 : }
779 14 : }
780 :
781 12 : Promise<Ret> SaveProjectScenario::uploadProject(const CloudProjectInfo& info, const AudioFile& audio, bool openEditUrl, bool publishMode)
782 : {
783 24 : return async::make_promise<Ret>([this, info, audio, openEditUrl, publishMode](auto resolve) {
784 12 : INotationProjectPtr project = projectSession()->project();
785 12 : if (!project) {
786 0 : return resolve(make_ret(Err::NoProjectError));
787 : }
788 :
789 12 : auto projectData = std::make_shared<QBuffer>();
790 12 : projectData->open(QIODevice::WriteOnly);
791 :
792 12 : Ret ret = project->writeToDevice(projectData.get());
793 12 : if (!ret) {
794 2 : LOGE() << ret.toString();
795 2 : return resolve(ret);
796 : }
797 :
798 10 : projectData->close();
799 10 : projectData->open(QIODevice::ReadOnly);
800 :
801 10 : bool isFirstSave = info.sourceUrl.isEmpty();
802 :
803 20 : ProgressPtr progress = museScoreComService()->uploadScore(projectData, info.name, info.visibility, info.sourceUrl,
804 10 : info.revisionId);
805 10 : m_uploadingProjectProgress = progress;
806 :
807 10 : showUploadProgressDialog();
808 10 : LOGD() << "Uploading project started";
809 :
810 10 : progress->progressChanged().onReceive(this, [](int64_t current, int64_t total, const std::string&) {
811 0 : if (total > 0) {
812 0 : LOGD() << "Uploading project progress: " << current << " / " << total << " bytes";
813 : }
814 : });
815 :
816 10 : progress->finished().onReceive(this, [this, project, info, audio, openEditUrl, publishMode,
817 : isFirstSave, progress, resolve](const ProgressResult& res) {
818 10 : progress->progressChanged().disconnect(this);
819 10 : progress->finished().disconnect(this);
820 :
821 10 : if (!res.ret) {
822 4 : LOGE() << res.ret.toString();
823 4 : onProjectUploadFailed(res.ret, info, audio, openEditUrl, publishMode).onResolve(this, [resolve](const Ret& failRet) {
824 4 : (void)resolve(failRet);
825 : });
826 5 : return;
827 : }
828 :
829 6 : ValMap urlMap = res.val.toMap();
830 6 : QString newSourceUrl = urlMap["sourceUrl"].toQString();
831 8 : QString editUrl = openEditUrl ? urlMap["editUrl"].toQString() : QString();
832 6 : int newRevisionId = urlMap["revisionId"].toInt();
833 :
834 6 : LOGD() << "Source url received: " << newSourceUrl;
835 :
836 6 : CloudProjectInfo cpinfo = project->cloudInfo();
837 6 : if (cpinfo.sourceUrl != newSourceUrl || cpinfo.revisionId != newRevisionId) {
838 : // TODO(cloud): does this work correctly with different save modes?
839 1 : cpinfo.sourceUrl = newSourceUrl;
840 1 : cpinfo.revisionId = newRevisionId;
841 1 : project->setCloudInfo(cpinfo);
842 :
843 1 : if (!project->isNewlyCreated()) {
844 1 : project->save();
845 : }
846 :
847 1 : if (project->isCloudProject()) {
848 0 : moveProject(project, configuration()->cloudProjectPath(muse::cloud::idFromCloudUrl(cpinfo.sourceUrl).toUint64()), true);
849 : }
850 : }
851 :
852 6 : if (audio.isValid()) {
853 3 : uploadAudioToMuseScoreCom(audio, newSourceUrl, editUrl, isFirstSave, publishMode).onResolve(this,
854 2 : [resolve](const Ret& ret) {
855 1 : (void)resolve(ret);
856 : });
857 1 : return;
858 : }
859 :
860 5 : onUploadFinished(editUrl, isFirstSave, audio, publishMode).onResolve(this, [res, resolve](const Ret&) {
861 5 : (void)resolve(res.ret);
862 : });
863 9 : });
864 :
865 10 : return Promise<Ret>::dummy_result();
866 36 : });
867 : }
868 :
869 1 : Promise<Ret> SaveProjectScenario::uploadAudioToMuseScoreCom(const AudioFile& audio, const QUrl& sourceUrl, const QUrl& urlToOpen,
870 : bool isFirstSave, bool publishMode)
871 : {
872 2 : return async::make_promise<Ret>([this, audio, sourceUrl, urlToOpen, isFirstSave, publishMode](auto resolve) {
873 1 : m_uploadingAudioProgress = museScoreComService()->uploadAudio(audio.device, audio.format, sourceUrl);
874 :
875 1 : m_uploadingAudioProgress->progressChanged().onReceive(this, [](int64_t current, int64_t total, const std::string&) {
876 0 : if (total > 0) {
877 0 : LOGD() << "Uploading audio progress: " << current << " / " << total << " bytes";
878 : }
879 : });
880 :
881 1 : m_uploadingAudioProgress->finished().onReceive(this, [this, audio, urlToOpen, isFirstSave, publishMode,
882 : resolve](const ProgressResult& res) {
883 1 : LOGD() << "Uploading audio finished";
884 :
885 1 : if (!res.ret) {
886 0 : LOGE() << res.ret.toString();
887 : }
888 :
889 1 : m_uploadingAudioProgress->progressChanged().disconnect(this);
890 1 : m_uploadingAudioProgress->finished().disconnect(this);
891 :
892 1 : onUploadFinished(urlToOpen, isFirstSave, audio, publishMode).onResolve(this, [res, resolve](const Ret&) {
893 1 : (void)resolve(res.ret);
894 : });
895 : });
896 :
897 1 : return Promise<Ret>::dummy_result();
898 2 : });
899 : }
900 :
901 6 : Promise<Ret> SaveProjectScenario::onUploadFinished(const QUrl& urlToOpen, bool isFirstSave, const AudioFile& audio, bool publishMode)
902 : {
903 : //! NOTE The exported audio has to outlive the message, because publishing offers to share it too.
904 : //! It is held through a holder the offer empties: what a resolved callback captured stays alive,
905 : //! and that would keep the temporary mp3 on disk.
906 6 : auto audioHolder = std::make_shared<AudioFile>(audio);
907 :
908 6 : return onProjectSuccessfullyUploaded(urlToOpen, isFirstSave)
909 12 : .then<Ret>(this, [this, audioHolder, publishMode](const Ret&, auto resolve) {
910 6 : const AudioFile sharedAudio = *audioHolder;
911 6 : *audioHolder = AudioFile();
912 :
913 6 : if (!publishMode || !(configuration()->alsoShareAudioCom() || configuration()->showAlsoShareAudioComDialog())) {
914 6 : return resolve(make_ok());
915 : }
916 :
917 0 : alsoShareAudioCom(sharedAudio).onResolve(this, [resolve](const Ret& ret) {
918 0 : (void)resolve(ret);
919 : });
920 :
921 0 : return Promise<Ret>::dummy_result();
922 18 : });
923 6 : }
924 :
925 6 : Promise<Ret> SaveProjectScenario::onProjectSuccessfullyUploaded(const QUrl& urlToOpen, bool isFirstSave)
926 : {
927 6 : setBusy(BusyStatus::Uploading, false);
928 :
929 6 : closeUploadProgressDialog();
930 :
931 6 : if (!urlToOpen.isEmpty()) {
932 0 : platformInteractive()->openUrl(urlToOpen);
933 0 : return resolvedPromise(make_ok());
934 : }
935 :
936 6 : QUrl scoreManagerUrl = this->scoreManagerUrl();
937 :
938 6 : if (configuration()->openDetailedProjectUploadedDialog()) {
939 0 : UriQuery query("musescore://project/upload/success");
940 0 : query.addParam("scoreManagerUrl", Val(scoreManagerUrl.toString()));
941 0 : configuration()->setOpenDetailedProjectUploadedDialog(false);
942 :
943 0 : return openDialog(query).then<Ret>(this, [](const RetVal<Val>&, auto resolve) {
944 0 : return resolve(make_ok());
945 0 : });
946 0 : }
947 :
948 6 : if (!isFirstSave) {
949 3 : return resolvedPromise(make_ok());
950 : }
951 :
952 3 : IInteractive::ButtonData viewOnlineBtn(IInteractive::Button::CustomButton, muse::trc("project/save", "View online"));
953 3 : IInteractive::ButtonData okBtn = interactive()->buttonData(IInteractive::Button::Ok);
954 :
955 : std::string msg = muse::trc("project/save", "All saved changes will now update to the cloud. "
956 3 : "You can manage this file in the score manager on MuseScore.com.");
957 :
958 18 : return interactive()->info(muse::trc("global", "Success!"), msg, { viewOnlineBtn, okBtn },
959 : static_cast<int>(IInteractive::Button::Ok))
960 6 : .then<Ret>(this, [this, viewOnlineBtn, scoreManagerUrl](const IInteractive::Result& res, auto resolve) {
961 3 : if (res.isButton(viewOnlineBtn.btn)) {
962 0 : platformInteractive()->openUrl(scoreManagerUrl);
963 : }
964 :
965 3 : return resolve(make_ok());
966 3 : });
967 9 : }
968 :
969 4 : Promise<Ret> SaveProjectScenario::onProjectUploadFailed(const Ret& ret, const CloudProjectInfo& info, const AudioFile& audio,
970 : bool openEditUrl, bool publishMode)
971 : {
972 4 : setBusy(BusyStatus::Uploading, false);
973 :
974 4 : closeUploadProgressDialog();
975 :
976 : //! NOTE The exported audio has to outlive the question, because a retry uploads it again.
977 : //! It is held through a holder the answer empties: what a resolved callback captured
978 : //! stays alive, and that would keep the temporary mp3 on disk.
979 4 : auto audioHolder = std::make_shared<AudioFile>(audio);
980 :
981 4 : return showCloudSaveError(ret, info, publishMode, true)
982 8 : .then<Ret>(this, [this, ret, info, audioHolder, openEditUrl, publishMode](const Ret& userResponse, auto resolve) {
983 4 : const AudioFile audio = *audioHolder;
984 4 : *audioHolder = AudioFile();
985 :
986 4 : switch (userResponse.code()) {
987 1 : case RET_CODE_CONFLICT_RESPONSE_SAVE_AS: {
988 : // The save that started this upload still holds the status; the new flow takes it over
989 1 : setBusy(BusyStatus::Saving, false);
990 :
991 1 : saveProject(SaveMode::SaveAs).onResolve(this, [resolve](const Ret& saveRet) {
992 1 : (void)resolve(saveRet);
993 : });
994 :
995 1 : return Promise<Ret>::dummy_result();
996 : }
997 1 : case RET_CODE_CONFLICT_RESPONSE_PUBLISH_AS_NEW_SCORE: {
998 1 : CloudProjectInfo newInfo = info;
999 1 : newInfo.sourceUrl = QUrl();
1000 :
1001 1 : uploadProject(newInfo, audio, openEditUrl, publishMode).onResolve(this, [resolve](const Ret& uploadRet) {
1002 1 : (void)resolve(uploadRet);
1003 : });
1004 :
1005 1 : return Promise<Ret>::dummy_result();
1006 1 : }
1007 0 : case RET_CODE_CONFLICT_RESPONSE_REPLACE: {
1008 0 : museScoreComService()->downloadScoreInfo(info.sourceUrl)
1009 0 : .onResolve(this, [this, ret, info, audio, openEditUrl, publishMode,
1010 : resolve](const RetVal<muse::cloud::ScoreInfo>& scoreInfo) {
1011 0 : if (!scoreInfo.ret) {
1012 0 : LOGE() << scoreInfo.ret.toString();
1013 0 : showCloudSaveError(scoreInfo.ret, info, publishMode, false);
1014 0 : (void)resolve(ret);
1015 0 : return;
1016 : }
1017 :
1018 0 : CloudProjectInfo newInfo = info;
1019 0 : newInfo.revisionId = scoreInfo.val.revisionId;
1020 :
1021 0 : uploadProject(newInfo, audio, openEditUrl, publishMode).onResolve(this, [resolve](const Ret& uploadRet) {
1022 0 : (void)resolve(uploadRet);
1023 : });
1024 0 : });
1025 :
1026 0 : return Promise<Ret>::dummy_result();
1027 : }
1028 2 : default:
1029 2 : break;
1030 : }
1031 :
1032 2 : return resolve(ret);
1033 12 : });
1034 4 : }
1035 :
1036 0 : void SaveProjectScenario::onAudioSuccessfullyUploaded(const QUrl& urlToOpen)
1037 : {
1038 0 : closeUploadProgressDialog();
1039 :
1040 0 : platformInteractive()->openUrl(urlToOpen);
1041 0 : }
1042 :
1043 0 : Promise<Ret> SaveProjectScenario::onAudioUploadFailed(const Ret& ret)
1044 : {
1045 0 : closeUploadProgressDialog();
1046 :
1047 0 : return showAudioCloudShareError(ret);
1048 : }
1049 :
1050 4 : Promise<Ret> SaveProjectScenario::warnCloudIsNotAvailable()
1051 : {
1052 4 : closeUploadProgressDialog();
1053 :
1054 4 : if (!configuration()->showCloudIsNotAvailableWarning()) {
1055 4 : return resolvedPromise(make_ok());
1056 : }
1057 :
1058 0 : std::string title = muse::trc("project/save", "Unable to connect to the cloud");
1059 0 : std::string msg = muse::trc("project/save", "Your changes will be saved to a local file until the connection resumes.");
1060 :
1061 0 : return interactive()->warning(title, msg,
1062 : { IInteractive::Button::Ok }, IInteractive::Button::Ok,
1063 0 : IInteractive::Option::WithIcon | IInteractive::Option::WithDontShowAgainCheckBox)
1064 0 : .then<Ret>(this, [this](const IInteractive::Result& res, auto resolve) {
1065 0 : configuration()->setShowCloudIsNotAvailableWarning(res.showAgain());
1066 :
1067 0 : return resolve(make_ok());
1068 0 : });
1069 0 : }
1070 :
1071 2 : Promise<bool> SaveProjectScenario::refuseSaveAfter(Promise<Ret> shown)
1072 : {
1073 2 : return shown.then<bool>(this, [](const Ret&, auto resolve) {
1074 2 : return resolve(false);
1075 4 : });
1076 : }
1077 :
1078 5 : Promise<bool> SaveProjectScenario::askIfUserAgreesToSaveProjectWithErrors(const Ret& ret, const SaveLocation& location)
1079 : {
1080 5 : switch (static_cast<Err>(ret.code())) {
1081 1 : case Err::NoPartsError:
1082 1 : return refuseSaveAfter(warnScoreCouldnotBeSaved(muse::trc("project/save", "Please add at least one instrument to enable saving.")));
1083 1 : case Err::CorruptionUponOpenningError:
1084 1 : return askIfUserAgreesToSaveCorruptedScoreUponOpenning(location, ret.text());
1085 3 : case Err::CorruptionError: {
1086 3 : auto project = currentNotationProject();
1087 3 : return askIfUserAgreesToSaveCorruptedScore(location, ret.text(), project->isNewlyCreated());
1088 3 : }
1089 0 : default:
1090 0 : return resolvedPromise(false);
1091 : }
1092 : }
1093 :
1094 3 : Promise<bool> SaveProjectScenario::askIfUserAgreesToSaveCorruptedScore(const SaveLocation& location, const std::string& errorText,
1095 : bool newlyCreated)
1096 : {
1097 3 : switch (location.type) {
1098 0 : case SaveLocationType::Cloud: {
1099 : Promise<Ret> shown = newlyCreated
1100 0 : ? showErrCorruptedScoreCannotBeSaved(location, errorText)
1101 0 : : warnCorruptedScoreCannotBeSavedOnCloud(errorText, !newlyCreated);
1102 :
1103 0 : return refuseSaveAfter(shown);
1104 0 : }
1105 3 : case SaveLocationType::Local:
1106 3 : return askIfUserAgreesToSaveCorruptedScoreLocally(errorText, !newlyCreated);
1107 0 : case SaveLocationType::Undefined: // fallthrough
1108 : default:
1109 0 : return resolvedPromise(false);
1110 : }
1111 : }
1112 :
1113 0 : Promise<Ret> SaveProjectScenario::warnCorruptedScoreCannotBeSavedOnCloud(const std::string& errorText, bool canRevert)
1114 : {
1115 0 : std::string title = muse::trc("project", "Your score cannot be uploaded to the cloud");
1116 :
1117 0 : IInteractive::Text text;
1118 0 : text.text = muse::trc("project", "This score has become corrupted and contains errors. "
1119 : "You can fix the errors manually, or save the score to your computer "
1120 0 : "and get help for this issue on MuseScore.org.");
1121 0 : text.detailedText = errorText;
1122 :
1123 0 : IInteractive::ButtonDatas buttons;
1124 0 : buttons.push_back(interactive()->buttonData(IInteractive::Button::Cancel));
1125 :
1126 0 : IInteractive::ButtonData saveCopyBtn(IInteractive::Button::CustomButton, muse::trc("project", "Save as…"), !canRevert /*accent*/);
1127 0 : buttons.push_back(saveCopyBtn);
1128 :
1129 0 : int defaultBtn = saveCopyBtn.btn;
1130 :
1131 0 : IInteractive::ButtonData revertToLastSavedBtn(saveCopyBtn.btn + 1, muse::trc("project", "Revert to last saved"),
1132 0 : true /*accent*/);
1133 :
1134 0 : if (canRevert) {
1135 0 : buttons.push_back(revertToLastSavedBtn);
1136 0 : defaultBtn = revertToLastSavedBtn.btn;
1137 : }
1138 :
1139 0 : return interactive()->error(title, text, buttons, defaultBtn)
1140 0 : .then<Ret>(this, [this, saveCopyBtn, revertToLastSavedBtn](const IInteractive::Result& res, auto resolve) {
1141 0 : int btn = res.button();
1142 :
1143 0 : if (btn == saveCopyBtn.btn) {
1144 : //! NOTE The save that ran into this still holds the status; the new flow takes it over
1145 0 : setBusy(BusyStatus::Saving, false);
1146 :
1147 0 : saveProject(SaveMode::SaveAs, SaveLocationType::Local, true /*force*/)
1148 0 : .onResolve(this, [resolve](const Ret& ret) {
1149 0 : (void)resolve(ret);
1150 : });
1151 :
1152 0 : return Promise<Ret>::dummy_result();
1153 : }
1154 :
1155 0 : if (btn == revertToLastSavedBtn.btn) {
1156 0 : askToRevertCorruptedScoreToLastSaved().onResolve(this, [resolve](const Ret& ret) {
1157 0 : (void)resolve(ret);
1158 : });
1159 :
1160 0 : return Promise<Ret>::dummy_result();
1161 : }
1162 :
1163 0 : return resolve(make_ret(Ret::Code::Cancel));
1164 0 : });
1165 0 : }
1166 :
1167 3 : Promise<bool> SaveProjectScenario::askIfUserAgreesToSaveCorruptedScoreLocally(const std::string& errorText,
1168 : bool canRevert)
1169 : {
1170 3 : std::string title = muse::trc("project", "This score has become corrupted and contains errors");
1171 :
1172 3 : IInteractive::Text text;
1173 3 : text.text = !canRevert
1174 6 : ? muse::trc("project", "You can continue saving it locally, although the file may become unusable. "
1175 : "You can try to fix the errors manually, or get help for this issue on MuseScore.org.")
1176 : : muse::trc("project", "You can continue saving it locally, although the file may become unusable. "
1177 : "To preserve your score, revert to the last saved version, or fix the errors manually. "
1178 3 : "You can also get help for this issue on MuseScore.org.");
1179 3 : text.detailedText = errorText;
1180 :
1181 3 : IInteractive::ButtonDatas buttons;
1182 3 : buttons.push_back(interactive()->buttonData(IInteractive::Button::Cancel));
1183 :
1184 3 : IInteractive::ButtonData saveAnywayBtn(IInteractive::Button::CustomButton, muse::trc("project", "Save anyway"),
1185 3 : !canRevert /*accent*/);
1186 3 : buttons.push_back(saveAnywayBtn);
1187 :
1188 3 : int defaultBtn = saveAnywayBtn.btn;
1189 :
1190 6 : IInteractive::ButtonData revertToLastSavedBtn(saveAnywayBtn.btn + 1, muse::trc("project", "Revert to last saved"),
1191 3 : true /*accent*/);
1192 3 : if (canRevert) {
1193 3 : buttons.push_back(revertToLastSavedBtn);
1194 3 : defaultBtn = revertToLastSavedBtn.btn;
1195 : }
1196 :
1197 6 : return interactive()->error(title, text, buttons, defaultBtn)
1198 6 : .then<bool>(this, [this, saveAnywayBtn, revertToLastSavedBtn](const IInteractive::Result& res, auto resolve) {
1199 3 : if (res.isButton(revertToLastSavedBtn.btn)) {
1200 2 : askToRevertCorruptedScoreToLastSaved().onResolve(this, [resolve](const Ret&) {
1201 2 : (void)resolve(false);
1202 : });
1203 :
1204 2 : return Promise<bool>::dummy_result();
1205 : }
1206 :
1207 1 : return resolve(res.isButton(saveAnywayBtn.btn));
1208 6 : });
1209 3 : }
1210 :
1211 1 : Promise<bool> SaveProjectScenario::askIfUserAgreesToSaveCorruptedScoreUponOpenning(const SaveLocation& location,
1212 : const std::string& errorText)
1213 : {
1214 1 : switch (location.type) {
1215 1 : case SaveLocationType::Cloud:
1216 1 : return refuseSaveAfter(showErrCorruptedScoreCannotBeSaved(location, errorText));
1217 0 : case SaveLocationType::Local:
1218 0 : return askIfUserAgreesToSaveCorruptedScoreLocally(errorText, false /*canRevert*/);
1219 0 : case SaveLocationType::Undefined: // fallthrough
1220 : default:
1221 0 : return resolvedPromise(false);
1222 : }
1223 : }
1224 :
1225 1 : Promise<Ret> SaveProjectScenario::showErrCorruptedScoreCannotBeSaved(const SaveLocation& location, const std::string& errorText)
1226 : {
1227 1 : std::string title = location.isLocal()
1228 1 : ? muse::trc("project", "Your score cannot be saved")
1229 1 : : muse::trc("project", "Your score cannot be uploaded to the cloud");
1230 :
1231 1 : IInteractive::Text text;
1232 1 : text.text = muse::trc("project", "This score is corrupted. You can get help for this issue on MuseScore.org.");
1233 1 : text.detailedText = errorText;
1234 :
1235 1 : IInteractive::ButtonData getHelpBtn(IInteractive::Button::CustomButton, muse::trc("project", "Get help"));
1236 :
1237 6 : return interactive()->error(title, text, {
1238 : getHelpBtn,
1239 1 : interactive()->buttonData(IInteractive::Button::Ok)
1240 2 : }).then<Ret>(this, [this, getHelpBtn](const IInteractive::Result& res, auto resolve) {
1241 1 : if (res.isButton(getHelpBtn.btn)) {
1242 0 : platformInteractive()->openUrl(configuration()->supportForumUrl());
1243 : }
1244 :
1245 1 : return resolve(make_ok());
1246 2 : });
1247 3 : }
1248 :
1249 3 : Promise<Ret> SaveProjectScenario::warnScoreCouldnotBeSaved(const Ret& ret)
1250 : {
1251 3 : std::string message = ret.text();
1252 3 : if (message.empty()) {
1253 3 : message = muse::trc("project/save", "An unknown error occurred while saving this file.");
1254 : }
1255 :
1256 3 : return warnScoreCouldnotBeSaved(message)
1257 6 : .then<Ret>(this, [ret](const Ret&, auto resolve) {
1258 3 : return resolve(ret);
1259 6 : });
1260 3 : }
1261 :
1262 4 : Promise<Ret> SaveProjectScenario::warnScoreCouldnotBeSaved(const std::string& errorText)
1263 : {
1264 8 : return interactive()->warning(muse::trc("project/save", "Your score could not be saved"), errorText)
1265 4 : .then<Ret>(this, [](const IInteractive::Result&, auto resolve) {
1266 4 : return resolve(make_ok());
1267 8 : });
1268 : }
1269 :
1270 3 : Promise<int> SaveProjectScenario::warnScoreHasBecomeCorruptedAfterSave(const Ret& ret)
1271 : {
1272 3 : const QString errDetailsMessage = QString::fromStdString(ret.toString()).toHtmlEscaped();
1273 :
1274 6 : const QString supportForumLink = String("<a href=\"%1\" style=\"text-decoration: none\">MuseScore.org</a>")
1275 3 : .arg(configuration()->supportForumUrl().toString());
1276 :
1277 3 : const std::string title = muse::trc("project/save", "An error occurred while saving your score");
1278 :
1279 6 : const std::string body = muse::qtrc("project/save",
1280 : "To preserve your score, try saving it again. "
1281 : "If this message still appears, please save your score as new copy. "
1282 : "You can also get help for this issue on %1.<br/><br/>"
1283 : "Error details (please cite when asking for support): %2")
1284 6 : .arg(supportForumLink, errDetailsMessage)
1285 3 : .toStdString();
1286 :
1287 3 : IInteractive::ButtonDatas buttons;
1288 :
1289 3 : IInteractive::ButtonData saveAsBtn(SAVE_AS_BTN_ID, muse::trc("project/save", "Save as…"));
1290 3 : saveAsBtn.role = IInteractive::ButtonRole::ContinueRole;
1291 3 : buttons.push_back(saveAsBtn);
1292 :
1293 3 : IInteractive::ButtonData retryBtn(RETRY_SAVE_BTN_ID, muse::trc("project", "Try again"), true /*accent*/);
1294 3 : retryBtn.role = IInteractive::ButtonRole::ContinueRole;
1295 3 : buttons.push_back(retryBtn);
1296 :
1297 3 : IInteractive::ButtonData cancelBtn = interactive()->buttonData(IInteractive::Button::Cancel);
1298 3 : buttons.push_back(cancelBtn);
1299 :
1300 6 : return interactive()->error(title, IInteractive::Text(body, IInteractive::TextFormat::RichText), buttons, retryBtn.btn)
1301 3 : .then<int>(this, [](const IInteractive::Result& res, auto resolve) {
1302 3 : return resolve(res.button());
1303 6 : });
1304 3 : }
1305 :
1306 2 : Promise<Ret> SaveProjectScenario::askToRevertCorruptedScoreToLastSaved()
1307 : {
1308 3 : TRACEFUNC;
1309 :
1310 2 : std::string title = muse::trc("project", "Revert to last saved?");
1311 2 : std::string body = muse::trc("project", "Your changes will be lost. This action cannot be undone.");
1312 :
1313 8 : return interactive()->warning(title, body, {
1314 : { IInteractive::Button::No, IInteractive::Button::Yes }
1315 : }, IInteractive::Button::Yes, IInteractive::Option::WithIcon)
1316 2 : .then<Ret>(this, [this](const IInteractive::Result& res, auto resolve) {
1317 2 : if (res.isButton(IInteractive::Button::No)) {
1318 1 : return resolve(make_ret(Ret::Code::Cancel));
1319 : }
1320 :
1321 2 : openProjectScenario()->revertToLastSaved().onResolve(this, [resolve](const Ret& ret) {
1322 1 : if (!ret) {
1323 0 : LOGE() << "score was not reverted: " << ret.toString();
1324 : }
1325 :
1326 1 : (void)resolve(ret);
1327 : });
1328 :
1329 1 : return Promise<Ret>::dummy_result();
1330 4 : });
1331 2 : }
1332 :
1333 31 : RecentFile SaveProjectScenario::makeRecentFile(INotationProjectPtr project)
1334 : {
1335 31 : RecentFile file;
1336 31 : file.path = project->path();
1337 :
1338 31 : if (project->isCloudProject()) {
1339 3 : file.displayNameOverride = project->cloudInfo().name;
1340 : }
1341 :
1342 31 : return file;
1343 0 : }
1344 :
1345 0 : void SaveProjectScenario::moveProject(INotationProjectPtr project, const muse::io::path_t& newPath, bool replace)
1346 : {
1347 0 : muse::io::path_t oldPath = project->path();
1348 0 : if (oldPath == newPath) {
1349 0 : return;
1350 : }
1351 :
1352 0 : fileSystem()->move(oldPath, newPath, replace);
1353 0 : project->setPath(newPath);
1354 :
1355 0 : recentFilesController()->moveRecentFile(oldPath, makeRecentFile(project));
1356 0 : }
1357 :
1358 6 : QUrl SaveProjectScenario::scoreManagerUrl() const
1359 : {
1360 6 : return museScoreComService()->scoreManagerUrl();
1361 : }
1362 :
1363 0 : static std::string saveCloudStatusCodeErrorMessage(const Ret& ret, bool withHelp = false)
1364 : {
1365 0 : std::string message;
1366 :
1367 0 : switch (ret.code()) {
1368 0 : case int(cloud::Err::Status400_InvalidRequest):
1369 : //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
1370 : //: that is deliberately not translated
1371 0 : message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
1372 0 : .arg("400 Invalid request").toStdString();
1373 0 : break;
1374 0 : case int(cloud::Err::Status401_AuthorizationRequired):
1375 : //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
1376 : //: that is deliberately not translated
1377 0 : message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
1378 0 : .arg("401 Authorization required").toStdString();
1379 0 : break;
1380 0 : case int(cloud::Err::Status422_ValidationFailed):
1381 : //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
1382 : //: that is deliberately not translated
1383 0 : message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
1384 0 : .arg("422 Validation failed").toStdString();
1385 0 : break;
1386 0 : case int(cloud::Err::Status429_RateLimitExceeded):
1387 : //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
1388 : //: that is deliberately not translated
1389 0 : message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
1390 0 : .arg("429 Rate limit exceeded").toStdString();
1391 0 : break;
1392 0 : case int(cloud::Err::Status500_InternalServerError):
1393 : //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
1394 : //: that is deliberately not translated
1395 0 : message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
1396 0 : .arg("500 Internal server error").toStdString();
1397 0 : break;
1398 0 : case int(cloud::Err::UnknownStatusCode): {
1399 0 : if (const auto status = ret.data<int>("status", -1); status != -1) {
1400 : //: %1 will be replaced with the error code that MuseScore.com returned, which is a number.
1401 0 : message = muse::qtrc("project/cloud", "MuseScore.com returned an unknown error code: %1.")
1402 0 : .arg(status).toStdString();
1403 : } else {
1404 0 : message = muse::trc("project/cloud", "MuseScore.com returned an unknown error code.");
1405 : }
1406 0 : } break;
1407 : }
1408 :
1409 0 : if (withHelp) {
1410 0 : message += "\n\n" + muse::trc("project/cloud", "Please try again later, or get help for this problem on MuseScore.com.");
1411 : }
1412 :
1413 0 : return message;
1414 0 : }
1415 :
1416 9 : Promise<RetVal<SaveLocation> > SaveProjectScenario::askSaveLocation(INotationProjectPtr project, SaveMode mode,
1417 : SaveLocationType preselectedType) const
1418 : {
1419 9 : if (preselectedType != SaveLocationType::Undefined) {
1420 1 : return askSaveLocationOfType(project, mode, preselectedType);
1421 : }
1422 :
1423 16 : return saveLocationType()
1424 16 : .then<RetVal<SaveLocation> >(this, [this, project, mode](const RetVal<SaveLocationType>& type, auto resolve) {
1425 8 : if (!type.ret) {
1426 0 : return resolve(RetVal<SaveLocation>(type.ret));
1427 : }
1428 :
1429 8 : askSaveLocationOfType(project, mode, type.val).onResolve(this, [resolve](const RetVal<SaveLocation>& location) {
1430 8 : (void)resolve(location);
1431 : });
1432 :
1433 8 : return Promise<RetVal<SaveLocation> >::dummy_result();
1434 8 : });
1435 : }
1436 :
1437 9 : Promise<RetVal<SaveLocation> > SaveProjectScenario::askSaveLocationOfType(INotationProjectPtr project, SaveMode mode,
1438 : SaveLocationType type) const
1439 : {
1440 9 : IF_ASSERT_FAILED(type != SaveLocationType::Undefined) {
1441 0 : return resolvedPromise(RetVal<SaveLocation>(make_ret(Ret::Code::UnknownError)));
1442 : }
1443 :
1444 : // The user may switch between Local and Cloud as often as they want
1445 9 : configuration()->setLastUsedSaveLocationType(type);
1446 :
1447 9 : if (type == SaveLocationType::Local) {
1448 14 : return askLocalPath(project, mode)
1449 14 : .then<RetVal<SaveLocation> >(this, [this, project, mode](const RetVal<muse::io::path_t>& path, auto resolve) {
1450 7 : switch (path.ret.code()) {
1451 1 : case int(Ret::Code::Ok):
1452 1 : return resolve(RetVal<SaveLocation>::make_ok(SaveLocation(path.val)));
1453 0 : case RET_CODE_CHANGE_SAVE_LOCATION_TYPE: {
1454 0 : askSaveLocationOfType(project, mode, SaveLocationType::Cloud)
1455 0 : .onResolve(this, [resolve](const RetVal<SaveLocation>& location) {
1456 0 : (void)resolve(location);
1457 : });
1458 :
1459 0 : return Promise<RetVal<SaveLocation> >::dummy_result();
1460 : }
1461 6 : default:
1462 6 : return resolve(RetVal<SaveLocation>(path.ret));
1463 : }
1464 7 : });
1465 : }
1466 :
1467 4 : return askCloudLocation(project, mode)
1468 4 : .then<RetVal<SaveLocation> >(this, [this, project, mode](const RetVal<CloudProjectInfo>& info, auto resolve) {
1469 2 : switch (info.ret.code()) {
1470 1 : case int(Ret::Code::Ok):
1471 1 : return resolve(RetVal<SaveLocation>::make_ok(SaveLocation(info.val)));
1472 0 : case RET_CODE_CHANGE_SAVE_LOCATION_TYPE: {
1473 0 : askSaveLocationOfType(project, mode, SaveLocationType::Local)
1474 0 : .onResolve(this, [resolve](const RetVal<SaveLocation>& location) {
1475 0 : (void)resolve(location);
1476 : });
1477 0 : return Promise<RetVal<SaveLocation> >::dummy_result();
1478 : }
1479 1 : default:
1480 1 : return resolve(RetVal<SaveLocation>(info.ret));
1481 : }
1482 2 : });
1483 : }
1484 :
1485 11 : Promise<RetVal<muse::io::path_t> > SaveProjectScenario::askLocalPath(INotationProjectPtr project, SaveMode saveMode) const
1486 : {
1487 11 : std::string dialogTitle = muse::trc("project/save", "Save score");
1488 11 : std::string filenameAddition;
1489 :
1490 11 : if (saveMode == SaveMode::SaveCopy) {
1491 : //: used to form a filename suggestion, like "originalFile - copy"
1492 1 : filenameAddition = " - " + muse::trc("project/save", "copy", "a copy of a file");
1493 10 : } else if (saveMode == SaveMode::SaveSelection) {
1494 : //: used to form a filename suggestion, like "originalFile - selection"
1495 0 : filenameAddition = " - " + muse::trc("project/save", "selection");
1496 : }
1497 :
1498 22 : muse::io::path_t defaultPath = configuration()->defaultSavingFilePath(project, filenameAddition);
1499 :
1500 : std::vector<std::string> filter {
1501 0 : muse::trc("project", "MuseScore file") + " (*.mscz)",
1502 11 : muse::trc("project", "Uncompressed MuseScore folder [experimental]")
1503 : #ifdef Q_OS_MAC
1504 : + " (*)"
1505 : #else
1506 : + " (*.)"
1507 : #endif
1508 44 : };
1509 :
1510 11 : return interactive()->selectSavingFile(dialogTitle, defaultPath, filter)
1511 0 : .then<RetVal<muse::io::path_t> >(this, [this](const muse::io::path_t& path, auto resolve, auto) {
1512 3 : muse::io::path_t selectedPath = path;
1513 :
1514 3 : if (selectedPath.empty()) {
1515 0 : return resolve(RetVal<muse::io::path_t>(make_ret(Ret::Code::Cancel)));
1516 : }
1517 :
1518 3 : if (!engraving::isMuseScoreFile(io::suffix(selectedPath))) {
1519 : // Then it must be that the user is trying to save a mscx file.
1520 : // At the selected path, a folder will be created,
1521 : // and inside the folder, a mscx file will be created.
1522 : // We should return the path to the mscx file.
1523 0 : selectedPath = selectedPath.appendingComponent(io::filename(selectedPath)).appendingSuffix(engraving::MSCX);
1524 : }
1525 :
1526 3 : configuration()->setLastSavedProjectsPath(io::dirpath(selectedPath));
1527 :
1528 3 : return resolve(RetVal<muse::io::path_t>::make_ok(selectedPath));
1529 14 : }, [](int code, const std::string& err, auto resolve, auto) {
1530 : //! NOTE Choosing nothing is not an error to report
1531 8 : if (code == int(Ret::Code::Cancel)) {
1532 8 : return resolve(RetVal<muse::io::path_t>(make_ret(Ret::Code::Cancel)));
1533 : }
1534 :
1535 0 : return resolve(RetVal<muse::io::path_t>(muse::make_ret(code, err)));
1536 22 : });
1537 22 : }
1538 :
1539 8 : Promise<RetVal<SaveLocationType> > SaveProjectScenario::saveLocationType() const
1540 : {
1541 8 : bool shouldAsk = configuration()->shouldAskSaveLocationType();
1542 8 : SaveLocationType lastUsed = configuration()->lastUsedSaveLocationType();
1543 8 : if (!shouldAsk && lastUsed != SaveLocationType::Undefined) {
1544 8 : return resolvedPromise(RetVal<SaveLocationType>::make_ok(lastUsed));
1545 : }
1546 :
1547 0 : return askSaveLocationType();
1548 : }
1549 :
1550 0 : Promise<RetVal<SaveLocationType> > SaveProjectScenario::askSaveLocationType() const
1551 : {
1552 0 : UriQuery query("musescore://project/asksavelocationtype");
1553 0 : bool shouldAsk = configuration()->shouldAskSaveLocationType();
1554 0 : query.addParam("askAgain", Val(shouldAsk));
1555 :
1556 0 : return openDialog(query).then<RetVal<SaveLocationType> >(this, [this](const RetVal<Val>& rv, auto resolve) {
1557 0 : if (!rv.ret) {
1558 0 : return resolve(RetVal<SaveLocationType>(rv.ret));
1559 : }
1560 :
1561 0 : QVariantMap vals = rv.val.toQVariant().toMap();
1562 :
1563 0 : bool askAgain = vals["askAgain"].toBool();
1564 0 : configuration()->setShouldAskSaveLocationType(askAgain);
1565 :
1566 0 : SaveLocationType type = static_cast<SaveLocationType>(vals["saveLocationType"].toInt());
1567 0 : return resolve(RetVal<SaveLocationType>::make_ok(type));
1568 0 : });
1569 0 : }
1570 :
1571 2 : Promise<RetVal<CloudProjectInfo> > SaveProjectScenario::askCloudLocation(INotationProjectPtr project, SaveMode mode) const
1572 : {
1573 2 : return doAskCloudLocation(project, mode, false);
1574 : }
1575 :
1576 5 : Promise<RetVal<CloudProjectInfo> > SaveProjectScenario::askPublishLocation(INotationProjectPtr project) const
1577 : {
1578 5 : return doAskCloudLocation(project, SaveMode::Save, true);
1579 : }
1580 :
1581 0 : Promise<RetVal<CloudAudioInfo> > SaveProjectScenario::askShareAudioLocation(INotationProjectPtr project) const
1582 : {
1583 0 : return audioComService()->authorization()->checkCloudIsAvailable()
1584 0 : .then<RetVal<CloudAudioInfo> >(this, [this, project](const Ret& isCloudAvailable, auto resolve) {
1585 0 : if (!isCloudAvailable) {
1586 0 : warnCloudNotAvailableForSharingAudio().onResolve(this, [resolve](const Ret& ret) {
1587 0 : (void)resolve(RetVal<CloudAudioInfo>(ret));
1588 : });
1589 :
1590 0 : return Promise<RetVal<CloudAudioInfo> >::dummy_result();
1591 : }
1592 :
1593 0 : doAskShareAudioLocation(project).onResolve(this, [resolve](const RetVal<CloudAudioInfo>& info) {
1594 0 : (void)resolve(info);
1595 : });
1596 :
1597 0 : return Promise<RetVal<CloudAudioInfo> >::dummy_result();
1598 0 : });
1599 : }
1600 :
1601 0 : Promise<RetVal<CloudAudioInfo> > SaveProjectScenario::doAskShareAudioLocation(INotationProjectPtr project) const
1602 : {
1603 0 : std::string dialogText = muse::trc("project/save", "Log in or create a new account on Audio.com to share your music.");
1604 :
1605 0 : return ensureAuthorization(muse::cloud::AUDIO_COM_CLOUD_CODE, false, dialogText)
1606 0 : .then<RetVal<CloudAudioInfo> >(this, [this, project](const RetVal<Val>& auth, auto resolve) {
1607 0 : if (!auth.ret) {
1608 0 : return resolve(RetVal<CloudAudioInfo>(auth.ret));
1609 : }
1610 :
1611 0 : QString defaultName = project->displayName();
1612 0 : QUrl uploadUrl = project->cloudAudioInfo().url;
1613 0 : cloud::Visibility defaultVisibility = cloud::Visibility::Public;
1614 :
1615 0 : UriQuery query("musescore://project/savetocloud");
1616 0 : query.addParam("isPublishShare", Val(true));
1617 0 : query.addParam("name", Val(defaultName));
1618 0 : query.addParam("visibility", Val(defaultVisibility));
1619 0 : query.addParam("cloudCode", Val(cloud::AUDIO_COM_CLOUD_CODE));
1620 :
1621 0 : if (!uploadUrl.isEmpty()) {
1622 0 : query.addParam("existingScoreOrAudioUrl", Val(uploadUrl.toString()));
1623 : }
1624 :
1625 0 : openDialog(query).onResolve(this, [resolve, uploadUrl](const RetVal<Val>& rv) {
1626 0 : if (!rv.ret) {
1627 0 : (void)resolve(RetVal<CloudAudioInfo>(rv.ret));
1628 0 : return;
1629 : }
1630 :
1631 0 : QVariantMap vals = rv.val.toQVariant().toMap();
1632 : using Response = cloud::SaveToCloudResponse::SaveToCloudResponse;
1633 0 : auto response = static_cast<Response>(vals["response"].toInt());
1634 0 : switch (response) {
1635 0 : case Response::Cancel:
1636 : case Response::SaveLocallyInstead:
1637 0 : (void)resolve(RetVal<CloudAudioInfo>(make_ret(Ret::Code::Cancel)));
1638 0 : return;
1639 0 : case Response::Ok:
1640 0 : break;
1641 : }
1642 :
1643 0 : CloudAudioInfo result;
1644 0 : result.name = vals["name"].toString();
1645 0 : result.visibility = static_cast<cloud::Visibility>(vals["visibility"].toInt());
1646 0 : result.replaceExisting = vals["replaceExisting"].toBool() && !uploadUrl.isEmpty();
1647 :
1648 0 : (void)resolve(RetVal<CloudAudioInfo>::make_ok(result));
1649 0 : });
1650 :
1651 0 : return Promise<RetVal<CloudAudioInfo> >::dummy_result();
1652 0 : });
1653 0 : }
1654 :
1655 7 : Promise<RetVal<CloudProjectInfo> > SaveProjectScenario::doAskCloudLocation(INotationProjectPtr project, SaveMode mode,
1656 : bool isPublishShare) const
1657 : {
1658 14 : return museScoreComService()->authorization()->checkCloudIsAvailable()
1659 14 : .then<RetVal<CloudProjectInfo> >(this, [this, project, mode, isPublishShare](const Ret& isCloudAvailable, auto resolve) {
1660 7 : if (!isCloudAvailable) {
1661 0 : warnCloudNotAvailableForUploading(isPublishShare).onResolve(this, [resolve](const Ret& ret) {
1662 0 : (void)resolve(RetVal<CloudProjectInfo>(ret));
1663 : });
1664 :
1665 0 : return Promise<RetVal<CloudProjectInfo> >::dummy_result();
1666 : }
1667 :
1668 7 : doAskCloudLocationAuthorized(project, mode, isPublishShare).onResolve(this, [resolve](const RetVal<CloudProjectInfo>& info) {
1669 7 : (void)resolve(info);
1670 : });
1671 :
1672 7 : return Promise<RetVal<CloudProjectInfo> >::dummy_result();
1673 14 : });
1674 : }
1675 :
1676 7 : Promise<RetVal<CloudProjectInfo> > SaveProjectScenario::doAskCloudLocationAuthorized(INotationProjectPtr project, SaveMode mode,
1677 : bool isPublishShare) const
1678 : {
1679 : std::string dialogText = isPublishShare
1680 7 : ? muse::trc("project/save", "Log in to MuseScore.com to publish this score.")
1681 7 : : muse::trc("project/save", "Log in to MuseScore.com to save this score to the cloud.");
1682 :
1683 7 : return ensureAuthorization(muse::cloud::MUSESCORE_COM_CLOUD_CODE, true, dialogText)
1684 14 : .then<RetVal<CloudProjectInfo> >(this, [this, project, mode, isPublishShare](const RetVal<Val>& auth, auto resolve) {
1685 7 : if (!auth.ret) {
1686 0 : return resolve(RetVal<CloudProjectInfo>(auth.ret));
1687 : }
1688 :
1689 : using Response = cloud::SaveToCloudResponse::SaveToCloudResponse;
1690 7 : if (static_cast<Response>(auth.val.toInt()) == Response::SaveLocallyInstead) {
1691 2 : return resolve(RetVal<CloudProjectInfo>(Ret(RET_CODE_CHANGE_SAVE_LOCATION_TYPE)));
1692 : }
1693 :
1694 5 : askCloudProjectInfo(project, mode, isPublishShare).onResolve(this, [resolve](const RetVal<CloudProjectInfo>& info) {
1695 5 : (void)resolve(info);
1696 : });
1697 :
1698 5 : return Promise<RetVal<CloudProjectInfo> >::dummy_result();
1699 14 : });
1700 7 : }
1701 :
1702 5 : Promise<RetVal<CloudProjectInfo> > SaveProjectScenario::askCloudProjectInfo(INotationProjectPtr project, SaveMode mode,
1703 : bool isPublishShare) const
1704 : {
1705 5 : const QString defaultName = project->displayName();
1706 5 : const cloud::Visibility defaultVisibility = isPublishShare ? cloud::Visibility::Public : cloud::Visibility::Private;
1707 5 : const QUrl existingScoreUrl = project->cloudInfo().sourceUrl;
1708 :
1709 5 : if (existingScoreUrl.isEmpty()) {
1710 10 : return askCloudProjectInfo(project, mode, isPublishShare, defaultName, defaultVisibility, existingScoreUrl);
1711 : }
1712 :
1713 0 : return museScoreComService()->downloadScoreInfo(existingScoreUrl)
1714 0 : .then<RetVal<CloudProjectInfo> >(this, [this, project, mode, isPublishShare, defaultName, defaultVisibility,
1715 : existingScoreUrl](const RetVal<cloud::ScoreInfo>& scoreInfo, auto resolve) {
1716 0 : QString name = defaultName;
1717 0 : cloud::Visibility visibility = defaultVisibility;
1718 0 : QUrl scoreUrl = existingScoreUrl;
1719 :
1720 0 : if (scoreInfo.val.isValid()) {
1721 0 : const cloud::AccountInfo& accountInfo = museScoreComService()->authorization()->accountInfo();
1722 0 : if (accountInfo.id.toInt() != scoreInfo.val.owner.id) {
1723 0 : scoreUrl = QUrl();
1724 : }
1725 : }
1726 :
1727 0 : switch (scoreInfo.ret.code()) {
1728 0 : case int(Ret::Code::Ok):
1729 0 : name = scoreInfo.val.title;
1730 0 : if (!isPublishShare) {
1731 0 : visibility = scoreInfo.val.visibility;
1732 : }
1733 0 : break;
1734 :
1735 0 : case int(cloud::Err::Status400_InvalidRequest):
1736 : case int(cloud::Err::Status403_AccountNotActivated):
1737 : case int(cloud::Err::Status422_ValidationFailed):
1738 : case int(cloud::Err::Status429_RateLimitExceeded):
1739 : case int(cloud::Err::Status500_InternalServerError):
1740 : case int(cloud::Err::UnknownStatusCode):
1741 : case int(cloud::Err::NetworkError):
1742 0 : showCloudSaveError(scoreInfo.ret, project->cloudInfo(), isPublishShare, false)
1743 0 : .onResolve(this, [resolve](const Ret& ret) {
1744 0 : (void)resolve(RetVal<CloudProjectInfo>(ret));
1745 : });
1746 :
1747 0 : return Promise<RetVal<CloudProjectInfo> >::dummy_result();
1748 :
1749 : // It's possible the source URL is invalid or points to a score on a different user's account.
1750 : // In this situation we shouldn't show an error.
1751 0 : default: break;
1752 : }
1753 :
1754 0 : askCloudProjectInfo(project, mode, isPublishShare, name, visibility, scoreUrl)
1755 0 : .onResolve(this, [resolve](const RetVal<CloudProjectInfo>& info) {
1756 0 : (void)resolve(info);
1757 : });
1758 :
1759 0 : return Promise<RetVal<CloudProjectInfo> >::dummy_result();
1760 0 : });
1761 5 : }
1762 :
1763 5 : Promise<RetVal<CloudProjectInfo> > SaveProjectScenario::askCloudProjectInfo(INotationProjectPtr project, SaveMode mode,
1764 : bool isPublishShare, const QString& defaultName,
1765 : muse::cloud::Visibility defaultVisibility,
1766 : const QUrl& existingScoreUrl) const
1767 : {
1768 5 : const CloudProjectInfo existingProjectInfo = project->cloudInfo();
1769 :
1770 5 : UriQuery query("musescore://project/savetocloud");
1771 15 : query.addParam("isPublishShare", Val(isPublishShare));
1772 15 : query.addParam("name", Val(defaultName));
1773 15 : query.addParam("visibility", Val(defaultVisibility));
1774 15 : query.addParam("existingScoreOrAudioUrl", Val(existingScoreUrl.toString()));
1775 15 : query.addParam("cloudCode", Val(cloud::MUSESCORE_COM_CLOUD_CODE));
1776 :
1777 5 : return openDialog(query)
1778 10 : .then<RetVal<CloudProjectInfo> >(this, [this, mode, isPublishShare, existingProjectInfo](const RetVal<Val>& rv, auto resolve) {
1779 5 : if (!rv.ret) {
1780 1 : return resolve(RetVal<CloudProjectInfo>(rv.ret));
1781 : }
1782 :
1783 4 : QVariantMap vals = rv.val.toQVariant().toMap();
1784 : using Response = cloud::SaveToCloudResponse::SaveToCloudResponse;
1785 4 : auto response = static_cast<Response>(vals["response"].toInt());
1786 4 : switch (response) {
1787 0 : case Response::Cancel:
1788 0 : return resolve(RetVal<CloudProjectInfo>(make_ret(Ret::Code::Cancel)));
1789 0 : case Response::SaveLocallyInstead:
1790 0 : return resolve(RetVal<CloudProjectInfo>(Ret(RET_CODE_CHANGE_SAVE_LOCATION_TYPE)));
1791 4 : case Response::Ok:
1792 4 : break;
1793 : }
1794 :
1795 4 : CloudProjectInfo result;
1796 :
1797 4 : if ((mode == SaveMode::Save || isPublishShare) && vals["replaceExisting"].toBool()) {
1798 0 : result = existingProjectInfo;
1799 : }
1800 :
1801 4 : result.name = vals["name"].toString();
1802 4 : result.visibility = static_cast<cloud::Visibility>(vals["visibility"].toInt());
1803 :
1804 4 : warnBeforePublishing(isPublishShare, result.visibility).onResolve(this, [result, resolve](bool agreed) {
1805 4 : if (!agreed) {
1806 0 : (void)resolve(RetVal<CloudProjectInfo>(make_ret(Ret::Code::Cancel)));
1807 0 : return;
1808 : }
1809 :
1810 4 : (void)resolve(RetVal<CloudProjectInfo>::make_ok(result));
1811 : });
1812 :
1813 4 : return Promise<RetVal<CloudProjectInfo> >::dummy_result();
1814 14 : });
1815 5 : }
1816 :
1817 4 : Promise<bool> SaveProjectScenario::warnBeforePublishing(bool isPublishShare, cloud::Visibility visibility) const
1818 : {
1819 4 : if (isPublishShare) {
1820 3 : if (!configuration()->shouldWarnBeforePublish()) {
1821 6 : return resolvedPromise(true);
1822 : }
1823 : } else {
1824 1 : if (!configuration()->shouldWarnBeforeSavingPubliclyToCloud()) {
1825 2 : return resolvedPromise(true);
1826 : }
1827 : }
1828 :
1829 0 : std::string title, message;
1830 :
1831 : IInteractive::ButtonDatas buttons = {
1832 0 : IInteractive::ButtonData(IInteractive::Button::Cancel, muse::trc("global", "Cancel")),
1833 0 : IInteractive::ButtonData(IInteractive::Button::Ok, muse::trc("project/save", "Publish"), true)
1834 0 : };
1835 :
1836 0 : IInteractive::Options options = IInteractive::Option::WithIcon | IInteractive::Option::WithDontShowAgainCheckBox;
1837 :
1838 0 : if (isPublishShare) {
1839 0 : title = muse::trc("project/save", "Publish changes online?");
1840 0 : message = muse::trc("project/save", "We will need to generate a new MP3 for web playback.");
1841 0 : } else if (visibility == cloud::Visibility::Public) {
1842 0 : title = muse::trc("project/save", "Publish this score online?"),
1843 0 : message = muse::trc("project/save", "All saved changes will be publicly visible on MuseScore.com. "
1844 : "If you want to make frequent changes, we recommend saving this "
1845 0 : "score privately until you’re ready to share it to the world.");
1846 : } else {
1847 0 : return resolvedPromise(true);
1848 : }
1849 :
1850 0 : return interactive()->warning(title, message, buttons, int(IInteractive::Button::Ok), options)
1851 0 : .then<bool>(this, [this, isPublishShare](const IInteractive::Result& res, auto resolve) {
1852 0 : bool ok = res.standardButton() == IInteractive::Button::Ok;
1853 0 : if (ok && !res.showAgain()) {
1854 0 : if (isPublishShare) {
1855 0 : configuration()->setShouldWarnBeforePublish(false);
1856 : } else {
1857 0 : configuration()->setShouldWarnBeforeSavingPubliclyToCloud(false);
1858 : }
1859 : }
1860 :
1861 0 : return resolve(ok);
1862 0 : });
1863 0 : }
1864 :
1865 1 : Promise<bool> SaveProjectScenario::warnBeforeSavingToExistingPubliclyVisibleCloudProject() const
1866 : {
1867 : IInteractive::ButtonDatas buttons = {
1868 0 : IInteractive::ButtonData(IInteractive::Button::Cancel, muse::trc("global", "Cancel")),
1869 1 : IInteractive::ButtonData(IInteractive::Button::Ok, muse::trc("project/save", "Publish"), true)
1870 4 : };
1871 :
1872 4 : return interactive()->warning(
1873 2 : muse::trc("project/save", "Publish changes online?"),
1874 2 : muse::trc("project/save", "Your saved changes will be publicly visible. We will also "
1875 : "need to generate a new MP3 for public playback."),
1876 : buttons, int(IInteractive::Button::Ok))
1877 1 : .then<bool>(this, [](const IInteractive::Result& res, auto resolve) {
1878 1 : return resolve(res.standardButton() == IInteractive::Button::Ok);
1879 2 : });
1880 2 : }
1881 :
1882 0 : Promise<Ret> SaveProjectScenario::warnCloudNotAvailableForUploading(bool isPublishShare) const
1883 : {
1884 0 : if (isPublishShare) {
1885 : //! NOTE Nothing to answer, so the flow does not wait for the message to be dismissed
1886 0 : interactive()->warning(muse::trc("project/save", "Unable to connect to MuseScore.com"),
1887 0 : muse::trc("project/save", "Please check your internet connection or try again later."));
1888 0 : return resolvedPromise(make_ret(Ret::Code::Cancel));
1889 : }
1890 :
1891 : IInteractive::ButtonDatas buttons = {
1892 0 : IInteractive::ButtonData(IInteractive::Button::Cancel, muse::trc("global", "Cancel")),
1893 0 : IInteractive::ButtonData(IInteractive::Button::Ok, muse::trc("project/save", "Save to computer"), true)
1894 0 : };
1895 :
1896 0 : return interactive()->warning(muse::trc("project/save", "Unable to connect to the cloud"),
1897 0 : muse::trc("project/save", "Please check your internet connection or try again later."),
1898 : buttons, int(IInteractive::Button::Ok))
1899 0 : .then<Ret>(this, [](const IInteractive::Result& res, auto resolve) {
1900 0 : if (res.standardButton() == IInteractive::Button::Ok) {
1901 0 : return resolve(Ret(RET_CODE_CHANGE_SAVE_LOCATION_TYPE));
1902 : }
1903 :
1904 0 : return resolve(make_ret(Ret::Code::Cancel));
1905 0 : });
1906 0 : }
1907 :
1908 0 : Promise<Ret> SaveProjectScenario::warnCloudNotAvailableForSharingAudio() const
1909 : {
1910 0 : return interactive()->warning(muse::trc("project/save", "Unable to connect to Audio.com"),
1911 0 : muse::trc("project/save", "Please check your internet connection or try again later."))
1912 0 : .then<Ret>(this, [](const IInteractive::Result&, auto resolve) {
1913 0 : return resolve(make_ret(Ret::Code::Cancel));
1914 0 : });
1915 : }
1916 :
1917 21 : Promise<RetVal<Val> > SaveProjectScenario::ensureAuthorization(const QString& cloudCode, bool publishingScore,
1918 : const std::string& text) const
1919 : {
1920 21 : IF_ASSERT_FAILED(cloudCode == muse::cloud::MUSESCORE_COM_CLOUD_CODE || cloudCode == muse::cloud::AUDIO_COM_CLOUD_CODE) {
1921 0 : return resolvedPromise(RetVal<Val>(make_ret(Err::UnknownError)));
1922 : }
1923 :
1924 21 : bool isMuseScoreCom = cloudCode == muse::cloud::MUSESCORE_COM_CLOUD_CODE;
1925 42 : bool userAuthorized = isMuseScoreCom ? museScoreComService()->authorization()->userAuthorized().val
1926 21 : : audioComService()->authorization()->userAuthorized().val;
1927 :
1928 21 : if (userAuthorized) {
1929 16 : return resolvedPromise(RetVal<Val>::make_ok(Val()));
1930 : }
1931 :
1932 5 : UriQuery query("muse://cloud/requireauthorization");
1933 15 : query.addParam("text", Val(text));
1934 15 : query.addParam("cloudCode", Val(cloudCode));
1935 15 : query.addParam("publishingScore", Val(publishingScore));
1936 5 : return openDialog(query);
1937 5 : }
1938 :
1939 4 : Promise<Ret> SaveProjectScenario::showCloudSaveError(const Ret& ret, const CloudProjectInfo& info, bool isPublishShare,
1940 : bool alreadyAttempted) const
1941 : {
1942 4 : std::string title;
1943 4 : if (alreadyAttempted) {
1944 : title = isPublishShare
1945 8 : ? muse::trc("project/save", "Your score could not be published")
1946 4 : : muse::trc("project/save", "Your score could not be saved to the cloud");
1947 : } else {
1948 : title = isPublishShare
1949 0 : ? muse::trc("project/save", "Your score cannot be published")
1950 0 : : muse::trc("project/save", "Your score cannot be saved to the cloud");
1951 : }
1952 :
1953 4 : std::string msg;
1954 :
1955 : static constexpr int helpBtnCode = int(IInteractive::Button::CustomButton) + 1;
1956 : static constexpr int saveLocallyBtnCode = int(IInteractive::Button::CustomButton) + 2;
1957 : static constexpr int saveAsBtnCode = int(IInteractive::Button::CustomButton) + 3;
1958 : static constexpr int publishAsNewScoreBtnCode = int(IInteractive::Button::CustomButton) + 4;
1959 : static constexpr int replaceBtnCode = int(IInteractive::Button::CustomButton) + 5;
1960 :
1961 4 : IInteractive::ButtonData okBtn = interactive()->buttonData(IInteractive::Button::Ok);
1962 4 : IInteractive::ButtonData saveLocallyBtn { saveLocallyBtnCode, muse::trc("project/save", "Save to computer") };
1963 4 : IInteractive::ButtonData helpBtn { helpBtnCode, muse::trc("project/save", "Get help") };
1964 :
1965 0 : IInteractive::ButtonDatas buttons = (alreadyAttempted || isPublishShare)
1966 12 : ? (IInteractive::ButtonDatas { helpBtn, okBtn })
1967 28 : : (IInteractive::ButtonDatas { helpBtn, saveLocallyBtn, okBtn });
1968 :
1969 4 : int defaultButtonCode = okBtn.btn;
1970 :
1971 4 : switch (ret.code()) {
1972 0 : case int(cloud::Err::Status403_AccountNotActivated):
1973 0 : msg = muse::trc("project/cloud", "Your MuseScore.com account needs to be verified first. "
1974 0 : "Please activate your account via the link in the activation email.");
1975 0 : buttons = { okBtn };
1976 0 : break;
1977 2 : case int(cloud::Err::Status409_Conflict):
1978 2 : title = muse::trc("project/save", "There are conflicting changes in the online score");
1979 2 : if (isPublishShare) {
1980 0 : msg = muse::qtrc("project/save", "You can replace the <a href=\"%1\">online score</a>, or publish this as a new score "
1981 : "to avoid losing changes in the current online version.")
1982 0 : .arg(info.sourceUrl.toString())
1983 0 : .toStdString();
1984 0 : buttons = {
1985 0 : interactive()->buttonData(IInteractive::Button::Cancel),
1986 0 : IInteractive::ButtonData { publishAsNewScoreBtnCode, muse::trc("project/save", "Publish as new score") },
1987 0 : IInteractive::ButtonData { replaceBtnCode, muse::trc("project/save", "Replace") }
1988 0 : };
1989 0 : defaultButtonCode = replaceBtnCode;
1990 : } else {
1991 2 : msg = muse::qtrc("project/save", "You can replace the <a href=\"%1\">online score</a>, or save this as a new file "
1992 : "to avoid losing changes in the current online version.")
1993 4 : .arg(info.sourceUrl.toString())
1994 2 : .toStdString();
1995 8 : buttons = {
1996 2 : interactive()->buttonData(IInteractive::Button::Cancel),
1997 0 : IInteractive::ButtonData { saveAsBtnCode, muse::trc("project/save", "Save as…") },
1998 2 : IInteractive::ButtonData { replaceBtnCode, muse::trc("project/save", "Replace") }
1999 8 : };
2000 2 : defaultButtonCode = replaceBtnCode;
2001 : }
2002 2 : break;
2003 :
2004 0 : case int(cloud::Err::Status400_InvalidRequest):
2005 : case int(cloud::Err::Status401_AuthorizationRequired):
2006 : case int(cloud::Err::Status422_ValidationFailed):
2007 : case int(cloud::Err::Status429_RateLimitExceeded):
2008 : case int(cloud::Err::Status500_InternalServerError):
2009 : case int(cloud::Err::UnknownStatusCode):
2010 0 : msg = saveCloudStatusCodeErrorMessage(ret, /*withHelp=*/ true);
2011 0 : break;
2012 :
2013 0 : case int(cloud::Err::NetworkError):
2014 0 : msg = muse::mtrc("project/cloud", "Could not connect to <a href=\"%1\">MuseScore.com</a>. "
2015 : "Please check your internet connection or try again later.")
2016 0 : .arg(u"https://musescore.com").toStdString();
2017 0 : break;
2018 2 : default:
2019 2 : msg = muse::trc("project/cloud", "Please try again later, or get help for this problem on MuseScore.com.");
2020 2 : break;
2021 : }
2022 :
2023 8 : return interactive()->warning(title, msg, buttons, defaultButtonCode)
2024 4 : .then<Ret>(this, [this](const IInteractive::Result& res, auto resolve) {
2025 4 : switch (res.button()) {
2026 0 : case helpBtnCode:
2027 0 : platformInteractive()->openUrl(configuration()->dotComBugReportUrl());
2028 0 : break;
2029 0 : case saveLocallyBtnCode:
2030 0 : return resolve(Ret(RET_CODE_CHANGE_SAVE_LOCATION_TYPE));
2031 1 : case saveAsBtnCode:
2032 1 : return resolve(Ret(RET_CODE_CONFLICT_RESPONSE_SAVE_AS));
2033 1 : case publishAsNewScoreBtnCode:
2034 1 : return resolve(Ret(RET_CODE_CONFLICT_RESPONSE_PUBLISH_AS_NEW_SCORE));
2035 0 : case replaceBtnCode:
2036 0 : return resolve(Ret(RET_CODE_CONFLICT_RESPONSE_REPLACE));
2037 : }
2038 :
2039 2 : return resolve(make_ret(Ret::Code::Cancel));
2040 8 : });
2041 12 : }
2042 :
2043 0 : Promise<Ret> SaveProjectScenario::showAudioCloudShareError(const Ret& ret) const
2044 : {
2045 0 : std::string title= muse::trc("project/share", "Your audio could not be shared");
2046 0 : std::string msg;
2047 :
2048 0 : IInteractive::ButtonData okBtn = interactive()->buttonData(IInteractive::Button::Ok);
2049 0 : IInteractive::ButtonDatas buttons = IInteractive::ButtonDatas { okBtn };
2050 :
2051 0 : switch (ret.code()) {
2052 0 : case int(cloud::Err::Status403_AccountNotActivated):
2053 0 : msg = muse::trc("project/share", "Your Audio.com account needs to be verified first. "
2054 0 : "Please activate your account via the link in the activation email.");
2055 0 : break;
2056 0 : case int(cloud::Err::UnknownStatusCode): {
2057 0 : if (const auto status = ret.data<int>("status", -1); status != -1) {
2058 : //: %1 will be replaced with the error code that audio.com returned, which is a number.
2059 0 : msg = muse::qtrc("project/share", "Audio.com returned an unknown error code: %1.")
2060 0 : .arg(status).toStdString();
2061 : } else {
2062 0 : msg = muse::trc("project/share", "Audio.com returned an unknown error code.");
2063 : }
2064 0 : msg += "\n\n" + muse::trc("project/share", "Please try again later, or get help for this problem on Audio.com.");
2065 0 : } break;
2066 0 : case int(cloud::Err::NetworkError):
2067 0 : msg = muse::trc("project/share", "Could not connect to Audio.com. "
2068 0 : "Please check your internet connection or try again later.");
2069 0 : break;
2070 0 : default:
2071 0 : msg = muse::trc("project/share", "Please try again later, or get help for this problem on Audio.com.");
2072 0 : break;
2073 : }
2074 :
2075 0 : return interactive()->warning(title, msg, buttons)
2076 0 : .then<Ret>(this, [ret](const IInteractive::Result&, auto resolve) {
2077 0 : return resolve(ret);
2078 0 : });
2079 0 : }
|