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