Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 72 additions & 6 deletions src/BloomExe/Spreadsheet/SpreadsheetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Bloom.Properties;
using Bloom.TeamCollection;
using Bloom.ToPalaso;
using Bloom.web;
using L10NSharp;
using SIL.IO;

Expand All @@ -22,18 +23,21 @@ public class SpreadsheetApi
private BookSelection _bookSelection;
private readonly BloomWebSocketServer _webSocketServer;
private readonly TeamCollectionManager _teamCollectionManager;
private readonly BookServer _bookServer;

public SpreadsheetApi(
CollectionModel collectionModel,
BloomWebSocketServer webSocketServer,
BookSelection bookSelection,
TeamCollectionManager teamCollectionManager
TeamCollectionManager teamCollectionManager,
BookServer bookServer
)
{
_collectionModel = collectionModel;
_webSocketServer = webSocketServer;
_bookSelection = bookSelection;
_teamCollectionManager = teamCollectionManager;
_bookServer = bookServer;
}

public void RegisterWithApiHandler(BloomApiHandler apiHandler)
Expand All @@ -58,29 +62,91 @@ private void ExportToSpreadsheet(ApiRequest request)
var bookPath = book.GetPathHtmlFile();
try
{
var dom = new HtmlDom(XmlHtmlConverter.GetXmlDomFromHtmlFile(bookPath, false));
var exporter = new SpreadsheetExporter(_webSocketServer, book.CollectionSettings);
string outputParentFolder = request.RequiredPostDynamic().parentFolderPath;
string outputFolder = Path.Combine(
outputParentFolder,
Path.GetFileNameWithoutExtension(bookPath)
);
SetSpreadsheetFolder(book, outputFolder);
string imagesFolderPath = Path.GetDirectoryName(bookPath);

// The .htm file the DOM is read from and the book folder its images are read from.
// Default to the selected book's location; updated below to the post-processing
// location because normalization can rename the folder/.htm on disk.
string htmPathForExport = bookPath;
string bookFolderForExport = Path.GetDirectoryName(bookPath);

// Before reading the DOM, bring the book up to date and run the per-page browser
// fix-ups so the exported content matches what the user would see (and what would be
// saved) had they visited each page in edit mode. This normalizes things the raw
// saved .htm may be missing (e.g. paragraph wrapping of bare text, canvas-element
// migration). This mutates the book on disk, exactly as an edit-mode visit does, so
// we only do it when the book is actually saveable (matching edit-mode semantics: a
// Team Collection book that isn't checked out, or any read-only book, would not get
// this treatment in the editor either). This work can take several seconds per book,
// so getDomAndFolder runs it on the progress dialog's background worker rather than
// here on the UI thread.
Func<IWebSocketProgress, (HtmlDom, string)> getDomAndFolder = progress =>
{
if (book.IsSaveable)
{
progress.MessageWithoutLocalizing("Updating pages to the current version...");
// Process a fresh Book instance read from disk so we don't disturb the
// in-memory selected book; ProcessBook rewrites the .htm in place.
var bookInfo = new BookInfo(
book.FolderPath,
true,
new AlwaysEditSaveContext()
);
var freshBook = _bookServer.GetBookFromBookInfo(bookInfo);
BookProcessor.ProcessBook(freshBook);
// ProcessBook's save may have renamed the folder/.htm to match the title, so
// read the DOM (and, via the returned folder, the images) from the fresh
// book's current location rather than the paths captured before processing.
htmPathForExport = freshBook.GetPathHtmlFile();
bookFolderForExport = freshBook.FolderPath;
}
return (
new HtmlDom(
XmlHtmlConverter.GetXmlDomFromHtmlFile(htmPathForExport, false)
),
bookFolderForExport
);
};

// If we rewrote the book on disk above, the in-memory selected book (which may be
// open in the hidden Edit tab) still holds its pre-normalization DOM and would
// clobber our changes the next time it is saved. Reload it from disk once the dialog
// closes (on the UI thread), pointing it at the (possibly renamed) folder. Mirrors
// what the spreadsheet import path does.
Action reconcileSelectedBook = () =>
{
if (!book.IsSaveable)
return;
var renamedTo = string.Equals(
bookFolderForExport,
book.FolderPath,
StringComparison.OrdinalIgnoreCase
)
? null
: bookFolderForExport;
book.ReloadFromDisk(renamedTo);
_bookSelection.InvokeSelectionChanged(false);
};

// Fire-and-forget: the returned Task completes once the background progress-dialog
// worker has been started, not when the export finishes. Completion is delivered via
// the resultCallback below (which opens the finished spreadsheet), so there is nothing
// useful to await here. The discard makes the intentional non-await explicit.
_ = exporter.ExportToFolderWithProgressAsync(
dom,
imagesFolderPath,
getDomAndFolder,
outputFolder,
outputFilePath =>
{
if (outputFilePath != null)
ProcessExtra.SafeStartInFront(outputFilePath);
}
},
reconcileSelectedBook
);
}
catch (Exception ex)
Expand Down
18 changes: 14 additions & 4 deletions src/BloomExe/Spreadsheet/SpreadsheetExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,25 @@ public SpreadsheetExporter(ILanguageDisplayNameResolver langDisplayNameResolver)
};

public async Task ExportToFolderWithProgressAsync(
HtmlDom dom,
string bookFolderPath,
Func<IWebSocketProgress, (HtmlDom Dom, string BookFolderPath)> getDomAndFolder,
string outputFolder,
Action<string> resultCallback
Action<string> resultCallback,
Action doWhenDialogCloses = null
)
{
await BrowserProgressDialog.DoWorkWithProgressDialogAsync(
_webSocketServer,
async (progress, worker) =>
{
// getDomAndFolder may do slow work (e.g. bringing the book up to date and
// running the per-page browser fix-ups so the exported content matches what the
// user would see after visiting each page in edit mode) before handing us the
// DOM to export. We obtain it here on the background worker thread rather than
// up front on the UI thread so that work does not freeze the UI. It returns the
// book folder alongside the DOM because that work can rename the book folder on
// disk (a save renames it to match the title), so the images must be read from
// the post-processing location, not a path captured beforehand.
var (dom, bookFolderPath) = getDomAndFolder(progress);
var spreadsheet = ExportToFolder(
dom,
bookFolderPath,
Expand All @@ -107,7 +116,8 @@ await BrowserProgressDialog.DoWorkWithProgressDialogAsync(
},
"collectionTab",
"Exporting Spreadsheet",
showCancelButton: false
showCancelButton: false,
doWhenDialogCloses: doWhenDialogCloses
);
}

Expand Down