LCOV - code coverage report
Current view: top level - src/project/internal - convertfiletoscorescenario.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 93.0 % 401 373
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              : #include "convertfiletoscorescenario.h"
      23              : 
      24              : #include <QUrl>
      25              : 
      26              : #include "global/translation.h"
      27              : #include "global/dataformatter.h"
      28              : #include "global/io/ioretcodes.h"
      29              : #include "global/log.h"
      30              : 
      31              : #include "project/projecterrors.h"
      32              : #include "project/projectcommands.h"
      33              : 
      34              : using namespace mu::project;
      35              : using namespace muse;
      36              : using namespace muse::cloud;
      37              : 
      38              : static constexpr muse::secs_t RETRY_TOAST_ELAPSED_THRESHOLD_SECS = muse::secs_t::make(5.0 * 60.0);
      39              : 
      40           53 : ConvertFileToScoreScenario::ConvertFileToScoreScenario(const muse::modularity::ContextPtr& iocCtx)
      41          106 :     : muse::Contextable(iocCtx)
      42              : {
      43          106 : }
      44              : 
      45           10 : void ConvertFileToScoreScenario::init()
      46              : {
      47           11 :     TRACEFUNC;
      48              : 
      49           10 :     service()->convertFinished().onReceive(this, [this](const Ret& ret, const WatchedScore& watched) {
      50            6 :         if (ret) {
      51            3 :             showScoreReadyNotification(watched);
      52              :         } else {
      53            3 :             showConvertFailedNotification(watched);
      54              :         }
      55              : 
      56            6 :         m_convertFinished.send(ret, watched);
      57            6 :     });
      58              : 
      59           10 :     service()->pollingStatusChanged().onReceive(this, [this](const PollingStatus& status) {
      60            8 :         if (std::holds_alternative<PollingSuccess>(status)) {
      61            0 :             m_retryToastShown = false;
      62            0 :             return;
      63              :         }
      64              : 
      65            8 :         const PollingFailure& failure = std::get<PollingFailure>(status);
      66              : 
      67            8 :         if (failure.gaveUp) {
      68            3 :             showPollingGaveUpNotification();
      69            3 :             return;
      70              :         }
      71              : 
      72            5 :         if (!m_retryToastShown && failure.elapsed >= RETRY_TOAST_ELAPSED_THRESHOLD_SECS) {
      73            1 :             m_retryToastShown = true;
      74            1 :             showPollingFailureNotification();
      75              :         }
      76              :     });
      77           10 : }
      78              : 
      79            2 : const ConvertConfig& ConvertFileToScoreScenario::config() const
      80              : {
      81            2 :     return service()->config();
      82              : }
      83              : 
      84            1 : bool ConvertFileToScoreScenario::isFileSupported(const io::path_t& path) const
      85              : {
      86            1 :     return service()->isFileSupported(path);
      87              : }
      88              : 
      89           14 : RetVal<ConvertFilesValidation> ConvertFileToScoreScenario::validateFiles(const io::paths_t& paths)
      90              : {
      91           14 :     RetVal<ConvertFilesValidation> result = service()->validateFiles(paths);
      92           14 :     if (!result.ret) {
      93           10 :         showValidationError(result.ret);
      94              :     }
      95              : 
      96           14 :     return result;
      97            0 : }
      98              : 
      99            5 : Ret ConvertFileToScoreScenario::validateLink(const QUrl& link)
     100              : {
     101            5 :     Ret ret = service()->validateLink(link);
     102            5 :     if (!ret) {
     103            4 :         showUnsupportedLinkError();
     104              :     }
     105              : 
     106            5 :     return ret;
     107            0 : }
     108              : 
     109           25 : void ConvertFileToScoreScenario::convertFiles(const io::paths_t& paths)
     110              : {
     111              :     //! NOTE Guards against repeated clicks opening multiple dialogs
     112              :     //! while checkConvertIsAllowed is still pending
     113           25 :     if (m_convertFlowInProgress) {
     114            1 :         return;
     115              :     }
     116              : 
     117           24 :     m_convertFlowInProgress = true;
     118              : 
     119           48 :     checkConvertIsAllowed()
     120           48 :     .onResolve(this, [this, paths](const Ret& ret) {
     121           24 :         if (!ret) {
     122            2 :             m_convertFlowInProgress = false;
     123           21 :             return;
     124              :         }
     125              : 
     126           22 :         if (paths.empty()) {
     127           36 :             selectFilesToConvert()
     128           36 :             .onResolve(this, [this](const ConvertSelection& selection) {
     129           16 :                 startConvert(selection.input, selection.convertedScoreName);
     130           16 :                 m_convertFlowInProgress = false;
     131           16 :             })
     132           18 :             .onReject(this, [this](int, const std::string&) {
     133            0 :                 m_convertFlowInProgress = false;
     134            0 :             });
     135           18 :             return;
     136              :         }
     137              : 
     138            4 :         RetVal<ConvertFilesValidation> validation = validateFiles(paths);
     139            4 :         if (!validation.ret) {
     140            1 :             m_convertFlowInProgress = false;
     141            1 :             return;
     142              :         }
     143              : 
     144            3 :         confirmConvert(paths, validation.val.type);
     145            4 :     })
     146           24 :     .onReject(this, [this](int, const std::string&) {
     147            0 :         m_convertFlowInProgress = false;
     148            0 :     });
     149              : }
     150              : 
     151            3 : async::Channel<Ret, WatchedScore> ConvertFileToScoreScenario::convertFinished() const
     152              : {
     153            3 :     return m_convertFinished;
     154              : }
     155              : 
     156            0 : ValNt<UploadingScoreList> ConvertFileToScoreScenario::uploadingScores() const
     157              : {
     158            0 :     return service()->uploadingScores();
     159              : }
     160              : 
     161            0 : ValNt<WatchedScoreList> ConvertFileToScoreScenario::watchedScores() const
     162              : {
     163            0 :     return service()->watchedScores();
     164              : }
     165              : 
     166            0 : bool ConvertFileToScoreScenario::isAwaitingReview(int scoreId) const
     167              : {
     168            0 :     const WatchedScore* watched = service()->watchedScoreById(scoreId);
     169            0 :     return watched && watched->conversion.status == ConvertStatus::AwaitingReview;
     170              : }
     171              : 
     172            2 : void ConvertFileToScoreScenario::cancelUpload(int uploadId)
     173              : {
     174            2 :     confirmCancel([this, uploadId]() {
     175            1 :         service()->cancelUpload(uploadId);
     176            1 :     });
     177            2 : }
     178              : 
     179            2 : void ConvertFileToScoreScenario::cancelConversion(ConvertType type, int convertId)
     180              : {
     181            2 :     confirmCancel([this, type, convertId]() {
     182            1 :         service()->cancelConversion(type, convertId);
     183            1 :     });
     184            2 : }
     185              : 
     186            4 : void ConvertFileToScoreScenario::confirmCancel(const std::function<void()>& onCancel)
     187              : {
     188            4 :     constexpr int keepConvertingBtn = int(IInteractive::Button::No);
     189            4 :     constexpr int cancelBtn = int(IInteractive::Button::Yes);
     190              : 
     191            4 :     IInteractive::ButtonData keepConverting(keepConvertingBtn, muse::trc("project/convert", "Continue converting"));
     192            4 :     keepConverting.role = IInteractive::ButtonRole::RejectRole;
     193              : 
     194            4 :     IInteractive::ButtonData cancel(cancelBtn, muse::trc("project/convert", "Yes, cancel"), /*accent*/ true);
     195            4 :     cancel.role = IInteractive::ButtonRole::DestructiveRole;
     196              : 
     197           32 :     interactive()->question(
     198            8 :         muse::trc("project/convert", "Are you sure you want to cancel this file conversion?"),
     199            8 :         muse::trc("project/convert", "Processing will be canceled and this score will be removed from your scores."),
     200              :         { keepConverting, cancel }, keepConvertingBtn, IInteractive::WithIcon)
     201            4 :     .onResolve(this, [onCancel, cancelBtn](const IInteractive::Result& result) {
     202            4 :         if (result.isButton(cancelBtn)) {
     203            2 :             onCancel();
     204              :         }
     205            4 :     });
     206            8 : }
     207              : 
     208            0 : async::Channel<PollingStatus> ConvertFileToScoreScenario::pollingStatusChanged() const
     209              : {
     210            0 :     return service()->pollingStatusChanged();
     211              : }
     212              : 
     213            1 : void ConvertFileToScoreScenario::retryPolling()
     214              : {
     215            1 :     service()->retryPolling();
     216            1 : }
     217              : 
     218           24 : async::Promise<Ret> ConvertFileToScoreScenario::checkConvertIsAllowed()
     219              : {
     220           48 :     return async::make_promise<Ret>([this](auto resolve, auto reject) {
     221           48 :         museScoreComService()->authorization()->checkCloudIsAvailable()
     222           24 :         .onResolve(this, [this, resolve, reject](const Ret& ret) {
     223           24 :             if (!ret) {
     224            2 :                 showCloudIsNotAvailableError();
     225            2 :                 (void)resolve(ret);
     226            2 :                 return;
     227              :             }
     228              : 
     229           44 :             ensureAuthorization()
     230           44 :             .onResolve(this, [resolve](const Ret& ret) {
     231           22 :                 (void)resolve(ret);
     232              :             })
     233           22 :             .onReject(this, [reject](int code, const std::string& msg) {
     234            0 :                 (void)reject(code, msg);
     235              :             });
     236              :         });
     237              : 
     238           24 :         return async::Promise<Ret>::dummy_result();
     239           48 :     });
     240              : }
     241              : 
     242           22 : async::Promise<Ret> ConvertFileToScoreScenario::ensureAuthorization()
     243              : {
     244           44 :     return async::make_promise<Ret>([this](auto resolve, auto reject) {
     245           22 :         IAuthorizationServicePtr authorizationService = museScoreComService()->authorization();
     246           22 :         if (authorizationService->userAuthorized().val) {
     247           21 :             return resolve(make_ok());
     248              :         }
     249              : 
     250            1 :         std::string dialogText = muse::trc("project/convert", "Log in or create a free account on MuseScore.com to convert a file.");
     251              : 
     252            1 :         UriQuery query("muse://cloud/requireauthorization");
     253            3 :         query.addParam("text", Val(dialogText));
     254            3 :         query.addParam("cloudCode", Val(authorizationService->cloudInfo().code));
     255            3 :         query.addParam("publishingScore", Val(false));
     256              : 
     257            1 :         interactive()->open(query)
     258            2 :         .onResolve(this, [resolve](const Val&) {
     259            1 :             (void)resolve(make_ok());
     260              :         })
     261            1 :         .onReject(this, [reject](int code, const std::string& msg) {
     262            0 :             (void)reject(code, msg);
     263              :         });
     264              : 
     265            1 :         return async::Promise<Ret>::dummy_result();
     266           66 :     });
     267              : }
     268              : 
     269           20 : async::Promise<ConvertFileToScoreScenario::ConvertSelection> ConvertFileToScoreScenario::selectFilesToConvert(const io::paths_t& paths,
     270              :                                                                                                               ConvertType type)
     271              : {
     272           20 :     UriQuery query("musescore://project/convert/selectfiles");
     273              : 
     274           20 :     if (!paths.empty()) {
     275            2 :         ValList pathsList;
     276            2 :         pathsList.reserve(paths.size());
     277            4 :         for (const io::path_t& path : paths) {
     278            2 :             pathsList.push_back(Val(path));
     279              :         }
     280            6 :         query.addParam("initialPaths", Val(pathsList));
     281            6 :         query.addParam("initialConvertType", Val(type));
     282            2 :     }
     283              : 
     284           20 :     return interactive()->open(query)
     285           20 :            .then<ConvertSelection>(this, [this](const Val& val, auto resolve) {
     286           18 :         return resolve(toConvertSelection(val));
     287           40 :     });
     288           20 : }
     289              : 
     290           18 : ConvertFileToScoreScenario::ConvertSelection ConvertFileToScoreScenario::toConvertSelection(const Val& val) const
     291              : {
     292           18 :     const QVariantMap map = val.toQVariant().toMap();
     293           18 :     const ConvertType type = static_cast<ConvertType>(map.value("type").toInt());
     294           18 :     const QString link = map.value("link").toString();
     295           18 :     const QStringList pathsList = map.value("paths").toStringList();
     296              : 
     297           18 :     io::paths_t paths;
     298           18 :     paths.reserve(pathsList.size());
     299           36 :     for (const QString& path : pathsList) {
     300           18 :         paths.push_back(io::path_t(path));
     301              :     }
     302              : 
     303           18 :     ConvertSelection selection;
     304           18 :     selection.convertedScoreName = map.value("convertedScoreName").toString();
     305              : 
     306           18 :     if (type == ConvertType::Audio2Score && !link.isEmpty()) {
     307            1 :         selection.input = Audio2ScoreConvertInput { QUrl::fromUserInput(link) };
     308           17 :     } else if (type == ConvertType::Audio2Score) {
     309            1 :         selection.input = Audio2ScoreConvertInput { paths };
     310              :     } else {
     311           16 :         selection.input = OmrConvertInput { paths };
     312              :     }
     313              : 
     314           18 :     return selection;
     315           18 : }
     316              : 
     317            3 : void ConvertFileToScoreScenario::confirmConvert(const io::paths_t& paths, ConvertType type)
     318              : {
     319            3 :     constexpr int proceedBtn = int(IInteractive::Button::CustomButton) + 1;
     320              : 
     321            3 :     IInteractive::ButtonData cancel = interactive()->buttonData(IInteractive::Button::Cancel);
     322            3 :     cancel.role = IInteractive::ButtonRole::RejectRole;
     323              : 
     324            3 :     IInteractive::ButtonData proceed(proceedBtn, muse::trc("global", "Proceed"), /*accent*/ true);
     325            3 :     proceed.role = IInteractive::ButtonRole::AcceptRole;
     326              : 
     327           24 :     interactive()->question(muse::trc("project/convert", "Would you like to convert this file to a score?"),
     328            6 :                             muse::trc("project/convert",
     329              :                                       "This file needs to be converted online before it can be edited. Would you like to proceed?"),
     330              :                             { cancel, proceed }, proceedBtn)
     331            6 :     .onResolve(this, [this, paths, type, proceedBtn](const IInteractive::Result& result) {
     332            3 :         if (!result.isButton(proceedBtn)) {
     333            1 :             m_convertFlowInProgress = false;
     334            1 :             return;
     335              :         }
     336              : 
     337            4 :         selectFilesToConvert(paths, type)
     338            4 :         .onResolve(this, [this](const ConvertSelection& selection) {
     339            2 :             startConvert(selection.input, selection.convertedScoreName);
     340            2 :             m_convertFlowInProgress = false;
     341            2 :         })
     342            2 :         .onReject(this, [this](int, const std::string&) {
     343            0 :             m_convertFlowInProgress = false;
     344            0 :         });
     345              :     })
     346            3 :     .onReject(this, [this](int, const std::string&) {
     347            0 :         m_convertFlowInProgress = false;
     348            0 :     });
     349            6 : }
     350              : 
     351           19 : void ConvertFileToScoreScenario::startConvert(const ConvertInput& input, const muse::String& convertedScoreName)
     352              : {
     353           19 :     RetVal<Progress> progress = service()->uploadToConvert(input, convertedScoreName);
     354           19 :     if (!progress.ret) {
     355            3 :         showUploadError(progress.ret, input, convertedScoreName);
     356            3 :         return;
     357              :     }
     358              : 
     359           16 :     if (configuration()->showConvertFileProcessingDialog()) {
     360            4 :         showFileProcessingDialog();
     361              :     }
     362              : 
     363           16 :     progress.val.finished().onReceive(this, [this, input, convertedScoreName](const ProgressResult& res) {
     364            4 :         if (!res.ret && res.ret.code() != static_cast<int>(Ret::Code::Cancel)) {
     365            3 :             showUploadError(res.ret, input, convertedScoreName);
     366              :         }
     367            4 :     });
     368           19 : }
     369              : 
     370            6 : void ConvertFileToScoreScenario::showUploadError(const Ret& ret, const ConvertInput& input, const muse::String& convertedScoreName)
     371              : {
     372            6 :     switch (ret.code()) {
     373            2 :     case static_cast<int>(Err::ConvertUploadFailed):
     374            2 :         showUploadFailedNotification(input, convertedScoreName);
     375            2 :         break;
     376            1 :     case static_cast<int>(Err::ConvertRateLimited):
     377            1 :         showUploadLimitReachedError();
     378            1 :         break;
     379            2 :     case static_cast<int>(io::Err::FSNotExist):
     380              :     case static_cast<int>(io::Err::FSReadError):
     381            2 :         showFileReadError(convertPathsOf(input).size());
     382            2 :         break;
     383            1 :     default:
     384            1 :         showUnknownError();
     385            1 :         break;
     386              :     }
     387            6 : }
     388              : 
     389           10 : void ConvertFileToScoreScenario::showValidationError(const Ret& ret)
     390              : {
     391           10 :     const ConvertConfig& config = service()->config();
     392              : 
     393           10 :     switch (static_cast<Err>(ret.code())) {
     394            2 :     case Err::ConvertUnsupportedFormat:
     395            2 :         showUnsupportedFormatError();
     396            2 :         break;
     397            1 :     case Err::ConvertMixedFileTypes:
     398            1 :         showMixedFileTypesError();
     399            1 :         break;
     400            1 :     case Err::ConvertMultiplePdfFiles:
     401            1 :         showMultiplePdfFilesError();
     402            1 :         break;
     403            1 :     case Err::ConvertAudioFileTooLarge:
     404            1 :         showFileTooLargeError(config.audio2score.file.maxFileSizeBytes);
     405            1 :         break;
     406            1 :     case Err::ConvertFileTooLarge:
     407            1 :         showFileTooLargeError(config.omr.pdf.maxFileSizeBytes);
     408            1 :         break;
     409            1 :     case Err::ConvertCombinedImageTooLarge:
     410            1 :         showCombinedImageSizeTooLargeError(config.omr.images.maxFileSizeBytes);
     411            1 :         break;
     412            1 :     case Err::ConvertTooManyAudioFiles:
     413            1 :         showTooManyAudioFilesError(config.audio2score.file.maxFiles);
     414            1 :         break;
     415            1 :     case Err::ConvertTooManyImages:
     416            1 :         showTooManyImagesError(config.omr.images.maxFiles);
     417            1 :         break;
     418            1 :     default:
     419            1 :         showUnknownError();
     420            1 :         break;
     421              :     }
     422           10 : }
     423              : 
     424            2 : void ConvertFileToScoreScenario::showCloudIsNotAvailableError()
     425              : {
     426           12 :     interactive()->warning(muse::trc("project/convert", "Unable to connect to MuseScore.com"),
     427            4 :                            muse::trc("project/convert",
     428              :                                      "An internet connection is required for file conversion. Please check your internet connection or try again later."),
     429            2 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     430            6 : }
     431              : 
     432            2 : void ConvertFileToScoreScenario::showUnknownError()
     433              : {
     434           12 :     interactive()->warning(muse::trc("project/convert", "Something went wrong"),
     435            4 :                            muse::trc("project/convert", "Check your internet connection and try again."),
     436            2 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     437            6 : }
     438              : 
     439            2 : void ConvertFileToScoreScenario::showFileReadError(size_t fileCount)
     440              : {
     441            2 :     if (fileCount > 1) {
     442            6 :         interactive()->warning(muse::trc("project/convert", "One or more files could not be read"),
     443            2 :                                muse::trc("project/convert", "Make sure the files still exist and that you have permission to open them."),
     444            1 :                                { interactive()->buttonData(IInteractive::Button::Ok) });
     445              :     } else {
     446            6 :         interactive()->warning(muse::trc("project/convert", "This file could not be read"),
     447            2 :                                muse::trc("project/convert", "Make sure the file still exists and that you have permission to open it."),
     448            1 :                                { interactive()->buttonData(IInteractive::Button::Ok) });
     449              :     }
     450            6 : }
     451              : 
     452            2 : void ConvertFileToScoreScenario::showFileTooLargeError(qint64 maxFileSizeBytes)
     453              : {
     454            2 :     QString size = DataFormatter::formatFileSize(size_t(maxFileSizeBytes));
     455            2 :     std::string text = muse::qtrc("project/convert", "The maximum file size is %1. Reduce the size of your file and try again.")
     456            2 :                        .arg(size).toStdString();
     457            8 :     interactive()->warning(muse::trc("project/convert", "This file is too large"), text,
     458            2 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     459            6 : }
     460              : 
     461            1 : void ConvertFileToScoreScenario::showCombinedImageSizeTooLargeError(qint64 maxFileSizeBytes)
     462              : {
     463            1 :     QString size = DataFormatter::formatFileSize(size_t(maxFileSizeBytes));
     464            1 :     std::string text = muse::qtrc("project/convert",
     465              :                                   "The maximum combined file size for all images is %1. Choose a smaller file or remove some images to continue.")
     466            1 :                        .arg(size).toStdString();
     467            4 :     interactive()->warning(muse::trc("project/convert", "Maximum file size exceeded"), text,
     468            1 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     469            3 : }
     470              : 
     471            2 : void ConvertFileToScoreScenario::showUnsupportedFormatError()
     472              : {
     473           12 :     interactive()->warning(muse::trc("project/convert", "This file type is not compatible"),
     474            4 :                            muse::trc("project/convert", "Make sure you’re importing a suitable PDF, image or audio file."),
     475            2 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     476            6 : }
     477              : 
     478            4 : void ConvertFileToScoreScenario::showUnsupportedLinkError()
     479              : {
     480            4 :     const LinkSources sources = service()->config().audio2score.link.allowedSources;
     481              : 
     482            4 :     std::string text;
     483            4 :     if (sources.testFlag(LinkSource::YouTube) && sources.testFlag(LinkSource::AudioCom)) {
     484            1 :         text = muse::trc("project/convert", "Make sure you’re using a valid link from YouTube or Audio.com.");
     485            3 :     } else if (sources.testFlag(LinkSource::YouTube)) {
     486            1 :         text = muse::trc("project/convert", "Make sure you’re using a valid link from YouTube.");
     487            2 :     } else if (sources.testFlag(LinkSource::AudioCom)) {
     488            1 :         text = muse::trc("project/convert", "Make sure you’re using a valid link from Audio.com.");
     489              :     }
     490              : 
     491           16 :     interactive()->warning(muse::trc("project/convert", "Please use a compatible URL"), text,
     492            4 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     493           12 : }
     494              : 
     495            1 : void ConvertFileToScoreScenario::showMixedFileTypesError()
     496              : {
     497            6 :     interactive()->warning(muse::trc("project/convert", "Please select files of the same type"),
     498            2 :                            muse::trc("project/convert",
     499              :                                      "Per conversion, you may select either one audio file, one PDF file, or multiple image files."),
     500            1 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     501            3 : }
     502              : 
     503            1 : void ConvertFileToScoreScenario::showMultiplePdfFilesError()
     504              : {
     505            6 :     interactive()->warning(muse::trc("project/convert", "Please select a single PDF file"),
     506            2 :                            muse::trc("project/convert", "Only one PDF file can be converted at a time."),
     507            1 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     508            3 : }
     509              : 
     510            1 : void ConvertFileToScoreScenario::showTooManyAudioFilesError(int maxFiles)
     511              : {
     512            2 :     std::string text = muse::qtrc("project/convert", "You can convert up to %n audio file(s) at a time. Remove some files and try again.",
     513            1 :                                   nullptr, maxFiles).toStdString();
     514            4 :     interactive()->warning(muse::trc("project/convert", "Too many files selected"), text,
     515            1 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     516            3 : }
     517              : 
     518            1 : void ConvertFileToScoreScenario::showTooManyImagesError(int maxImages)
     519              : {
     520            2 :     std::string text = muse::qtrc("project/convert", "You can convert up to %n image(s) at a time. Remove some images and try again.",
     521            1 :                                   nullptr, maxImages).toStdString();
     522            4 :     interactive()->warning(muse::trc("project/convert", "Too many images selected"), text,
     523            1 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     524            3 : }
     525              : 
     526            1 : void ConvertFileToScoreScenario::showUploadLimitReachedError()
     527              : {
     528            6 :     interactive()->warning(muse::trc("project/convert", "Upload limit reached"),
     529            2 :                            muse::trc("project/convert", "You’ve reached the limit for file conversions. Please try again later."),
     530            1 :                            { interactive()->buttonData(IInteractive::Button::Ok) });
     531            3 : }
     532              : 
     533            4 : void ConvertFileToScoreScenario::showFileProcessingDialog()
     534              : {
     535           12 :     interactive()->open("musescore://project/convert/processing")
     536            4 :     .onResolve(this, [this](const Val& val) {
     537            3 :         const QVariantMap map = val.toQVariant().toMap();
     538            3 :         const QString action = map.value("action").toString();
     539              : 
     540            3 :         configuration()->setShowConvertFileProcessingDialog(map.value("showAgain").toBool());
     541              : 
     542            3 :         if (action == "convertMore") {
     543            0 :             convertFiles();
     544            3 :         } else if (action == "goToScores") {
     545            3 :             interactive()->open("musescore://home?section=scores&subSection=myOnlineScores");
     546              :         }
     547            3 :     });
     548            4 : }
     549              : 
     550            3 : void ConvertFileToScoreScenario::showScoreReadyNotification(const WatchedScore& watched)
     551              : {
     552            3 :     IF_ASSERT_FAILED(watched.scoreId) {
     553            1 :         return;
     554              :     }
     555              : 
     556            3 :     const io::path_t path = configuration()->cloudProjectPath(*watched.scoreId);
     557            3 :     if (multiwindowsProvider()->isProjectAlreadyOpened(path)) {
     558            1 :         return;
     559              :     }
     560              : 
     561            2 :     constexpr int openScoreBtn = int(toast::ToastActionCode::Custom) + 1;
     562            2 :     const int scoreId = *watched.scoreId;
     563              : 
     564            2 :     std::string msg = muse::qtrc("project/convert", "‘%1’ has finished processing and is ready to open.")
     565            2 :                       .arg(watched.name.toQString()).toStdString();
     566              : 
     567           12 :     toastService()->show(muse::trc("project/convert", "Your score is ready!"), msg,
     568              :                          muse::ui::IconCode::Code::TICK_FILLED, true,
     569              :     {
     570            0 :         { muse::trc("global", "Dismiss"), toast::ToastActionCode::Dismiss },
     571            2 :         { muse::trc("project/convert", "Open score"), openScoreBtn, /*accent*/ true },
     572            2 :     }).onResolve(this, [this, scoreId, openScoreBtn](const toast::ToastResult& result) {
     573            1 :         if (result.isCode(openScoreBtn)) {
     574            2 :             const QUrl url(QString("musescore://open-score/%1").arg(scoreId));
     575            4 :             commandDispatcher()->dispatch(project::PROJECT_OPEN_COMMAND, { { "url", muse::Val(url.toString()) } });
     576            1 :         }
     577            3 :     });
     578            5 : }
     579              : 
     580            3 : void ConvertFileToScoreScenario::showConvertFailedNotification(const WatchedScore& watched)
     581              : {
     582            3 :     if (watched.name.isEmpty()) {
     583            1 :         return;
     584              :     }
     585              : 
     586            2 :     std::string msg = muse::qtrc("project/convert", "We weren’t able to convert ‘%1’. Please try again with a better quality file.")
     587            2 :                       .arg(watched.name.toQString()).toStdString();
     588              : 
     589           12 :     toastService()->show(muse::trc("project/convert", "Error processing score"), msg,
     590              :                          muse::ui::IconCode::Code::ERROR_FILLED, true,
     591              :     {
     592            0 :         { muse::trc("project/convert", "Try another file"), toast::ToastActionCode::TryAgain },
     593            2 :         { muse::trc("global", "OK"), toast::ToastActionCode::Dismiss, /*accent*/ true },
     594            2 :     }).onResolve(this, [this](const toast::ToastResult& result) {
     595            1 :         if (result.isCode(toast::ToastActionCode::TryAgain)) {
     596            1 :             convertFiles();
     597              :         }
     598            1 :     });
     599            4 : }
     600              : 
     601            2 : void ConvertFileToScoreScenario::showUploadFailedNotification(const ConvertInput& input, const muse::String& convertedScoreName)
     602              : {
     603            2 :     std::string msg = muse::qtrc("project/convert", "We weren’t able to upload ‘%1’. Check your internet connection and try again.")
     604            2 :                       .arg(convertedScoreName.toQString()).toStdString();
     605              : 
     606           12 :     toastService()->show(muse::trc("project/convert", "Error uploading score"), msg,
     607              :                          muse::ui::IconCode::Code::ERROR_FILLED, true,
     608              :     {
     609            0 :         { muse::trc("global", "Dismiss"), toast::ToastActionCode::Dismiss },
     610            2 :         { muse::trc("global", "Retry"), toast::ToastActionCode::TryAgain, /*accent*/ true },
     611            2 :     }).onResolve(this, [this, input, convertedScoreName](const toast::ToastResult& result) {
     612            2 :         if (result.isCode(toast::ToastActionCode::TryAgain)) {
     613            1 :             startConvert(input, convertedScoreName);
     614              :         }
     615            2 :     });
     616            4 : }
     617              : 
     618            1 : void ConvertFileToScoreScenario::showPollingFailureNotification()
     619              : {
     620            2 :     toastService()->showWarning(
     621            2 :         muse::trc("project/convert", "We’re having trouble connecting to MuseScore.com."),
     622            2 :         muse::trc("project/convert", "We’ll keep trying intermittently."));
     623            1 : }
     624              : 
     625            3 : void ConvertFileToScoreScenario::showPollingGaveUpNotification()
     626              : {
     627           21 :     toastService()->show(muse::trc("project/convert", "Unable to connect to MuseScore.com"),
     628            6 :                          muse::trc("project/convert",
     629              :                                    "An internet connection is required for file conversion. Please check your internet connection or try again later."),
     630              :                          muse::ui::IconCode::Code::ERROR_FILLED, true,
     631              :     {
     632            0 :         { muse::trc("global", "Dismiss"), toast::ToastActionCode::Dismiss },
     633            3 :         { muse::trc("global", "Retry"), toast::ToastActionCode::TryAgain, /*accent*/ true },
     634            3 :     }).onResolve(this, [this](const toast::ToastResult& result) {
     635            2 :         if (result.isCode(toast::ToastActionCode::TryAgain)) {
     636            1 :             retryPolling();
     637              :         }
     638            2 :     });
     639            6 : }
        

Generated by: LCOV version 2.0-1