From 94e8cf2c08e8ea2a78790f716373193187ad8f93 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Sat, 17 Dec 2022 19:53:48 -0800 Subject: [PATCH 01/14] Add compression and download functions --- src/lib/compressBlob.js | 14 +++++++++++++ src/lib/decompressBlob.js | 13 ++++++++++++ src/lib/index.js | 5 +++++ src/lib/pipeResponseToFile.js | 11 ++++++++++ src/lib/saveCompressedBlob.js | 38 +++++++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 src/lib/compressBlob.js create mode 100644 src/lib/decompressBlob.js create mode 100644 src/lib/index.js create mode 100644 src/lib/pipeResponseToFile.js create mode 100644 src/lib/saveCompressedBlob.js diff --git a/src/lib/compressBlob.js b/src/lib/compressBlob.js new file mode 100644 index 00000000..20b5c766 --- /dev/null +++ b/src/lib/compressBlob.js @@ -0,0 +1,14 @@ + +/** + * Compress a blob. + * @param {Blob} blob The blob to compress. + * @returns {Response} The compressed response. + */ +export function compressBlob(blob) { + //noinspection JSUnresolvedFunction + return new Response( + blob + .stream() + .pipeThrough(new CompressionStream('gzip')) + ); +} diff --git a/src/lib/decompressBlob.js b/src/lib/decompressBlob.js new file mode 100644 index 00000000..439dd4f8 --- /dev/null +++ b/src/lib/decompressBlob.js @@ -0,0 +1,13 @@ + +/** + * Decompress a blob. + * @param {Blob} blob The blob to save. + * @returns {Promise} The file handle. + */ +export function decompressBlob(blob) { + const + ds = new DecompressionStream('gzip'), + decompressedStream = blob.stream().pipeThrough(ds); + + return new Response(decompressedStream); +} diff --git a/src/lib/index.js b/src/lib/index.js new file mode 100644 index 00000000..139de49a --- /dev/null +++ b/src/lib/index.js @@ -0,0 +1,5 @@ + +export { compressBlob } from './compressBlob.js'; +export { decompressBlob } from './decompressBlob.js'; +export { pipeResponseToFile } from './pipeResponseToFile.js'; +export { saveCompressedBlob } from './saveCompressedBlob.js'; diff --git a/src/lib/pipeResponseToFile.js b/src/lib/pipeResponseToFile.js new file mode 100644 index 00000000..5ba9e642 --- /dev/null +++ b/src/lib/pipeResponseToFile.js @@ -0,0 +1,11 @@ + +/** + * Pipe a response to a file. + * @param {Response} response The response to pipe. + * @param {FileSystemFileHandle} handle The file handle. + * @returns {Promise} The file handle. + */ +export async function pipeResponseToFile(response, handle) { + response.body.pipeTo( await handle.createWritable()) + return handle; +} diff --git a/src/lib/saveCompressedBlob.js b/src/lib/saveCompressedBlob.js new file mode 100644 index 00000000..03f21daf --- /dev/null +++ b/src/lib/saveCompressedBlob.js @@ -0,0 +1,38 @@ +import { compressBlob } from './compressBlob.js' +import { pipeResponseToFile } from './pipeResponseToFile.js' + + +const types = [ + { + description: 'Zine', + accept: { + 'application/gzip': ['.gz'], + } + } +] + + +/** + * Compress and save a blob. + * @param {Blob} blob The blob to save. + * @param {string} filename The suggested filename. + * @param {string} id The file picker ID. + * @returns {Promise} The file handle. + */ +export async function saveCompressedBlob( + blob = new Blob(), + filename = 'file.gz', + id = 'download', +) { + const + response = compressBlob(blob), + + handle = await window.showSaveFilePicker({ + types, + suggestedName: filename, + id: 'zine', + }); + + await pipeResponseToFile(response, handle); + return handle; +} From 0a90fbcc4bd7352d45706c39c48e14d837d405a7 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Sat, 17 Dec 2022 19:54:20 -0800 Subject: [PATCH 02/14] Add imports --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 77aeaa81..3f1a9068 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ "lint": "next lint", "vercel-build": "next build" }, + "imports": { + "#Lib": "./src/lib/index.js", + "#Lib/*": "./src/lib/*" + }, "dependencies": { "@aws-sdk/client-s3": "^3.180.0", "alea": "^1.0.1", From 59bf308c7432c76d67abc1a5e28758a5f65c6154 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Sat, 17 Dec 2022 19:55:06 -0800 Subject: [PATCH 03/14] Make default filename static --- src/components/generators/StoryboardComponent.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/generators/StoryboardComponent.jsx b/src/components/generators/StoryboardComponent.jsx index 74d75eb6..4e315160 100644 --- a/src/components/generators/StoryboardComponent.jsx +++ b/src/components/generators/StoryboardComponent.jsx @@ -16,6 +16,7 @@ import { // const textDecoder = new TextDecoder(); +const defaultFilename = 'storyboard.zine.gz'; // @@ -165,7 +166,7 @@ export const StoryboardComponent = ({ ], { type: 'application/octet-stream', }); - downloadFile(blob, 'storyboard.zine'); + downloadFile(blob, defaultFilename); }}> @@ -212,4 +213,4 @@ export const StoryboardComponent = ({ /> ) -}; \ No newline at end of file +}; From c24b43d4e979d58d7b443a5784a88761cd882058 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Sat, 17 Dec 2022 19:55:24 -0800 Subject: [PATCH 04/14] Compress scene --- .../generators/StoryboardComponent.jsx | 3 ++- src/utils/http-utils.js | 16 +++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/components/generators/StoryboardComponent.jsx b/src/components/generators/StoryboardComponent.jsx index 4e315160..1dfd3481 100644 --- a/src/components/generators/StoryboardComponent.jsx +++ b/src/components/generators/StoryboardComponent.jsx @@ -12,6 +12,7 @@ import { import { mainImageKey, } from '../../zine/zine-data-specs.js'; +import { decompressBlob } from '#Lib' // @@ -176,7 +177,7 @@ export const StoryboardComponent = ({ const file = e.target.files[0]; if (file) { (async () => { - const arrayBuffer = await file.arrayBuffer(); + const arrayBuffer = await decompressBlob(file).arrayBuffer(); // check magic bytes const firstBytes = new Uint8Array(arrayBuffer, 0, 4); const firstBytesString = textDecoder.decode(firstBytes); diff --git a/src/utils/http-utils.js b/src/utils/http-utils.js index 0a369a82..09c9d1d5 100644 --- a/src/utils/http-utils.js +++ b/src/utils/http-utils.js @@ -1,3 +1,5 @@ +import { saveCompressedBlob } from '#Lib' + export const getFormData = o => { const formData = new FormData(); for (const k in o) { @@ -6,14 +8,6 @@ export const getFormData = o => { return formData; }; -export function downloadFile(file, filename) { - const blobURL = URL.createObjectURL(file); - const tempLink = document.createElement('a'); - tempLink.style.display = 'none'; - tempLink.href = blobURL; - tempLink.setAttribute('download', filename); - - document.body.appendChild(tempLink); - tempLink.click(); - document.body.removeChild(tempLink); -} \ No newline at end of file +export async function downloadFile(file, filename) { + return saveCompressedBlob(file, filename) +} From 860fabbacb87575adeaad653d16ed32f2b2eb0c4 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Sat, 17 Dec 2022 20:46:10 -0800 Subject: [PATCH 05/14] Remove import maps --- package.json | 4 -- .../generators/StoryboardComponent.jsx | 2 +- src/lib/saveBlob.js | 38 +++++++++++++++++++ src/utils/http-utils.js | 3 +- 4 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 src/lib/saveBlob.js diff --git a/package.json b/package.json index 3f1a9068..77aeaa81 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,6 @@ "lint": "next lint", "vercel-build": "next build" }, - "imports": { - "#Lib": "./src/lib/index.js", - "#Lib/*": "./src/lib/*" - }, "dependencies": { "@aws-sdk/client-s3": "^3.180.0", "alea": "^1.0.1", diff --git a/src/components/generators/StoryboardComponent.jsx b/src/components/generators/StoryboardComponent.jsx index 1dfd3481..ebea9058 100644 --- a/src/components/generators/StoryboardComponent.jsx +++ b/src/components/generators/StoryboardComponent.jsx @@ -12,7 +12,7 @@ import { import { mainImageKey, } from '../../zine/zine-data-specs.js'; -import { decompressBlob } from '#Lib' +import { decompressBlob } from '../../lib/index.js'; // diff --git a/src/lib/saveBlob.js b/src/lib/saveBlob.js new file mode 100644 index 00000000..8342a594 --- /dev/null +++ b/src/lib/saveBlob.js @@ -0,0 +1,38 @@ +import { compressBlob } from './compressBlob.js' +import { pipeResponseToFile } from './pipeResponseToFile.js' + + +const types = [ + { + description: 'Zine', + accept: { + 'application/octet-stream': ['.zine'], + } + } +] + + +/** + * Save a blob. + * @param {Blob} blob The blob to save. + * @param {string} filename The suggested filename. + * @param {string} id The file picker ID. + * @returns {Promise} The file handle. + */ +export async function saveBlob( + blob = new Blob(), + filename = 'file.gz', + id = 'download', +) { + const + response = new Response(blob), + + handle = await window.showSaveFilePicker({ + types, + suggestedName: filename, + id: 'zine', + }); + + await pipeResponseToFile(response, handle); + return handle; +} diff --git a/src/utils/http-utils.js b/src/utils/http-utils.js index 09c9d1d5..e0bd3998 100644 --- a/src/utils/http-utils.js +++ b/src/utils/http-utils.js @@ -1,4 +1,5 @@ -import { saveCompressedBlob } from '#Lib' +import { saveCompressedBlob } from '../lib/index.js' + export const getFormData = o => { const formData = new FormData(); From 5d3baf60ba2b36d1f8e805cb6905e33b0cabe801 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:29:06 -0800 Subject: [PATCH 06/14] Remove lib directory --- src/lib/compressBlob.js | 14 ------------- src/lib/decompressBlob.js | 13 ------------ src/lib/index.js | 5 ----- src/lib/pipeResponseToFile.js | 11 ---------- src/lib/saveBlob.js | 38 ----------------------------------- src/lib/saveCompressedBlob.js | 38 ----------------------------------- 6 files changed, 119 deletions(-) delete mode 100644 src/lib/compressBlob.js delete mode 100644 src/lib/decompressBlob.js delete mode 100644 src/lib/index.js delete mode 100644 src/lib/pipeResponseToFile.js delete mode 100644 src/lib/saveBlob.js delete mode 100644 src/lib/saveCompressedBlob.js diff --git a/src/lib/compressBlob.js b/src/lib/compressBlob.js deleted file mode 100644 index 20b5c766..00000000 --- a/src/lib/compressBlob.js +++ /dev/null @@ -1,14 +0,0 @@ - -/** - * Compress a blob. - * @param {Blob} blob The blob to compress. - * @returns {Response} The compressed response. - */ -export function compressBlob(blob) { - //noinspection JSUnresolvedFunction - return new Response( - blob - .stream() - .pipeThrough(new CompressionStream('gzip')) - ); -} diff --git a/src/lib/decompressBlob.js b/src/lib/decompressBlob.js deleted file mode 100644 index 439dd4f8..00000000 --- a/src/lib/decompressBlob.js +++ /dev/null @@ -1,13 +0,0 @@ - -/** - * Decompress a blob. - * @param {Blob} blob The blob to save. - * @returns {Promise} The file handle. - */ -export function decompressBlob(blob) { - const - ds = new DecompressionStream('gzip'), - decompressedStream = blob.stream().pipeThrough(ds); - - return new Response(decompressedStream); -} diff --git a/src/lib/index.js b/src/lib/index.js deleted file mode 100644 index 139de49a..00000000 --- a/src/lib/index.js +++ /dev/null @@ -1,5 +0,0 @@ - -export { compressBlob } from './compressBlob.js'; -export { decompressBlob } from './decompressBlob.js'; -export { pipeResponseToFile } from './pipeResponseToFile.js'; -export { saveCompressedBlob } from './saveCompressedBlob.js'; diff --git a/src/lib/pipeResponseToFile.js b/src/lib/pipeResponseToFile.js deleted file mode 100644 index 5ba9e642..00000000 --- a/src/lib/pipeResponseToFile.js +++ /dev/null @@ -1,11 +0,0 @@ - -/** - * Pipe a response to a file. - * @param {Response} response The response to pipe. - * @param {FileSystemFileHandle} handle The file handle. - * @returns {Promise} The file handle. - */ -export async function pipeResponseToFile(response, handle) { - response.body.pipeTo( await handle.createWritable()) - return handle; -} diff --git a/src/lib/saveBlob.js b/src/lib/saveBlob.js deleted file mode 100644 index 8342a594..00000000 --- a/src/lib/saveBlob.js +++ /dev/null @@ -1,38 +0,0 @@ -import { compressBlob } from './compressBlob.js' -import { pipeResponseToFile } from './pipeResponseToFile.js' - - -const types = [ - { - description: 'Zine', - accept: { - 'application/octet-stream': ['.zine'], - } - } -] - - -/** - * Save a blob. - * @param {Blob} blob The blob to save. - * @param {string} filename The suggested filename. - * @param {string} id The file picker ID. - * @returns {Promise} The file handle. - */ -export async function saveBlob( - blob = new Blob(), - filename = 'file.gz', - id = 'download', -) { - const - response = new Response(blob), - - handle = await window.showSaveFilePicker({ - types, - suggestedName: filename, - id: 'zine', - }); - - await pipeResponseToFile(response, handle); - return handle; -} diff --git a/src/lib/saveCompressedBlob.js b/src/lib/saveCompressedBlob.js deleted file mode 100644 index 03f21daf..00000000 --- a/src/lib/saveCompressedBlob.js +++ /dev/null @@ -1,38 +0,0 @@ -import { compressBlob } from './compressBlob.js' -import { pipeResponseToFile } from './pipeResponseToFile.js' - - -const types = [ - { - description: 'Zine', - accept: { - 'application/gzip': ['.gz'], - } - } -] - - -/** - * Compress and save a blob. - * @param {Blob} blob The blob to save. - * @param {string} filename The suggested filename. - * @param {string} id The file picker ID. - * @returns {Promise} The file handle. - */ -export async function saveCompressedBlob( - blob = new Blob(), - filename = 'file.gz', - id = 'download', -) { - const - response = compressBlob(blob), - - handle = await window.showSaveFilePicker({ - types, - suggestedName: filename, - id: 'zine', - }); - - await pipeResponseToFile(response, handle); - return handle; -} From 04787a6f6f94039720d0615e290c490984758fc7 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:29:37 -0800 Subject: [PATCH 07/14] Add compression utilities --- src/utils/compression.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/utils/compression.js diff --git a/src/utils/compression.js b/src/utils/compression.js new file mode 100644 index 00000000..159ca770 --- /dev/null +++ b/src/utils/compression.js @@ -0,0 +1,22 @@ + + +/** + * Compress a blob and return a readable stream. + * @param {ReadableStream} stream The stream to compress. + * @returns {ReadableStream} The compressed stream. + */ +export function compressStream(stream) { + //noinspection JSUnresolvedFunction + return stream.pipeThrough(new CompressionStream('gzip')); +} + + +/** + * Decompress a blob and return a readable stream. + * @param {ReadableStream} stream The stream to decompress. + * @returns {ReadableStream} The decompressed stream. + */ +export function decompressStream(stream) { + //noinspection JSUnresolvedFunction + return stream.pipeThrough(new DecompressionStream('gzip')); +} From 2551e1cebfba2d99a75c0d6b84fa0d32447b570f Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:29:51 -0800 Subject: [PATCH 08/14] Restore downloadFile() --- src/utils/http-utils.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/utils/http-utils.js b/src/utils/http-utils.js index e0bd3998..bfd9be93 100644 --- a/src/utils/http-utils.js +++ b/src/utils/http-utils.js @@ -1,5 +1,3 @@ -import { saveCompressedBlob } from '../lib/index.js' - export const getFormData = o => { const formData = new FormData(); @@ -9,6 +7,14 @@ export const getFormData = o => { return formData; }; -export async function downloadFile(file, filename) { - return saveCompressedBlob(file, filename) +export function downloadFile(file, filename) { + const blobURL = URL.createObjectURL(file); + const tempLink = document.createElement('a'); + tempLink.style.display = 'none'; + tempLink.href = blobURL; + tempLink.setAttribute('download', filename); + + document.body.appendChild(tempLink); + tempLink.click(); + document.body.removeChild(tempLink); } From c20b40f06216e431c1f42fa074175d3b929ae96c Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:31:14 -0800 Subject: [PATCH 09/14] Add data constants --- constants/data.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 constants/data.js diff --git a/constants/data.js b/constants/data.js new file mode 100644 index 00000000..52d69977 --- /dev/null +++ b/constants/data.js @@ -0,0 +1,5 @@ + +export const + _1KB = 1024, + _1MB = _1KB * _1KB, + _1GB = _1KB * _1MB; From f1fded1ae4370820825eaa00cf497d4dcc030a8d Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:31:39 -0800 Subject: [PATCH 10/14] Add upload and download toasts --- src/utils/toasts/DownloadToast.js | 106 ++++++++++++++++++++++++++++++ src/utils/toasts/Toast.js | 37 +++++++++++ src/utils/toasts/UploadToast.js | 104 +++++++++++++++++++++++++++++ src/utils/toasts/constants.js | 6 ++ src/utils/toasts/index.js | 4 ++ 5 files changed, 257 insertions(+) create mode 100644 src/utils/toasts/DownloadToast.js create mode 100644 src/utils/toasts/Toast.js create mode 100644 src/utils/toasts/UploadToast.js create mode 100644 src/utils/toasts/constants.js create mode 100644 src/utils/toasts/index.js diff --git a/src/utils/toasts/DownloadToast.js b/src/utils/toasts/DownloadToast.js new file mode 100644 index 00000000..8ce839f1 --- /dev/null +++ b/src/utils/toasts/DownloadToast.js @@ -0,0 +1,106 @@ +import { _1MB } from '../../../constants/data.js'; + +import { + backgroundColor, + failColor, + successColor, + toastDuration +} from './constants.js'; + +import { Toast } from './Toast.js'; + + +const + /** + * Notify the user and remove the toast if the download fails. + */ + handleError = (ctx, e) => { + // Notify the user. + ctx.toast.textContent = + `Download failed for ${ctx.name}: ${e.message}`; + + // Change bg color and remove after a delay. + ctx.toast.style.background = failColor; + setTimeout(() => ctx.remove(), toastDuration); + }; + + +/** + * A toast which tracks the progress of a streamed download. + */ +export class DownloadToast extends Toast { + currentSize = 0; + name = ''; + loaded = 0; + + pipe = createPipe( this ) + size; + + /** + * Create a new download toast. + * @param {string} [name] The stream's name. + * @param {number} [size] The stream's total size. + */ + constructor( name= '', size ) { + super(); + + // Set name and size. + this.name = name; + if (size) this.size = (size / _1MB).toFixed(2); + + // Set initial text. + this.toast.textContent = `Downloading ${this.name}... (0MB)`; + } +} + + +const createPipe = ctx => { + return new TransformStream({ + /** + * Update the toast as the download progresses. + * @param {Uint8Array} chunk The chunk of data. + * @param {TransformStreamDefaultController} controller The controller. + */ + transform: (chunk, controller) => { + // TransformStream.transform doesn't catch errors by default. + try { + // Update metrics. + ctx.loaded += chunk.length; + ctx.currentSize = (ctx.loaded / _1MB ).toFixed(2); + + // Show current progress. + if ( ctx.size || ctx.size === 0 ) { + // Update text. + ctx.toast.textContent = + `Downloading ${ctx.name}... ` + + `(${ctx.currentSize}/${ctx.size} MB)`; + + // Update progress bar. + ctx.toast.style.background = + `linear-gradient(to right, ` + + `${successColor} ${ctx.currentSize / ctx.size * 100}%, ` + + `${backgroundColor} ${ctx.currentSize / ctx.size * 100}%)`; + } else { + ctx.toast.textContent = + `Downloading ${ctx.name}... (${ctx.currentSize} MB)`; + } + + // Continue. + controller.enqueue(chunk); + } catch (e) { handleError(ctx, e); } + }, + + /** + * Remove the toast when the download is complete. + */ + flush: () => { + // Notify the user. + ctx.toast.textContent = + `Downloaded ${ctx.name}. (${ctx.currentSize} MB)`; + + // Change bg color and remove after a delay. + ctx.toast.style.background = successColor; + setTimeout(() => ctx.remove(), toastDuration); + }, + }) +} diff --git a/src/utils/toasts/Toast.js b/src/utils/toasts/Toast.js new file mode 100644 index 00000000..c299a564 --- /dev/null +++ b/src/utils/toasts/Toast.js @@ -0,0 +1,37 @@ +import { backgroundColor } from './constants.js'; + + +const toastStyle = + 'position: fixed;' + + 'bottom: 0;' + + 'left: 0;' + + 'right: 0;' + + `background: ${backgroundColor};` + + 'color: #fff;' + + 'padding: 16px;' + + 'text-align: center;'; + + +/** + * Creates a toast element for displaying messages to the user. + */ +export class Toast { + // Toast element + toast = null; + + /** + * Create a new toast. + */ + constructor() { + // Create, format and append a toast element. + this.toast = document.createElement('div'); + this.toast.style = toastStyle; + + document.body.appendChild(this.toast); + } + + /** + * Remove the toast. + */ + remove() { this.toast.remove(); } +} diff --git a/src/utils/toasts/UploadToast.js b/src/utils/toasts/UploadToast.js new file mode 100644 index 00000000..ff4d541b --- /dev/null +++ b/src/utils/toasts/UploadToast.js @@ -0,0 +1,104 @@ +import { _1MB } from '../../../constants/data.js'; + +import { + backgroundColor, + failColor, + successColor, + toastDuration +} from './constants.js'; + +import { Toast } from './Toast.js'; + + +const + /** + * Notify the user and remove the toast if the download fails. + */ + handleError = (ctx, e) => { + // Notify the user. + ctx.toast.textContent = + `Upload failed for ${ctx.name}: ${e.message}`; + + // Change bg color and remove after a delay. + ctx.toast.style.background = failColor; + setTimeout(() => ctx.remove(), toastDuration); + }; + +/** + * A toast which tracks the progress of a streamed download. + */ +export class UploadToast extends Toast { + currentSize = 0; + name = ''; + loaded = 0; + + pipe = createPipe( this ) + size; + + /** + * Create a new download toast. + * @param {string} name The stream's name. + * @param {number} [size] The stream's total size. + */ + constructor( name = '', size) { + super(); + + // Set name and size. + this.name = name; + if (size) this.size = (size / _1MB).toFixed(2); + + // Set initial text. + this.toast.textContent = `Uploading ${this.name}... (0MB)`; + } +} + + +const createPipe = ctx => { + return new TransformStream({ + /** + * Update the toast as the download progresses. + * @param {Uint8Array} chunk The chunk of data. + * @param {TransformStreamDefaultController} controller The controller. + */ + transform: (chunk, controller) => { + // TransformStream.transform doesn't catch errors by default. + try { + // Update metrics. + ctx.loaded += chunk.length; + ctx.currentSize = (ctx.loaded / _1MB ).toFixed(2); + + // Show current progress. + if ( ctx.size || ctx.size === 0 ) { + ctx.toast.textContent = + `Uploading ${ctx.name}... ` + + `(${ctx.currentSize}/${ctx.size} MB)`; + + // Update progress bar. + ctx.toast.style.background = + `linear-gradient(to right, ` + + `${successColor} ${ctx.currentSize / ctx.size * 100}%, ` + + `${backgroundColor} ${ctx.currentSize / ctx.size * 100}%)`; + } else { + ctx.toast.textContent = + `Uploading ${ctx.name}... (${ctx.currentSize} MB)`; + } + + // Continue. + controller.enqueue(chunk); + } catch (e) { handleError(ctx, e); } + }, + + /** + * Remove the toast when the download is complete. + */ + flush: () => { + // Notify the user. + ctx.toast.textContent = + `Uploaded ${ctx.name}. (${ctx.currentSize} MB)`; + + // Change bg color and remove after a delay. + ctx.toast.style.background = successColor; + setTimeout(() => ctx.remove(), toastDuration); + }, + }) +} diff --git a/src/utils/toasts/constants.js b/src/utils/toasts/constants.js new file mode 100644 index 00000000..ccbba67b --- /dev/null +++ b/src/utils/toasts/constants.js @@ -0,0 +1,6 @@ + +export const + backgroundColor = '#333', + failColor = 'firebrick', + successColor = 'forestgreen', + toastDuration = 5000; diff --git a/src/utils/toasts/index.js b/src/utils/toasts/index.js new file mode 100644 index 00000000..6a2b0453 --- /dev/null +++ b/src/utils/toasts/index.js @@ -0,0 +1,4 @@ + +export { DownloadToast } from './DownloadToast.js'; +export { Toast } from './Toast.js'; +export { UploadToast } from './UploadToast.js'; From d9dd7f66dc0b8cd58570ac081d8c41beb5233d82 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:32:47 -0800 Subject: [PATCH 11/14] Add file utils --- src/utils/file/index.js | 5 ++++ src/utils/file/loadStream.js | 18 ++++++++++++ src/utils/file/pipeResponseToFile.js | 23 +++++++++++++++ src/utils/file/saveCompressedStream.js | 22 ++++++++++++++ src/utils/file/saveStream.js | 40 ++++++++++++++++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 src/utils/file/index.js create mode 100644 src/utils/file/loadStream.js create mode 100644 src/utils/file/pipeResponseToFile.js create mode 100644 src/utils/file/saveCompressedStream.js create mode 100644 src/utils/file/saveStream.js diff --git a/src/utils/file/index.js b/src/utils/file/index.js new file mode 100644 index 00000000..b88904e8 --- /dev/null +++ b/src/utils/file/index.js @@ -0,0 +1,5 @@ + +export { loadStream } from './loadStream.js'; +export { pipeResponseToFile } from './pipeResponseToFile.js'; +export { saveCompressedStream } from './saveCompressedStream.js'; +export { saveStream } from './saveStream.js'; diff --git a/src/utils/file/loadStream.js b/src/utils/file/loadStream.js new file mode 100644 index 00000000..18b4b11f --- /dev/null +++ b/src/utils/file/loadStream.js @@ -0,0 +1,18 @@ +import { UploadToast } from '../toasts/index.js' + + +/** + * Load a stream. + * @param {ReadableStream} stream The stream to load. + * @param {string} name The name of the stream. + * @param {number} [size] The total size of the stream. + * @returns {ReadableStream} The stream. + */ +export function loadStream( + stream = new ReadableStream(), + name = 'stream', + size, +) { + return stream + .pipeThrough(new UploadToast(name, size).pipe); +} diff --git a/src/utils/file/pipeResponseToFile.js b/src/utils/file/pipeResponseToFile.js new file mode 100644 index 00000000..fa45077b --- /dev/null +++ b/src/utils/file/pipeResponseToFile.js @@ -0,0 +1,23 @@ +import { DownloadToast } from '../toasts/index.js' + + +/** + * Pipe a response to a file. + * @param {Response} response The response to pipe. + * @param {FileSystemFileHandle} handle The file handle. + * @param {number} [size] The total size of the response. + * @returns {Promise} The file handle. + */ +export async function pipeResponseToFile(response, handle, size) { + //noinspection JSUnresolvedFunction + const writable = await handle.createWritable(); + + // Notify the user that the download has begun. + const toast = new DownloadToast(handle.name, size); + + await response.body + .pipeThrough(toast.pipe) + .pipeTo(writable) + + return handle; +} diff --git a/src/utils/file/saveCompressedStream.js b/src/utils/file/saveCompressedStream.js new file mode 100644 index 00000000..b8f49998 --- /dev/null +++ b/src/utils/file/saveCompressedStream.js @@ -0,0 +1,22 @@ +import { compressStream } from '../compression.js' +import { saveStream } from './saveStream.js' + + +const gzipTypes = [{ + description: 'Gzip', + accept: { + 'application/gzip': ['.gz'], + } +}] + + +/** + * Compresses a stream and save it to a file. + * @param {ReadableStream} stream The stream to compress. + * @param {string} suggestedName The suggested name. + * @param {number} [size] The total size of the stream. + * @returns {Promise} The file handle. + */ +export function saveCompressedStream(stream, suggestedName, size) { + return saveStream(compressStream(stream), gzipTypes, suggestedName, size); +} diff --git a/src/utils/file/saveStream.js b/src/utils/file/saveStream.js new file mode 100644 index 00000000..5cd49190 --- /dev/null +++ b/src/utils/file/saveStream.js @@ -0,0 +1,40 @@ +import { pipeResponseToFile } from './pipeResponseToFile.js'; + + +const defaultTypes = [{ + description: 'File', + accept: { + 'application/octet-stream': ['.bin'], + } +}]; + + +/** + * Get the first file extension from a types object. + */ +function getFirstExtension(types) { + return Object.values(types[0].accept)[0][0]; +} + + +/** + * Save a stream to a file. + * @param {ReadableStream} stream The blob to save. + * @param {Array} types The file types.blob + * @param {string} suggestedName The suggested filename. + * @param {number} [size] The total size of the stream. + * @returns {Promise} The file handle. + */ +export async function saveStream( + stream = new ReadableStream(), + types = defaultTypes, + suggestedName = `file${getFirstExtension(types)}`, + size, +) { + // Prepare a response and get a file handle. + //noinspection JSUnresolvedFunction + const handle = await window.showSaveFilePicker({ types, suggestedName }); + + await pipeResponseToFile(new Response(stream), handle, size); + return handle; +} From fed37b9994c353dd26a177688ad6cd7b5987ebe3 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:34:50 -0800 Subject: [PATCH 12/14] Use new compression and file utils --- .../generators/StoryboardComponent.jsx | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/components/generators/StoryboardComponent.jsx b/src/components/generators/StoryboardComponent.jsx index ebea9058..b3d8d2e8 100644 --- a/src/components/generators/StoryboardComponent.jsx +++ b/src/components/generators/StoryboardComponent.jsx @@ -12,7 +12,8 @@ import { import { mainImageKey, } from '../../zine/zine-data-specs.js'; -import { decompressBlob } from '../../lib/index.js'; +import { decompressStream } from '../../utils/compression.js'; +import { loadStream, saveCompressedStream } from '../../utils/file/index.js' // @@ -153,21 +154,24 @@ export const StoryboardComponent = ({ onDrop={drop} >
- @@ -177,7 +181,17 @@ export const StoryboardComponent = ({ const file = e.target.files[0]; if (file) { (async () => { - const arrayBuffer = await decompressBlob(file).arrayBuffer(); + const + // Decompress stream. + // TODO: Estimate compressed size so we can show progress. + stream = loadStream( + decompressStream(file.stream()), + file.name + ), + + // Get array buffer. + arrayBuffer = await new Response( stream ).arrayBuffer(); + // check magic bytes const firstBytes = new Uint8Array(arrayBuffer, 0, 4); const firstBytesString = textDecoder.decode(firstBytes); From 8f36ed58639325f86bbebe59cd41f872c500ca57 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:35:34 -0800 Subject: [PATCH 13/14] Scroll overflow storyboard layers to prevent width blowout --- styles/Storyboard3DRenderer.module.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/styles/Storyboard3DRenderer.module.css b/styles/Storyboard3DRenderer.module.css index 16d7c67d..a3cdcaeb 100644 --- a/styles/Storyboard3DRenderer.module.css +++ b/styles/Storyboard3DRenderer.module.css @@ -39,6 +39,7 @@ .storyboard3DRenderer .layers { display: flex; + overflow-x: auto; } .storyboard3DRenderer .layers .layer { width: 150px; @@ -56,4 +57,4 @@ } .storyboard3DRenderer .layers .layer.selected { cursor: default; -} \ No newline at end of file +} From 2ac8760c4ff8da7e9f9c8801abfe99589c6fea01 Mon Sep 17 00:00:00 2001 From: soulofmischief <30357883+soulofmischief@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:36:35 -0800 Subject: [PATCH 14/14] Clean up code --- .../generators/StoryboardComponent.jsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/components/generators/StoryboardComponent.jsx b/src/components/generators/StoryboardComponent.jsx index b3d8d2e8..11c67ccf 100644 --- a/src/components/generators/StoryboardComponent.jsx +++ b/src/components/generators/StoryboardComponent.jsx @@ -1,26 +1,18 @@ -import {useState, useEffect} from 'react'; +import { useState, useEffect } from 'react'; import classnames from 'classnames'; -import {PlaceholderImg} from '../placeholders/PlaceholderImg.jsx'; -import {ArrayBufferRenderer} from '../renderers/ArrayBufferRenderer.jsx'; -// import {zbencode, zbdecode} from '../../zine/encoding.js'; -import {downloadFile} from '../../utils/http-utils.js'; +import { PlaceholderImg } from '../placeholders/PlaceholderImg.jsx'; +import { ArrayBufferRenderer } from '../renderers/ArrayBufferRenderer.jsx'; import styles from '../../../styles/Storyboard.module.css'; -import { - zineMagicBytes, -} from '../../zine/zine-format.js'; -import { - mainImageKey, -} from '../../zine/zine-data-specs.js'; +import { zineMagicBytes } from '../../zine/zine-format.js'; +import { mainImageKey } from '../../zine/zine-data-specs.js'; import { decompressStream } from '../../utils/compression.js'; import { loadStream, saveCompressedStream } from '../../utils/file/index.js' -// const textDecoder = new TextDecoder(); const defaultFilename = 'storyboard.zine.gz'; -// const StoryboardPanel = ({ storyboard,