From e62e9a26fb6a355d2166e5ea701e820408920b44 Mon Sep 17 00:00:00 2001 From: yayunhuang Date: Wed, 21 Aug 2024 11:18:49 +0800 Subject: [PATCH 1/7] Fix multiple color options selected for iphone 13 pro --- src/scripts/device_info.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/device_info.json b/src/scripts/device_info.json index a24e65d..1896291 100755 --- a/src/scripts/device_info.json +++ b/src/scripts/device_info.json @@ -2156,9 +2156,9 @@ }, { "credits": "

Meta - Design Resources

", - "color_str": "Gold", + "color_str": "Silver", "color": { - "id": "gold", + "id": "silver", "name": "Silver", "hexcode": "#F2F5F4" }, From 5e9f9a66c5ee35f20f15a1ac6caa5c378bbe758c Mon Sep 17 00:00:00 2001 From: yayunhuang Date: Tue, 20 Aug 2024 12:52:46 +0800 Subject: [PATCH 2/7] Fix generate incorrect image when upload multiple images --- public/image_process.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/image_process.py b/public/image_process.py index 62b432e..f3f6782 100644 --- a/public/image_process.py +++ b/public/image_process.py @@ -2,7 +2,7 @@ import io import os -from js import Uint8Array, imageUploadList, imageUpload +from js import Uint8Array, imageUploadList from PIL import Image @@ -29,6 +29,11 @@ async def upload_single_image(origin_image, file_name): async def upload_file(): + from js import imageUpload + + # Since we will update `imageUpload` when calling this function, + # need to re-import it to force update to new value + # or we will always generate first uploaded image basename, ext = os.path.splitext(imageUpload.name) if ext.lower() not in [".psd", ".jpg", ".jpeg", ".png"]: return From ef6cf2c4457d6d319bfcdbd3a841c05cb28c7cc1 Mon Sep 17 00:00:00 2001 From: yayunhuang Date: Tue, 20 Aug 2024 12:54:23 +0800 Subject: [PATCH 3/7] Add loading state in image preview selection --- public/images/spinner-blue-2.png | Bin 0 -> 949 bytes public/scripts/models/image-upload.js | 46 ++++++---- public/scripts/upload.js | 123 ++++++++++++++++++++------ src/styles/upload.css | 12 +++ 4 files changed, 136 insertions(+), 45 deletions(-) create mode 100644 public/images/spinner-blue-2.png diff --git a/public/images/spinner-blue-2.png b/public/images/spinner-blue-2.png new file mode 100644 index 0000000000000000000000000000000000000000..0ee2e96ed22d2724009fc9d729608748ceb89bf6 GIT binary patch literal 949 zcmV;m14{gfP)CmK~#7Fm6T0r zT}2ef&zUnb_r8~yHXl!4C^0V!U5PE2g&;yB3R=jrqMJ$t#ezsT1woL2P(=h6Qe3zw zQtCn%#i|&H65Nz7>er&^LP*k@Ce3Tpm$u34o3GPzAGEajX#4&!cP=w$e)B(b&s>E2 z=(Ud*Yno^%N7bhd$4uM0BLDyA2L~3mTZ3B-j(@Q9y0Q3*CA3_=kkbm6 zwf56T-mLs_U*NvO%hjm(Ls>X1ASWPa4SGY^vKCxM;EjxD1(Y~wg3C7Nuit;QZ*>#! z`9pJqMA=uedtSII++;>w%{87j2A48PIoGOcEmo{nU540`#AQTRz^$D;e4x5~2l&j} z^+99(QB7DOg~1qT*o?mGZ^rMBe^y=FnAYU6)o011=fo)yLeLtnVt(nH_x3%}(nUZ* z_fAMS#0-&jx`eFX{pF*|5^ST%i5+J?`F7X_*w1z&|=BQ&YJ6z=d7O;*>ax zN}46})|sQ54|^+p`NR4y@i1->5|D~I=buy)=nWT8I}gT%^z*~Q3LP9TFO;-8uZUe< zR6vYFDkOd?L>3rZ=jar4DEU~wXPI)6wns&M%7^r{xU(&3DcXObLp`~(YsDfvGRozI zf)3E{VzMqm_TP#AYtW%aN70vZ)hY{6TL5v$P3Od+OIh#!l^xKbW@cDx%g#`cnpEor zaM4FFlCo*8-q;PDYG*}iWBX>~Fb4b+UYI?!6-ahf^*TQ?I0Ds@1l!!YylmEc zQhu?$`cC9avwP%HpOk?bw { console.warn("onabort"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.onerror = () => { console.warn("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.readAsDataURL(this.file); }); @@ -159,39 +165,45 @@ class ImageUpload { }; img.onerror = () => { console.warn("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; img.onabort = () => { console.warn("onabort"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; img.src = fileReader.result; }; fileReader.onabort = () => { - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.onerror = () => { console.log("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.readAsDataURL(this.file); }); } + get isGeneratingPreviewState() { + return this.state === ImageUploadState.GeneratingPreview; + } + get isProcessingState() { return !this.isProcessedState; } get isSuccessState() { - return this.readState === ReadState.ReadSuccess; + return this.state === ImageUploadState.ReadSuccess; } get isProcessedState() { - return this.isErrorState || this.isSuccessState; + return ( + this.isErrorState || this.isSuccessState || this.isGeneratingPreviewState + ); } get isErrorState() { - return this.readState.startsWith("Err"); + return this.state.startsWith("Err"); } get imageFile() { diff --git a/public/scripts/upload.js b/public/scripts/upload.js index 4006909..77d0e72 100644 --- a/public/scripts/upload.js +++ b/public/scripts/upload.js @@ -46,6 +46,13 @@ async function runWorker(worker) { } function runPreviewWorker(worker, imageUpload) { + if (imageUpload.isErrorState) { + return; + } + window.viewModel.fileList.updateImageUploadStateByULID( + imageUpload.ulid, + ImageUploadState.GeneratingPreview, + ); const imageUploadFile = imageUpload.file; worker.postMessage({ imageUpload: imageUploadFile, @@ -77,6 +84,11 @@ function runPreviewWorker(worker, imageUpload) { imageUploadHint.style.background = "transparent"; }); } + + window.viewModel.fileList.updateImageUploadStateByULID( + imageUpload.ulid, + ImageUploadState.ReadSuccess, + ); }) .catch(function (err) { console.error("Get error while storing images to localforage:", err); @@ -136,13 +148,22 @@ class FileListViewModel { } } - async remove(filename, index) { - this._imageUploads = this._imageUploads.filter((upload, i) => { + async remove(filename, fileUlid) { + this._imageUploads = this._imageUploads.filter((upload) => { const isSameFilename = upload.file.name === filename; - const isSameIndex = i === index; + const isSameIndex = fileUlid === upload.ulid; return !(isSameFilename && isSameIndex); }); } + + updateImageUploadStateByULID(ulid, state) { + this._imageUploads.forEach((imageUpload) => { + if (imageUpload.ulid == ulid) { + imageUpload.state = state; + } + return imageUpload; + }); + } } class RootViewModel { @@ -249,23 +270,23 @@ function dismissUploading() { uploading?.classList.add("d-none"); } -function findFileListItem(fileIndex) { +function findFileListItem(fileUlid) { const fileListNode = document.querySelector(".file-list"); const itemNodes = fileListNode.querySelectorAll(".file-list-item"); for (const itemNode of itemNodes) { - if (itemNode.dataset.fileIndex === String(fileIndex)) { + if (itemNode.dataset.fileUlid === String(fileUlid)) { return itemNode; } } return null; } -function appendInitialFileListItem(fileIndex, filename) { +function appendInitialFileListItem(fileUlid, filename) { const fileListNode = document.querySelector(".file-list"); const itemNode = document.createElement("li"); itemNode.classList.add("file-list-item"); - itemNode.dataset.fileIndex = fileIndex; + itemNode.dataset.fileUlid = fileUlid; const headerNode = document.createElement("div"); headerNode.classList.add("file-list-item__filename"); @@ -277,7 +298,7 @@ function appendInitialFileListItem(fileIndex, filename) { const crossNode = document.createElement("button"); crossNode.classList.add("file-list-item__cross"); crossNode.onclick = async () => { - await window.viewModel.fileList.remove(filename, fileIndex); + await window.viewModel.fileList.remove(filename, fileUlid); }; headerNode.appendChild(crossNode); @@ -352,7 +373,7 @@ function updateFileListItem(itemNode, imageUpload) { isSameAspectRatio(imageDim, recommendDim) || isSameAspectRatio(imageDimRotate, recommendDim); const shouldShowAspectRatioWarning = - imageUpload.readState !== ReadState.Reading && !isCorrectDim; + imageUpload.state !== ImageUploadState.Reading && !isCorrectDim; // Update status icon // error status has higher precedence over warning if (imageUpload.isErrorState) { @@ -361,8 +382,17 @@ function updateFileListItem(itemNode, imageUpload) { } else if (shouldShowAspectRatioWarning) { itemNode.classList.add("file-list-item--warning"); } + + if (imageUpload.isGeneratingPreviewState) { + setTimeout(() => { + itemNode.classList.remove("file-list-item--done"); + itemNode.classList.add("file-list-item--loading"); + }, 10); + } + if (imageUpload.isSuccessState) { setTimeout(() => { + itemNode.classList.remove("file-list-item--loading"); itemNode.classList.remove("file-list-item--progress"); }, 10); itemNode.classList.add("file-list-item--done"); @@ -371,14 +401,14 @@ function updateFileListItem(itemNode, imageUpload) { } // update hint text if (imageUpload.isErrorState) { - switch (imageUpload.readState) { - case ReadState.ErrUnsupportedFileType: + switch (imageUpload.state) { + case ImageUploadState.ErrUnsupportedFileType: hintNode.innerText = "Supported file extensions: JPG, PNG or PSD."; break; - case ReadState.ErrExceedMaxFileSize: + case ImageUploadState.ErrExceedMaxFileSize: hintNode.innerText = `File size should be less than ${MAX_FILE_SIZE_READABLE}.`; break; - case ReadState.ErrRead: + case ImageUploadState.ErrRead: default: hintNode.innerText = "Something went wrong. Please try upload again or refresh the page."; @@ -388,16 +418,21 @@ function updateFileListItem(itemNode, imageUpload) { hintNode.innerText = `Uploaded file dimension (${imageDim.width} × ${imageDim.height} pixels) differs from ideal (${recommendDim.width} × ${recommendDim.height} pixels).`; } // update progress bar - if (imageUpload.isProcessingState || imageUpload.isSuccessState) { + if ( + imageUpload.isProcessingState || + imageUpload.isSuccessState || + imageUpload.isGeneratingMockupState + ) { progressFillNode.classList.add("file-list-item__progress-bar-fill--30"); - switch (imageUpload.readState) { - case ReadState.ReadyForRead: + switch (imageUpload.state) { + case ImageUploadState.ReadyForRead: progressFillNode.classList.add("file-list-item__progress-bar-fill--60"); break; - case ReadState.Reading: + case ImageUploadState.Reading: progressFillNode.classList.add("file-list-item__progress-bar-fill--90"); break; - case ReadState.ReadSuccess: + case ImageUploadState.ReadSuccess: + case ImageUploadState.GeneratingPreview: progressFillNode.classList.add( "file-list-item__progress-bar-fill--100", ); @@ -616,18 +651,49 @@ function main() { } }); - // observe fileListViewModel: imageUploads[].readState + // observe fileListViewModel: imageUploads[].state + mobx.reaction( + () => + viewModel.fileList.imageUploads.map((imageUpload) => imageUpload.state), + async () => { + const imageUploads = viewModel.fileList.imageUploads; + for (let i = 0; i < imageUploads.length; ++i) { + let itemNode = findFileListItem(imageUploads[i].ulid); + if (itemNode == null) { + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); + } + updateFileListItem(itemNode, imageUploads[i]); + } + + // scroll to upload element on mobile devices + if (window.innerWidth <= 992) { + const HEADER_HEIGHT = 80; + scrollToElementTop(uploadSection, HEADER_HEIGHT); + } + }, + { + equals: mobx.comparer.shallow, + }, + ); + + // observe fileListViewModel: imageUploads[].previewState mobx.reaction( () => viewModel.fileList.imageUploads.map( - (imageUpload) => imageUpload.readState, + (imageUpload) => imageUpload.previewState, ), async () => { const imageUploads = viewModel.fileList.imageUploads; for (let i = 0; i < imageUploads.length; ++i) { - let itemNode = findFileListItem(i); + let itemNode = findFileListItem(imageUploads[i].ulid); if (itemNode == null) { - itemNode = appendInitialFileListItem(i, imageUploads[i].file.name); + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); } updateFileListItem(itemNode, imageUploads[i]); } @@ -650,9 +716,12 @@ function main() { removeAllFileListItems(); // remove then re-render const imageUploads = viewModel.fileList.imageUploads; for (let i = 0; i < imageUploads.length; ++i) { - let itemNode = findFileListItem(i); + let itemNode = findFileListItem(imageUploads[i].ulid); if (itemNode == null) { - itemNode = appendInitialFileListItem(i, imageUploads[i].file.name); + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); } updateFileListItem(itemNode, imageUploads[i]); } @@ -663,14 +732,12 @@ function main() { ); if (isDebug) { - // observe fileListViewModel: imageUploads, imageUploads[].readState + // observe fileListViewModel: imageUploads, imageUploads[].state mobx.autorun(() => { console.log("file list:", mobx.toJS(viewModel.fileList.imageUploads)); console.log( "read states:", - viewModel.fileList.imageUploads.map( - (imageUpload) => imageUpload.readState, - ), + viewModel.fileList.imageUploads.map((imageUpload) => imageUpload.state), ); }); } diff --git a/src/styles/upload.css b/src/styles/upload.css index 71be1f2..0b27aea 100644 --- a/src/styles/upload.css +++ b/src/styles/upload.css @@ -409,6 +409,18 @@ main { background-image: url("/images/upload-error.svg"); } +.file-list-item--loading .file-list-item__filename::before { + content: ""; + display: block; + width: 20px; + height: 20px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + background-image: url("/images/spinner-blue-2.png"); + animation: spin 2s linear infinite; +} + .file-list-item__hint { margin: 5px 0 0 0; font-size: 12px; From 498b0e98144579f8618b832ed994a4bbab2abd3e Mon Sep 17 00:00:00 2001 From: yayunhuang Date: Tue, 20 Aug 2024 16:20:25 +0800 Subject: [PATCH 4/7] Implement preview image selection --- public/images/preview-gray.svg | 3 + public/images/preview-white.svg | 3 + public/scripts/models/image-upload.js | 4 + public/scripts/preview_worker.js | 2 +- public/scripts/upload.js | 152 +++++++++++++++++++++----- src/styles/upload.css | 48 ++++++++ 6 files changed, 183 insertions(+), 29 deletions(-) create mode 100644 public/images/preview-gray.svg create mode 100644 public/images/preview-white.svg diff --git a/public/images/preview-gray.svg b/public/images/preview-gray.svg new file mode 100644 index 0000000..673dfa4 --- /dev/null +++ b/public/images/preview-gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/preview-white.svg b/public/images/preview-white.svg new file mode 100644 index 0000000..b857f64 --- /dev/null +++ b/public/images/preview-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/scripts/models/image-upload.js b/public/scripts/models/image-upload.js index 3957848..a8d85ed 100644 --- a/public/scripts/models/image-upload.js +++ b/public/scripts/models/image-upload.js @@ -67,6 +67,10 @@ class ImageUpload { this.state = state; } + updatePreviewUrl(previewUrl) { + this.previewUrl = previewUrl; + } + // Cache file type from header such that no need to parse again __fileTypeFromHeader = null; async _getFileTypeFromHeader() { diff --git a/public/scripts/preview_worker.js b/public/scripts/preview_worker.js index 37b9c9f..b3c4e7a 100644 --- a/public/scripts/preview_worker.js +++ b/public/scripts/preview_worker.js @@ -59,7 +59,7 @@ async function main() { // TODO: Handle preview loading state in widget let results = await runPreviewMockup(pyodideObject); console.log("preview results", results); - self.postMessage(results); + self.postMessage({ ulid: event.data.ulid, results: results }); } catch (error) { self.postMessage({ error: error.message }); } diff --git a/public/scripts/upload.js b/public/scripts/upload.js index 77d0e72..7f64a5a 100644 --- a/public/scripts/upload.js +++ b/public/scripts/upload.js @@ -59,12 +59,13 @@ function runPreviewWorker(worker, imageUpload) { location: window.location.toString(), deviceId: window.workerDeviceId, deviceInfo: window.deviceInfo, + ulid: imageUpload.ulid, }); worker.addEventListener( "message", function (e) { window.localforage - .setItem(`previewImage-${imageUpload.ulid}`, e.data[1]) + .setItem(`previewImage-${e.data["ulid"]}`, e.data["results"][1]) .then(function () { const imageContainer = document.querySelector( ".upload__device-image-rect", @@ -72,21 +73,21 @@ function runPreviewWorker(worker, imageUpload) { /* Put first generated mockup to preview area */ if (!imageContainer.style.backgroundImage) { - imageContainer.style.backgroundImage = `url(${e.data[1]})`; - imageContainer.style.backgroundSize = "cover"; - imageContainer.style.backgroundPosition = "center"; + imageContainer.style.backgroundImage = `url(${e.data["results"][1]})`; const imageUploadHints = document.querySelectorAll( ".upload__device-hint", ); imageUploadHints.forEach((imageUploadHint) => { - imageUploadHint.innerHTML = ""; - imageUploadHint.style.background = "transparent"; + imageUploadHint.style.display = "none"; }); } - + window.viewModel.fileList.updateImageUploadPreviewUrlByULID( + e.data["ulid"], + e.data["results"][1], + ); window.viewModel.fileList.updateImageUploadStateByULID( - imageUpload.ulid, + e.data["ulid"], ImageUploadState.ReadSuccess, ); }) @@ -139,31 +140,50 @@ class FileListViewModel { files = [file]; } - for (const file of files) { - const imageUpload = new ImageUpload(file, MAX_FILE_SIZE_BYTE); + for (let i = 0; i < files.length; i += 1) { + const imageUpload = new ImageUpload(files[i], MAX_FILE_SIZE_BYTE); await imageUpload.read(); imageUpload.ulid = ULID.ulid(); - this._imageUploads.push(imageUpload); - window.viewModel.generatePreviewMockup(imageUpload); + + // Avoiding read same image file + setTimeout(() => { + this._imageUploads.push(imageUpload); + window.viewModel.generatePreviewMockup(imageUpload); + }, i * 10); } + + window.viewModel.selectedPreviewImageULID = + window.viewModel.defaultImageUploadULID; } async remove(filename, fileUlid) { this._imageUploads = this._imageUploads.filter((upload) => { const isSameFilename = upload.file.name === filename; - const isSameIndex = fileUlid === upload.ulid; - return !(isSameFilename && isSameIndex); + const isSameULID = fileUlid === upload.ulid; + return !(isSameFilename && isSameULID); }); + + window.viewModel.selectedPreviewImageULID = + window.viewModel.defaultImageUploadULID; } updateImageUploadStateByULID(ulid, state) { - this._imageUploads.forEach((imageUpload) => { + this._imageUploads = this._imageUploads.map((imageUpload) => { if (imageUpload.ulid == ulid) { imageUpload.state = state; } return imageUpload; }); } + + updateImageUploadPreviewUrlByULID(ulid, previewUrl) { + this._imageUploads = this._imageUploads.map((imageUpload) => { + if (imageUpload.ulid == ulid) { + imageUpload.previewUrl = previewUrl; + } + return imageUpload; + }); + } } class RootViewModel { @@ -176,6 +196,7 @@ class RootViewModel { worker = new Worker("/scripts/web_worker.js"); previewWorker = new Worker("/scripts/preview_worker.js"); selectedColorId = null; + selectedPreviewImageULID = null; constructor(maxMockupWaitSec, fileListViewModel, selectedColorId) { mobx.makeObservable(this, { @@ -185,6 +206,7 @@ class RootViewModel { isGeneratingMockup: mobx.computed, generateMockup: mobx.action, cancelMockup: mobx.action, + selectedPreviewImageULID: mobx.observable, }); this.selectedColorId = selectedColorId; this.maxMockupWaitSec = maxMockupWaitSec; @@ -239,6 +261,12 @@ class RootViewModel { get previewUrl() { return "/download/?deviceId=" + window.workerDeviceId; } + + get defaultImageUploadULID() { + return this.fileList.imageUploads.length > 0 + ? this.fileList.imageUploads[0].ulid + : null; + } } function preventDefault(node, events) { @@ -285,6 +313,19 @@ function appendInitialFileListItem(fileUlid, filename) { const fileListNode = document.querySelector(".file-list"); const itemNode = document.createElement("li"); + + const fileInfoNode = document.createElement("div"); + const previewStateNode = document.createElement("div"); + previewStateNode.addEventListener("click", () => { + window.viewModel.selectedPreviewImageULID = fileUlid; + }); + + previewStateNode.classList.add("file-list-item__preview-state"); + itemNode.appendChild(previewStateNode); + + fileInfoNode.classList.add("file-list-item__file-info"); + itemNode.appendChild(fileInfoNode); + itemNode.classList.add("file-list-item"); itemNode.dataset.fileUlid = fileUlid; @@ -302,14 +343,13 @@ function appendInitialFileListItem(fileUlid, filename) { }; headerNode.appendChild(crossNode); - itemNode.appendChild(headerNode); - - itemNode.insertAdjacentHTML( + fileInfoNode.appendChild(headerNode); + fileInfoNode.insertAdjacentHTML( "beforeend", `

`, ); - itemNode.insertAdjacentHTML( + fileInfoNode.insertAdjacentHTML( "beforeend", `
@@ -329,12 +369,14 @@ function updateFileListItem(itemNode, imageUpload) { const progressFillNode = itemNode.querySelector( ".file-list-item__progress-bar-fill", ); + const previewNode = itemNode.querySelector(".file-list-item__preview-state"); // clear previous state itemNode.classList.remove( "file-list-item--done", "file-list-item--error", "file-list-item--warning", + "file-list-item__previewable", // NOTE: do not remove progress state immediately so the progress bar can proceed to 100% before being removed // "file-list-item--progress" ); @@ -344,6 +386,10 @@ function updateFileListItem(itemNode, imageUpload) { "file-list-item__progress-bar-fill--90", "file-list-item__progress-bar-fill--100", ); + previewNode.classList.remove( + "file-list-item__preview_selected", + "file-list-item__preview_non_selected", + ); /* Expected UI for each state | | error (correct ratio) | error (wrong ratio) | processing (correct ratio) | processing (wrong ratio) | done (correct ratio) | done (wrong ratio) | @@ -384,18 +430,19 @@ function updateFileListItem(itemNode, imageUpload) { } if (imageUpload.isGeneratingPreviewState) { - setTimeout(() => { - itemNode.classList.remove("file-list-item--done"); - itemNode.classList.add("file-list-item--loading"); - }, 10); + itemNode.classList.remove("file-list-item--done"); + itemNode.classList.add("file-list-item--loading"); } if (imageUpload.isSuccessState) { - setTimeout(() => { - itemNode.classList.remove("file-list-item--loading"); - itemNode.classList.remove("file-list-item--progress"); - }, 10); - itemNode.classList.add("file-list-item--done"); + itemNode.classList.remove( + "file-list-item--loading", + "file-list-item--progress", + ); + itemNode.classList.add( + "file-list-item--done", + "file-list-item__previewable", + ); } else if (imageUpload.isProcessingState) { itemNode.classList.add("file-list-item--progress"); } @@ -442,6 +489,15 @@ function updateFileListItem(itemNode, imageUpload) { } } + // update preview button + if (imageUpload.isSuccessState) { + if (window.viewModel.selectedPreviewImageULID == imageUpload.ulid) { + previewNode.classList.add("file-list-item__preview_selected"); + } else { + previewNode.classList.add("file-list-item__preview_non_selected"); + } + } + if (imageUpload.isSuccessState && !shouldShowAspectRatioWarning) { hintNode.classList.add("d-none"); } else { @@ -731,6 +787,46 @@ function main() { }, ); + // observe viewModel: selectedPreviewImageULID + mobx.reaction( + () => viewModel.selectedPreviewImageULID, + () => { + // update preview area + const imageContainer = document.querySelector( + ".upload__device-image-rect", + ); + if (viewModel.selectedPreviewImageULID === null) { + imageContainer.style.backgroundImage = ""; + const imageUploadHints = document.querySelectorAll( + ".upload__device-hint", + ); + imageUploadHints.forEach((imageUploadHint) => { + imageUploadHint.style.display = "flex"; + }); + } + + removeAllFileListItems(); // remove then re-render + const imageUploads = viewModel.fileList.imageUploads; + for (let i = 0; i < imageUploads.length; ++i) { + let itemNode = findFileListItem(imageUploads[i].ulid); + if (itemNode == null) { + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); + } + updateFileListItem(itemNode, imageUploads[i]); + + if ( + imageUploads[i].isSuccessState && + imageUploads[i].ulid == window.viewModel.selectedPreviewImageULID + ) { + imageContainer.style.backgroundImage = `url(${imageUploads[i].previewUrl})`; + } + } + }, + ); + if (isDebug) { // observe fileListViewModel: imageUploads, imageUploads[].state mobx.autorun(() => { diff --git a/src/styles/upload.css b/src/styles/upload.css index 0b27aea..bc61229 100644 --- a/src/styles/upload.css +++ b/src/styles/upload.css @@ -153,6 +153,11 @@ main { z-index: 1; } +.upload__device-image-rect { + background-size: cover; + background-position: center; +} + .upload__device-image-rect-wrapper { position: absolute; top: 0; @@ -341,6 +346,48 @@ main { } .file-list-item { + display: flex; + + &.file-list-item__previewable { + gap: 4px; + + .file-list-item__file-info { + max-width: calc(100% - 28px); + } + + .file-list-item__preview-state { + cursor: pointer; + } + } +} + +.file-list-item_left { + width: 0; +} + +.file-list-item__preview_selected, +.file-list-item__preview_non_selected { + margin: 8px 0 0 0; + padding-inline: 6px; + width: 24px; + display: flex; + border-radius: 5px; + background-size: 12px 8px; + background-repeat: no-repeat; + background-position: center; +} + +.file-list-item__preview_selected { + background-image: url("/images/preview-white.svg"); + background-color: rgba(0, 67, 224, 1); +} + +.file-list-item__preview_non_selected { + background-image: url("/images/preview-gray.svg"); + background-color: white; +} + +.file-list-item__file-info { margin: 8px 0 0 0; border: 1px solid var(--gray-5); border-radius: 10px; @@ -349,6 +396,7 @@ main { display: flex; flex-direction: column; justify-content: center; + flex: 1; } .file-list-item__filename { From 499e2b04aec8edf7269b9c85878ef3f5525a4a59 Mon Sep 17 00:00:00 2001 From: yayunhuang Date: Tue, 20 Aug 2024 16:44:35 +0800 Subject: [PATCH 5/7] Handle error state if preview failed --- public/scripts/preview_worker.js | 2 +- public/scripts/upload.js | 74 +++++++++++++++++++------------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/public/scripts/preview_worker.js b/public/scripts/preview_worker.js index b3c4e7a..9c9cc77 100644 --- a/public/scripts/preview_worker.js +++ b/public/scripts/preview_worker.js @@ -61,7 +61,7 @@ async function main() { console.log("preview results", results); self.postMessage({ ulid: event.data.ulid, results: results }); } catch (error) { - self.postMessage({ error: error.message }); + self.postMessage({ ulid: event.data.ulid, error: error.message }); } }; } diff --git a/public/scripts/upload.js b/public/scripts/upload.js index 7f64a5a..8efac3b 100644 --- a/public/scripts/upload.js +++ b/public/scripts/upload.js @@ -37,7 +37,6 @@ async function runWorker(worker) { window.location.href = "/download/?deviceId=" + window.workerDeviceId; }) .catch(function (err) { - // TODO: Handle preview error in widget console.error("Get error while storing images to localforage:", err); }); }, @@ -64,36 +63,44 @@ function runPreviewWorker(worker, imageUpload) { worker.addEventListener( "message", function (e) { - window.localforage - .setItem(`previewImage-${e.data["ulid"]}`, e.data["results"][1]) - .then(function () { - const imageContainer = document.querySelector( - ".upload__device-image-rect", - ); + if (e.data["error"] !== undefined) { + console.log( + "Get error while generating preview image", + e.data["error"], + ); + window.viewModel.fileList.updateImageUploadStateByULID( + e.data["ulid"], + ImageUploadState.ErrPreview, + ); + return; + } - /* Put first generated mockup to preview area */ - if (!imageContainer.style.backgroundImage) { - imageContainer.style.backgroundImage = `url(${e.data["results"][1]})`; - - const imageUploadHints = document.querySelectorAll( - ".upload__device-hint", - ); - imageUploadHints.forEach((imageUploadHint) => { - imageUploadHint.style.display = "none"; - }); - } - window.viewModel.fileList.updateImageUploadPreviewUrlByULID( - e.data["ulid"], - e.data["results"][1], - ); - window.viewModel.fileList.updateImageUploadStateByULID( - e.data["ulid"], - ImageUploadState.ReadSuccess, - ); - }) - .catch(function (err) { - console.error("Get error while storing images to localforage:", err); + const ulid = e.data["ulid"]; + const [_, previewUrl] = e.data["results"]; + + const imageContainer = document.querySelector( + ".upload__device-image-rect", + ); + + /* Put first generated mockup to preview area */ + if (!imageContainer.style.backgroundImage) { + imageContainer.style.backgroundImage = `url(${previewUrl})`; + + const imageUploadHints = document.querySelectorAll( + ".upload__device-hint", + ); + imageUploadHints.forEach((imageUploadHint) => { + imageUploadHint.style.display = "none"; }); + } + window.viewModel.fileList.updateImageUploadPreviewUrlByULID( + ulid, + previewUrl, + ); + window.viewModel.fileList.updateImageUploadStateByULID( + ulid, + ImageUploadState.ReadSuccess, + ); }, false, ); @@ -423,7 +430,10 @@ function updateFileListItem(itemNode, imageUpload) { // Update status icon // error status has higher precedence over warning if (imageUpload.isErrorState) { - itemNode.classList.remove("file-list-item--progress"); + itemNode.classList.remove( + "file-list-item--progress", + "file-list-item--loading", + ); itemNode.classList.add("file-list-item--error"); } else if (shouldShowAspectRatioWarning) { itemNode.classList.add("file-list-item--warning"); @@ -455,6 +465,10 @@ function updateFileListItem(itemNode, imageUpload) { case ImageUploadState.ErrExceedMaxFileSize: hintNode.innerText = `File size should be less than ${MAX_FILE_SIZE_READABLE}.`; break; + case ImageUploadState.ErrPreview: + hintNode.innerText = + "Preview failed. Please upload the image again to retry."; + break; case ImageUploadState.ErrRead: default: hintNode.innerText = From 5f75f03a1a9ba2d39edf29231a8a42e05b96c3b6 Mon Sep 17 00:00:00 2001 From: yayunhuang Date: Tue, 20 Aug 2024 17:10:32 +0800 Subject: [PATCH 6/7] Handle generation failed --- public/scripts/upload.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/scripts/upload.js b/public/scripts/upload.js index 8efac3b..8cde416 100644 --- a/public/scripts/upload.js +++ b/public/scripts/upload.js @@ -34,6 +34,18 @@ async function runWorker(worker) { window.localforage .setItem("pictureArray", e.data) .then(function (pictureArray) { + if (e.data["error"] !== undefined) { + console.log("Get error while generating mockup", e.data["error"]); + window.viewModel.cancelMockup(); + + // Alert after `cancelMockup` finish + setTimeout(() => { + alert( + "Oops, something went wrong. Please try a different image/device.\nIf it persists, we'd appreciate if you report it on our GitHub 🙏 https://github.com/oursky/mockuphone.com/issues.", + ); + }, 100); + return; + } window.location.href = "/download/?deviceId=" + window.workerDeviceId; }) .catch(function (err) { From 9099b4bd0486601defd802792db9c2263e057160 Mon Sep 17 00:00:00 2001 From: yayunhuang Date: Wed, 21 Aug 2024 16:54:06 +0800 Subject: [PATCH 7/7] Remove progress bar related script --- public/scripts/upload.js | 40 ---------------------------------------- src/styles/upload.css | 32 -------------------------------- 2 files changed, 72 deletions(-) diff --git a/public/scripts/upload.js b/public/scripts/upload.js index 8cde416..28e47a5 100644 --- a/public/scripts/upload.js +++ b/public/scripts/upload.js @@ -368,13 +368,6 @@ function appendInitialFileListItem(fileUlid, filename) { `

`, ); - fileInfoNode.insertAdjacentHTML( - "beforeend", - `
-
-
`, - ); - return fileListNode.appendChild(itemNode); } @@ -385,9 +378,6 @@ function removeAllFileListItems() { function updateFileListItem(itemNode, imageUpload) { const hintNode = itemNode.querySelector(".file-list-item__hint"); - const progressFillNode = itemNode.querySelector( - ".file-list-item__progress-bar-fill", - ); const previewNode = itemNode.querySelector(".file-list-item__preview-state"); // clear previous state @@ -399,12 +389,6 @@ function updateFileListItem(itemNode, imageUpload) { // NOTE: do not remove progress state immediately so the progress bar can proceed to 100% before being removed // "file-list-item--progress" ); - progressFillNode.classList.remove( - "file-list-item__progress-bar-fill--30", - "file-list-item__progress-bar-fill--60", - "file-list-item__progress-bar-fill--90", - "file-list-item__progress-bar-fill--100", - ); previewNode.classList.remove( "file-list-item__preview_selected", "file-list-item__preview_non_selected", @@ -490,30 +474,6 @@ function updateFileListItem(itemNode, imageUpload) { } else if (shouldShowAspectRatioWarning) { hintNode.innerText = `Uploaded file dimension (${imageDim.width} × ${imageDim.height} pixels) differs from ideal (${recommendDim.width} × ${recommendDim.height} pixels).`; } - // update progress bar - if ( - imageUpload.isProcessingState || - imageUpload.isSuccessState || - imageUpload.isGeneratingMockupState - ) { - progressFillNode.classList.add("file-list-item__progress-bar-fill--30"); - switch (imageUpload.state) { - case ImageUploadState.ReadyForRead: - progressFillNode.classList.add("file-list-item__progress-bar-fill--60"); - break; - case ImageUploadState.Reading: - progressFillNode.classList.add("file-list-item__progress-bar-fill--90"); - break; - case ImageUploadState.ReadSuccess: - case ImageUploadState.GeneratingPreview: - progressFillNode.classList.add( - "file-list-item__progress-bar-fill--100", - ); - break; - default: - break; - } - } // update preview button if (imageUpload.isSuccessState) { diff --git a/src/styles/upload.css b/src/styles/upload.css index bc61229..a4a0bcd 100644 --- a/src/styles/upload.css +++ b/src/styles/upload.css @@ -475,38 +475,6 @@ main { color: var(--gray-3); } -.file-list-item__progress-bar-border { - display: none; - margin: 5px 0 0 0; - width: 100%; - height: 6px; - background: rgba(0, 0, 0, 0.1); - border-radius: 30px; - overflow: hidden; -} -.file-list-item--progress .file-list-item__progress-bar-border { - display: block; -} - -.file-list-item__progress-bar-fill { - width: 0; - height: 100%; - background: var(--green-3); - transition: width 1s ease-in-out; -} -.file-list-item__progress-bar-fill--30 { - width: 30%; -} -.file-list-item__progress-bar-fill--60 { - width: 60%; -} -.file-list-item__progress-bar-fill--90 { - width: 90%; -} -.file-list-item__progress-bar-fill--100 { - width: 100%; -} - .color-section { margin: 20px 0 0 0; }