CCA: Enhance multi-page document scanning UI

* Store cropped blobs to speed up saving/sharing files.
* Wait for cropping when closing the review UI to avoid flickering.
* Don't close the review UI when saving files fails.

Bug: b:223089758
Test: tast run dut camera.CCAUIDocumentScanning*. Trigger error when saving files. Add delay in cropping images and close the UI by pressing ESC.
Change-Id: I5389c945cc2bfb11fffdf42e32a6aa091bd0e331
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4008139
Commit-Queue: Chu-Hsuan Yang <chuhsuan@chromium.org>
Reviewed-by: Wei Lee <wtlee@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1069568}
diff --git a/ash/webui/camera_app_ui/resources/css/mode/scan.css b/ash/webui/camera_app_ui/resources/css/mode/scan.css
index ec8324b..5fdf1d84 100644
--- a/ash/webui/camera_app_ui/resources/css/mode/scan.css
+++ b/ash/webui/camera_app_ui/resources/css/mode/scan.css
@@ -425,15 +425,15 @@
 }
 
 #view-document-review {
-  --preview-area-padding-inline-end: 22px;
-  --preview-area-padding-inline-start: 26px;
   --buttons-container-height: calc(var(--bottom-line) * 2);
+  --preview-area-padding-inline-end: calc(var(--right-line) * 2);
+  --preview-area-padding-inline-start: calc(var(--left-line) * 2);
   display: flex;
 }
 
-#view-document-review.single {
-  --preview-area-padding-inline-end: calc(var(--right-line) * 2);
-  --preview-area-padding-inline-start: calc(var(--left-line) * 2);
+#view-document-review:has(.page:nth-child(2)) {
+  --preview-area-padding-inline-end: 22px;
+  --preview-area-padding-inline-start: 26px;
 }
 
 #view-document-review .document-pages {
@@ -443,7 +443,7 @@
   padding: 16px;
 }
 
-#view-document-review.single .document-pages {
+#view-document-review:not(:has(.page:nth-child(2))) .document-pages {
   display: none;
 }
 
@@ -563,7 +563,7 @@
   justify-content: center;
 }
 
-#view-document-review:not(.single) .document-preview-mode button[i18n-text=label_save_photo_document] {
+#view-document-review:has(.page:nth-child(2)) .document-preview-mode button[i18n-text=label_save_photo_document] {
   display: none;
 }
 
diff --git a/ash/webui/camera_app_ui/resources/js/views/document_review.ts b/ash/webui/camera_app_ui/resources/js/views/document_review.ts
index 0f62e61..ea52c8a 100644
--- a/ash/webui/camera_app_ui/resources/js/views/document_review.ts
+++ b/ash/webui/camera_app_ui/resources/js/views/document_review.ts
@@ -51,6 +51,7 @@
 interface PageInternal extends Page {
   isCornersUpdated: boolean;
   isRotationUpdated: boolean;
+  croppedBlob: Blob;
 }
 
 export enum Mode {
@@ -95,7 +96,6 @@
     pages: 'document-pages',
     preview: 'document-preview',
     thumbnail: 'thumbnail',
-    single: 'single',
   } as const;
 
   private readonly pageTemplateSelector = '#document-review-page';
@@ -222,10 +222,17 @@
             mimeType === MimeType.JPEG ? DocScanResultActionType.SAVE_AS_PHOTO :
                                          DocScanResultActionType.SAVE_AS_PDF);
         nav.open(ViewName.FLASH);
