diff --git a/.vscode/settings.json b/.vscode/settings.json index 07a34c23f284..8665ed1fc82f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -68,5 +68,13 @@ "Winforms", "xmatter" ], - "chat.useNestedAgentsMdFiles": true + "chat.useNestedAgentsMdFiles": true, + "workbench.colorCustomizations": { + "statusBar.background": "#96668f", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#ab84a5", + "statusBarItem.remoteBackground": "#96668f", + "statusBarItem.remoteForeground": "#e7e7e7" + }, + "peacock.color": "#96668f" } diff --git a/src/BloomBrowserUI/bookEdit/toolbox/canvas/image-overlay.svg b/src/BloomBrowserUI/bookEdit/toolbox/canvas/image-overlay.svg index edda0a2d1977..e2b395aa3b80 100644 --- a/src/BloomBrowserUI/bookEdit/toolbox/canvas/image-overlay.svg +++ b/src/BloomBrowserUI/bookEdit/toolbox/canvas/image-overlay.svg @@ -1,3 +1,8 @@ - - + + + + + + + diff --git a/src/BloomBrowserUI/images/placeHolder.png b/src/BloomBrowserUI/images/placeHolder.png index 5075adae58cc..5cefd0479994 100644 Binary files a/src/BloomBrowserUI/images/placeHolder.png and b/src/BloomBrowserUI/images/placeHolder.png differ diff --git a/src/BloomBrowserUI/placeHolderImages.less b/src/BloomBrowserUI/placeHolderImages.less index ac3d02807bde..f613ea66bafb 100644 --- a/src/BloomBrowserUI/placeHolderImages.less +++ b/src/BloomBrowserUI/placeHolderImages.less @@ -1,8 +1,12 @@ -//@image-placeholder: url('data:image/svg+xml,'); +// bloom blue looks better on white, but conflicts with some page colors #3994a5; +@placeholder-color-encoded: "%234f4f4f"; // %23 instead of # for URL encoding +@placeholder-opacity: 0.2; -@image-placeholder: url('data:image/svg+xml,'); +@image-placeholder: url('data:image/svg+xml,'); -@canvas-placeholder: url('data:image/svg+xml,'); +@canvas-placeholder: url('data:image/svg+xml,'); + +@video-placeholder: url('data:image/svg+xml,'); .image-placeholder-background() { background-image: @image-placeholder; @@ -20,9 +24,15 @@ .image-placeholder-background(); } +.customPage .bloom-videoContainer.bloom-noVideoSelected { + background: @video-placeholder no-repeat center; + background-size: contain; +} + // When previewing templates in the Collection Tab, and also when in Change Layout mode in the Edit tab, // img elements may not be wrapped in canvas-element divs (see BL-15514). Also below. // Real books always have a data-l1 attribute added +.preview.template .bloom-canvas:has(img[src*="placeHolder.png"]), .preview:not([data-l1]) .bloom-canvas:has(img[src*="placeHolder.png"]), .origami-layout-mode .bloom-canvas:not(:has(.bloom-canvas-element)):has( @@ -60,7 +70,11 @@ // Hopefully we don't in any way put the string "placeHolder.png" in the inline style of canvases which should not get it! // We need !important here to override the inline style attribute background-image which we normally use to put images in bloompubs .bloomPlayer-page.template - .bloom-canvas[style*="background-image"][style*="placeHolder.png"] { + .bloom-canvas:is( + [style*="background-image"][style*="placeHolder.png"], + /* BloomPlayer moves background-image URLs to data-background for lazy loading, so include that too. */ + [data-background*="placeHolder.png"] + ) { background-image: @image-placeholder !important; &[data-tool-id="canvas"] { diff --git a/src/BloomBrowserUI/react_components/icons/ImagePlaceholderIcon.tsx b/src/BloomBrowserUI/react_components/icons/ImagePlaceholderIcon.tsx index 3af45838594a..7fde956d4dcd 100644 --- a/src/BloomBrowserUI/react_components/icons/ImagePlaceholderIcon.tsx +++ b/src/BloomBrowserUI/react_components/icons/ImagePlaceholderIcon.tsx @@ -12,15 +12,40 @@ export const ImagePlaceholderIcon: React.FunctionComponent< const { color, strokeColor, ...rest } = props; return ( + + + + + @@ -34,23 +59,44 @@ export const WrongImagePlaceholderIcon: React.FunctionComponent< const { color, strokeColor, ...rest } = props; return ( + + + + + ); }; diff --git a/src/BloomExe/Book/AccessibilityCheckers.cs b/src/BloomExe/Book/AccessibilityCheckers.cs index 48d4230cd5e5..1f91538f9622 100644 --- a/src/BloomExe/Book/AccessibilityCheckers.cs +++ b/src/BloomExe/Book/AccessibilityCheckers.cs @@ -34,7 +34,7 @@ public static IEnumerable CheckDescriptionsForAllImages(Book.Book book) "The {0} is where the page number will be inserted." ); - // Note in BL-6089 we may decide to except placeholder.png from these complaints, if + // Note in BL-6089 we may decide to except placeHolder.png from these complaints, if // if we are going to trim them out of epub and bloom reader publishing. // Note that we intentionally are not dealing with unusual hypothetical situations like where diff --git a/src/BloomExe/Book/Book.cs b/src/BloomExe/Book/Book.cs index 1ac1a2025e8f..92bb22aef88f 100644 --- a/src/BloomExe/Book/Book.cs +++ b/src/BloomExe/Book/Book.cs @@ -2361,8 +2361,12 @@ internal static void RemoveImgTagInDataDiv(HtmlDom bookDom) } var imgNode = imgNodes[0]; var src = imgNode.GetAttribute("src"); - coverImageElement.InnerText = - (string.IsNullOrEmpty(src)) ? string.Empty : HttpUtility.UrlDecode(src); + // If malformed markup omits a src, treat it as the legacy placeholder marker + // so downstream code sees an intentional placeholder rather than a missing value. + // This preserves expectations in BringBookUpToDate_EmbeddedEmptyImgTagRemoved. + if (string.IsNullOrWhiteSpace(src)) + src = "placeHolder.png"; + coverImageElement.InnerText = HttpUtility.UrlDecode(src); } } @@ -5189,7 +5193,7 @@ public string GetCoverImagePathAndElt(out SafeXmlElement coverImgElt) ref coverImageFileName ); // We no longer put placeHolder.png files in books (BL-15441) but we still need to detect when the placeholder - // is called for, so here we return placeHolder.png instead of null. Callers of this method should handle this special case. + // is called for, so return coverImagePath unchanged when it is a placeholder. Callers of this method should handle this special-case value. if (ImageUtils.IsPlaceholderImageFilename(coverImagePath)) { return coverImagePath; diff --git a/src/BloomExe/web/BloomServer.cs b/src/BloomExe/web/BloomServer.cs index 94daca5322cd..b52092373422 100644 --- a/src/BloomExe/web/BloomServer.cs +++ b/src/BloomExe/web/BloomServer.cs @@ -674,6 +674,18 @@ bool IsInBookFolder(string path) return path.Replace("\\", "/").StartsWith(CurrentBook.FolderPath.Replace("\\", "/")); } + private bool TryHandlePlaceholderImageRequest(IRequestInfo info, string imageFile) + { + if (!ImageUtils.IsPlaceholderImageFilename(imageFile)) + return false; + + // We now use css to put in the placeholder images, but still use "placeHolder.png" to mark them. + // So we actually don't want to provide an image file for this placeholder marker. + // Return 204 No Content to avoid browser showing broken image icon. + info.WriteNoContent(); + return true; + } + // Handle requests for image files, that is, URLs that end in one of our image extensions. // Returns true if this is, in fact, a request for an image, in which case it will have // been handled; any reporting of problems will have been done, and a response generated. @@ -686,6 +698,9 @@ private bool ProcessImageFileRequest(IRequestInfo info) if (!IsImageTypeThatCanBeDegraded(imageFile) && !isSvg) return false; + if (TryHandlePlaceholderImageRequest(info, imageFile)) + return true; + // This can't be right. At some point it may have had something to do with // images in page thumbnails, but that is now handled by a param. // But we definitely don't want Bloom to fail to find any picture of a thumbnail! @@ -736,6 +751,9 @@ private bool ProcessImageFileRequest(IRequestInfo info) imageFile = Path.Combine(sourceDir, imageFile); + if (TryHandlePlaceholderImageRequest(info, imageFile)) + return true; + if (!RobustFileExistsWithCaseCheck(imageFile)) { // There are a few special cases where it's not desirable to change the source of the image @@ -746,13 +764,6 @@ private bool ProcessImageFileRequest(IRequestInfo info) { imageFile = imageFile.Replace("flat", "icy_orange"); } - else if (ImageUtils.IsPlaceholderImageFilename(imageFile)) - { - // We now use css to put in the placeholder images, but still use "placeHolder.png" to mark them - // So we actually don't want to provide an image file for placeHolder.png. - info.WriteCompleteOutput(""); - return true; - } // If the user does add a video or widget, these placeholder .svgs will get copied to the // book folder and used from there. But we don't copy to the book folder while the user // is still in origami in case the user doesn't actually add the video or widget. diff --git a/src/BloomExe/web/IRequestInfo.cs b/src/BloomExe/web/IRequestInfo.cs index 9025f4fadccb..5f08d86f8330 100644 --- a/src/BloomExe/web/IRequestInfo.cs +++ b/src/BloomExe/web/IRequestInfo.cs @@ -20,7 +20,7 @@ public interface IRequestInfo string RequestContentType { get; } string ResponseContentType { set; } string RawUrl { get; } - bool HaveOutput { get; } + bool HaveFullyProcessedRequest { get; } void WriteCompleteOutput(string s); void ReplyWithFileContent(string path, string originalPath = null); void ReplyWithStreamContent(Stream input, string responseType); @@ -34,6 +34,7 @@ public interface IRequestInfo string GetPostString(bool unescape = true); HttpMethods HttpMethod { get; } void ExternalLinkSucceeded(); + void WriteNoContent(); string DoNotCacheFolder { set; } byte[] GetRawPostData(); Stream GetRawPostStream(); diff --git a/src/BloomExe/web/RequestInfo.cs b/src/BloomExe/web/RequestInfo.cs index b88fa0e772aa..6c4c130c20a3 100644 --- a/src/BloomExe/web/RequestInfo.cs +++ b/src/BloomExe/web/RequestInfo.cs @@ -76,7 +76,22 @@ public RequestInfo(IHttpListenerContext context) public void ExternalLinkSucceeded() { _actualContext.Response.StatusCode = 200; //Completed - HaveOutput = true; + HaveFullyProcessedRequest = true; + } + + public void WriteNoContent() + { + try + { + _actualContext.Response.StatusCode = 204; // No Content + _actualContext.Response.ContentLength64 = 0; + _actualContext.Response.Close(); + } + catch (HttpListenerException e) + { + ReportHttpListenerProblem(e); + } + HaveFullyProcessedRequest = true; } public string DoNotCacheFolder { get; set; } @@ -105,7 +120,7 @@ private void WriteOutput(byte[] buffer, HttpListenerResponse response) { ReportHttpListenerProblem(e); } - HaveOutput = true; + HaveFullyProcessedRequest = true; } private static void ReportHttpListenerProblem(HttpListenerException e) @@ -119,7 +134,7 @@ private static void ReportHttpListenerProblem(HttpListenerException e) Debug.WriteLine(e.Message); } - public bool HaveOutput { get; private set; } + public bool HaveFullyProcessedRequest { get; private set; } public void ReplyWithFileContent(string path, string originalPath = null) { @@ -129,7 +144,7 @@ public void ReplyWithFileContent(string path, string originalPath = null) { // Earlier there was concern that we were coming here to look for .wav file existence, but that task // is now handled in the "/bloom/api/audio" endpoint. So if we get here, we're looking for a different file. - // Besides, if we don't set HaveOutput to true (w/WriteError), we'll have other problems. + // Besides, if we don't set HaveFullyProcessedRequest to true (w/WriteError), we'll have other problems. Logger.WriteError("Server could not find" + path, new FileNotFoundException()); WriteError(404, "Server could not find " + path); return; @@ -146,7 +161,7 @@ public void ReplyWithFileContent(string path, string originalPath = null) // BL-12237 actually had a FileNotFoundException here, in a Team Collection setting, which should // have been caught by the RobustFile.Exists() above. So we'll just log it and continue. // The important thing for avoiding a big ugly EndpointHandler error (in the case of BL-12237) is to - // set HaveOutput to true, which WriteError() does. + // set HaveFullyProcessedRequest to true, which WriteError() does. Logger.WriteError("Server could not read " + path, error); WriteError(500, "Server could not read " + path + ": " + error.Message); return; @@ -268,7 +283,7 @@ public void ReplyWithFileContent(string path, string originalPath = null) fs.Dispose(); } - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void ReplyWithStreamContent(Stream input, string responseType) @@ -304,7 +319,7 @@ public void ReplyWithStreamContent(Stream input, string responseType) _actualContext.Response.Close(buffer, false); } - HaveOutput = true; + HaveFullyProcessedRequest = true; } readonly HashSet _cacheableExtensions = new HashSet( @@ -386,7 +401,7 @@ public void WriteError(int errorCode, string errorDescription) if (LocalPathWithoutQuery.ToLowerInvariant().EndsWith(".json")) _actualContext.Response.ContentType = "application/json"; _actualContext.Response.Close(); - HaveOutput = true; + HaveFullyProcessedRequest = true; } private string SanitizeForAscii(string errorDescription) @@ -598,6 +613,7 @@ public void WriteRedirect(string url, bool permanent) // This supports Bloom Player Storybook's "Live from Bloom Editor" feature, preventing CORS errors on the redirect. _actualContext.Response.AppendHeader("Access-Control-Allow-Origin", "*"); _actualContext.Response.Close(); + HaveFullyProcessedRequest = true; } } } diff --git a/src/BloomExe/web/controllers/ApiRequest.cs b/src/BloomExe/web/controllers/ApiRequest.cs index 9ffb772bb71a..da5b7abcdc6a 100644 --- a/src/BloomExe/web/controllers/ApiRequest.cs +++ b/src/BloomExe/web/controllers/ApiRequest.cs @@ -272,7 +272,7 @@ await InvokeWithErrorHandling( } } } - if (!info.HaveOutput) + if (!info.HaveFullyProcessedRequest) { throw new ApplicationException( $"The EndpointHandler for {info.RawUrl} never called a Succeeded(), Failed(), or ReplyWith() Function." diff --git a/src/BloomTests/PretendRequestInfo.cs b/src/BloomTests/PretendRequestInfo.cs index f1031b23139e..35ad93125ed8 100644 --- a/src/BloomTests/PretendRequestInfo.cs +++ b/src/BloomTests/PretendRequestInfo.cs @@ -60,20 +60,20 @@ public string ReplyContentsAsXml // get is required to fulfil interface contract. Not currently used in tests. // set is required to satisfy (Team City version of) compiler for a valid auto-implemented property. - public bool HaveOutput { get; set; } + public bool HaveFullyProcessedRequest { get; set; } public void WriteCompleteOutput(string s) { var buffer = Encoding.UTF8.GetBytes(s); ReplyContents = Encoding.UTF8.GetString(buffer); - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void ReplyWithFileContent(string path, string originalPath = null) { ReplyImagePath = path; WriteCompleteOutput(RobustFile.ReadAllText(path)); - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void ReplyWithStreamContent(Stream input, string responseType) @@ -90,7 +90,7 @@ public void WriteError(int errorCode, string errorDescription) { StatusCode = errorCode; StatusDescription = errorDescription; - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void WriteError(int errorCode) @@ -127,7 +127,17 @@ public string GetPostString(bool unescape = true) return ""; } - public void ExternalLinkSucceeded() { } + public void ExternalLinkSucceeded() + { + StatusCode = 200; + HaveFullyProcessedRequest = true; + } + + public void WriteNoContent() + { + StatusCode = 204; + HaveFullyProcessedRequest = true; + } public string DoNotCacheFolder { get; set; } diff --git a/src/BloomTests/Spreadsheet/SpreadsheetImporterTests.cs b/src/BloomTests/Spreadsheet/SpreadsheetImporterTests.cs index 6abed15dd9c0..96f98308e07b 100644 --- a/src/BloomTests/Spreadsheet/SpreadsheetImporterTests.cs +++ b/src/BloomTests/Spreadsheet/SpreadsheetImporterTests.cs @@ -679,7 +679,7 @@ public static string PageWithJustImage(int pageNumber, int icNumber)
- +
diff --git a/src/content/templates/template books/Games/Games.html b/src/content/templates/template books/Games/Games.html index b3907022b185..6f189814d0b2 100644 --- a/src/content/templates/template books/Games/Games.html +++ b/src/content/templates/template books/Games/Games.html @@ -1,4 +1,4 @@ - + @@ -5178,7 +5178,7 @@ >