public void HandleChooseFolder(ApiRequest request) { var initialPath = request.GetParamOrNull("path"); var description = request.GetParamOrNull("description"); var forOutput = request.GetParamOrNull("forOutput"); var isForOutput = !String.IsNullOrEmpty(forOutput) && forOutput.ToLowerInvariant() == "true"; dynamic result = new DynamicJson(); var book = _bookSelection?.CurrentSelection; bool repeat = false; do { using (var dlg = new FolderBrowserDialog()) { if (!String.IsNullOrEmpty(initialPath)) { dlg.SelectedPath = initialPath; } dlg.ShowNewFolderButton = true; if (!string.IsNullOrEmpty(description)) { dlg.Description = description; } result.success = dlg.ShowDialog() == DialogResult.OK; result.path = result.success ? dlg.SelectedPath : ""; string collectionFolder = string.Empty; repeat = result.success && isForOutput && Utils.MiscUtils.IsFolderInsideBloomCollection(result.path, out collectionFolder); if (repeat) { Utils.MiscUtils.WarnUserOfInvalidFolderChoice(collectionFolder, result.path); // Change the initialFolder to just above the collection folder, or to the documents folder // if that ends up empty. initialPath = Path.GetDirectoryName(collectionFolder); if (String.IsNullOrEmpty(initialPath)) { initialPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); } } } } while (repeat); // We send the result through a websocket rather than simply returning it because // if the user is very slow (one site said FF times out after 90s) the browser may // abandon the request before it completes. The POST result is ignored and the // browser simply listens to the socket. // We'd prefer this request to return immediately and set a callback to run // when the dialog closes and handle the results, but FolderBrowserDialog // does not offer such an API. Instead, we just ignore any timeout // in our Javascript code. _webSocketServer.SendBundle("common", "chooseFolder-results", result); request.PostSucceeded(); }
public static void DoWorkWithProgressDialog(BloomWebSocketServer socketServer, string socketContext, Func <Form> makeDialog, Func <IWebSocketProgress, bool> doWhat, Action <Form> doWhenMainActionFalse = null) { var progress = new WebSocketProgress(socketServer, socketContext); // NOTE: This (specifically ShowDialog) blocks the main thread until the dialog is closed. // Be careful to avoid deadlocks. using (var dlg = makeDialog()) { // For now let's not try to handle letting the user abort. dlg.ControlBox = false; var worker = new BackgroundWorker(); worker.DoWork += (sender, args) => { // A way of waiting until the dialog is ready to receive progress messages while (!socketServer.IsSocketOpen(socketContext)) { Thread.Sleep(50); } bool waitForUserToCloseDialogOrReportProblems; try { waitForUserToCloseDialogOrReportProblems = doWhat(progress); } catch (Exception ex) { // depending on the nature of the problem, we might want to do more or less than this. // But at least this lets the dialog reach one of the states where it can be closed, // and gives the user some idea things are not right. socketServer.SendEvent(socketContext, "finished"); waitForUserToCloseDialogOrReportProblems = true; progress.MessageWithoutLocalizing("Something went wrong: " + ex.Message, ProgressKind.Error); } // stop the spinner socketServer.SendEvent(socketContext, "finished"); if (waitForUserToCloseDialogOrReportProblems) { // Now the user is allowed to close the dialog or report problems. // (ProgressDialog in JS-land is watching for this message, which causes it to turn // on the buttons that allow the dialog to be manually closed (or a problem to be reported). socketServer.SendBundle(socketContext, "show-buttons", new DynamicJson()); } else { // Just close the dialog dlg.Invoke((Action)(() => { if (doWhenMainActionFalse != null) { doWhenMainActionFalse(dlg); } else { dlg.Close(); } })); } }; worker.RunWorkerAsync(); dlg.ShowDialog(); // returns when dialog closed } }
public void HandleCheckInCurrentBook(ApiRequest request) { Action <float> reportCheckinProgress = (fraction) => { dynamic messageBundle = new DynamicJson(); messageBundle.fraction = fraction; _socketServer.SendBundle("checkinProgress", "progress", messageBundle); // The status panel is supposed to be showing a progress bar in response to getting the bundle, // but since we're doing the checkin on the UI thread, it doesn't get painted without this. Application.DoEvents(); }; try { // Right before calling this API, the status panel makes a change that // should make the progress bar visible. But this method is running on // the UI thread so without this call it won't appear until later, when // we have Application.DoEvents() as part of reporting progress. We do // quite a bit on large books before the first file is written to the // zip, so one more DoEvents() here lets the bar appear at once. Application.DoEvents(); _bookSelection.CurrentSelection.Save(); if (!_tcManager.CheckConnection()) { request.Failed(); return; } var bookName = Path.GetFileName(_bookSelection.CurrentSelection.FolderPath); if (_tcManager.CurrentCollection.OkToCheckIn(bookName)) { _tcManager.CurrentCollection.PutBook(_bookSelection.CurrentSelection.FolderPath, true, false, reportCheckinProgress); reportCheckinProgress(0); // cleans up panel for next time // review: not super happy about this being here in the api. Was stymied by // PutBook not knowing about the actual book object, but maybe that could be passed in. BookHistory.AddEvent(_bookSelection.CurrentSelection, BookHistoryEventType.CheckIn); _tcManager.CurrentCollection.PutBook(_bookSelection.CurrentSelection.FolderPath, checkin: true); Analytics.Track("TeamCollectionCheckinBook", new Dictionary <string, string>() { { "CollectionId", _settings?.CollectionId }, { "CollectionName", _settings?.CollectionName }, { "Backend", _tcManager?.CurrentCollection?.GetBackendType() }, { "User", CurrentUser }, { "BookId", _bookSelection?.CurrentSelection.ID }, { "BookName", _bookSelection?.CurrentSelection.Title } }); } else { // We can't check in! The system has broken down...perhaps conflicting checkouts while offline. // Save our version in Lost-and-Found _tcManager.CurrentCollection.PutBook(_bookSelection.CurrentSelection.FolderPath, false, true, reportCheckinProgress); reportCheckinProgress(0); // cleans up panel for next time // overwrite it with the current repo version. _tcManager.CurrentCollection.CopyBookFromRepoToLocal(bookName, dialogOnError: true); // Force a full reload of the book from disk and update the UI to match. _bookSelection.SelectBook(_bookServer.GetBookFromBookInfo(_bookSelection.CurrentSelection.BookInfo, true)); var msg = LocalizationManager.GetString("TeamCollection.ConflictingEditOrCheckout", "Someone else has edited this book or checked it out even though you were editing it! Your changes have been saved to Lost and Found"); ErrorReport.NotifyUserOfProblem(msg); Analytics.Track("TeamCollectionConflictingEditOrCheckout", new Dictionary <string, string>() { { "CollectionId", _settings?.CollectionId }, { "CollectionName", _settings?.CollectionName }, { "Backend", _tcManager?.CurrentCollection?.GetBackendType() }, { "User", CurrentUser }, { "BookId", _bookSelection?.CurrentSelection?.ID }, { "BookName", _bookSelection?.CurrentSelection?.Title } }); } UpdateUiForBook(); request.PostSucceeded(); } catch (Exception e) { reportCheckinProgress(0); // cleans up panel progress indicator var msgId = "TeamCollection.ErrorCheckingBookIn"; var msgEnglish = "Error checking in {0}: {1}"; var log = _tcManager?.CurrentCollection?.MessageLog; // Pushing an error into the log will show the Reload Collection button. It's not obvious this // is useful here, since we don't know exactly what went wrong. However, it at least gives the user // the option to try it. if (log != null) { log.WriteMessage(MessageAndMilestoneType.Error, msgId, msgEnglish, _bookSelection?.CurrentSelection?.FolderPath, e.Message); } Logger.WriteError(String.Format(msgEnglish, _bookSelection?.CurrentSelection?.FolderPath, e.Message), e); NonFatalProblem.ReportSentryOnly(e, $"Something went wrong for {request.LocalPath()} ({_bookSelection?.CurrentSelection?.FolderPath})"); request.Failed("checkin failed"); } }