-        this.save(mimeType).then(() => this.clearPages()).finally(() => {
-          this.close();
-          nav.close(ViewName.FLASH);
-        });
+        this.save(mimeType)
+            .then(() => {
+              this.clearPages();
+              this.close();
+            })
+            .catch(() => {
+              showToast(I18nString.ERROR_MSG_SAVE_FILE_FAILED);
+            })
+            .finally(() => {
+              nav.close(ViewName.FLASH);
+            });
       },
     });
     this.modes = {
@@ -239,15 +246,15 @@
    * Adds a page to `this.pages` and updates related elements.
    */
   async addPage(page: Page): Promise<void> {
+    const {blob: croppedBlob} = await this.crop(page);
     const pageInternal: PageInternal = {
       ...page,
       isCornersUpdated: false,
       isRotationUpdated: false,
+      croppedBlob,
     };
-    const croppedPage = await this.crop(pageInternal);
-    await this.addPageView(croppedPage.blob);
+    await this.addPageView(croppedBlob);
     this.pages.push(pageInternal);
-    this.root.classList.toggle(this.classes.single, this.pages.length === 1);
   }
 
   private async addPageView(blob: Blob): Promise<void> {
@@ -261,21 +268,13 @@
    * is JPEG, only saves the first page.
    */
   private async save(mimeType: MimeType.JPEG|MimeType.PDF): Promise<void> {
-    const blobs = await Promise.all(this.pages.map(async (page) => {
-      const croppedPage = await this.crop(page);
-      return croppedPage.blob;
-    }));
+    const blobs = this.pages.map((page) => page.croppedBlob);
     const name = (new Filenamer()).newDocumentName(mimeType);
-    try {
-      if (mimeType === MimeType.JPEG) {
-        await this.resultSaver.savePhoto(blobs[0], name, null);
-      } else {
-        const pdfBlob = await ChromeHelper.getInstance().convertToPdf(blobs);
-        await this.resultSaver.savePhoto(pdfBlob, name, null);
-      }
-    } catch (e) {
-      showToast(I18nString.ERROR_MSG_SAVE_FILE_FAILED);
-      throw e;
+    if (mimeType === MimeType.JPEG) {
+      await this.resultSaver.savePhoto(blobs[0], name, null);
+    } else {
+      const pdfBlob = await ChromeHelper.getInstance().convertToPdf(blobs);
+      await this.resultSaver.savePhoto(pdfBlob, name, null);
     }
   }
 
@@ -284,10 +283,7 @@
    * share the first page.
    */
   private async share(mimeType: MimeType.JPEG|MimeType.PDF): Promise<void> {
-    const blobs = await Promise.all(this.pages.map(async (page) => {
-      const croppedPage = await this.crop(page);
-      return croppedPage.blob;
-    }));
+    const blobs = this.pages.map((page) => page.croppedBlob);
     const name = (new Filenamer()).newDocumentName(mimeType);
     const blob = mimeType === MimeType.JPEG ?
         blobs[0] :
@@ -396,10 +392,10 @@
 
   private async updatePageInternal(index: number, page: PageInternal):
       Promise<void> {
-    const croppedPage = await this.crop(page);
+    const {blob: croppedBlob} = await this.crop(page);
     const pageElement = this.pagesElement.children[index];
-    await this.updatePageView(pageElement, croppedPage.blob);
-    this.pages[index] = page;
+    await this.updatePageView(pageElement, croppedBlob);
+    this.pages[index] = {...page, croppedBlob};
   }
 
   private async updatePageView(pageElement: ParentNode, blob: Blob):
@@ -431,7 +427,6 @@
     await this.selectPage(
         this.selectedIndex === this.pages.length ? this.pages.length - 1 :
                                                    this.selectedIndex);
-    this.root.classList.toggle(this.classes.single, this.pages.length === 1);
   }
 
   private deletePageView(index: number): void {
@@ -482,7 +477,7 @@
     this.pagesElement.replaceChildren();
   }
 
-  private async crop(page: PageInternal): Promise<PageInternal> {
+  private async crop(page: Page): Promise<Page> {
     const {blob, corners, rotation} = page;
     const newBlob = await ChromeHelper.getInstance().convertToDocument(
         blob, corners, rotation, MimeType.JPEG);
@@ -512,6 +507,7 @@
   protected override leaving(): boolean {
     this.hideMultiPageAvailableIndicator?.();
     this.hideMultiPageAvailableIndicator = null;
+    this.waitForUpdatingPage();
     if (this.pages.length === 0) {
       this.fixCount = 0;
     }