LCOV - code coverage report
Current view: top level - src/project/internal - saveprojectscenario.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 61.9 % 1212 750
Test Date: 2026-11-05 04:07:02

            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 : }
        

Generated by: LCOV version 2.0-1