From aff5b7cbea7262b321f14a661c8ebc1d68867f79 Mon Sep 17 00:00:00 2001 From: Caleb Evans Date: Wed, 21 Aug 2024 16:59:35 -0700 Subject: [PATCH] Convert components to JSX --- scripts/components/app-header.jsx | 30 ++- scripts/components/app.jsx | 54 +++--- scripts/components/control.jsx | 76 +++++--- scripts/components/drawing-area.jsx | 60 +++--- scripts/components/export-gif.jsx | 74 ++++--- scripts/components/export.jsx | 134 +++++++------ scripts/components/frame.jsx | 216 +++++++++++---------- scripts/components/import.jsx | 82 ++++---- scripts/components/loading.jsx | 14 +- scripts/components/modal.jsx | 28 +-- scripts/components/overlay.jsx | 4 +- scripts/components/panel.jsx | 35 ++-- scripts/components/progress-bar.jsx | 23 +-- scripts/components/settings.jsx | 101 ++++++---- scripts/components/storage-upgrader.jsx | 44 ++--- scripts/components/story-controls.jsx | 176 ++++++++--------- scripts/components/story-editor.jsx | 62 +++--- scripts/components/story-header.jsx | 126 ++++++------ scripts/components/story-list.jsx | 36 ++-- scripts/components/story.jsx | 19 +- scripts/components/timeline.jsx | 72 +++---- scripts/components/update-notification.jsx | 33 ++-- 22 files changed, 805 insertions(+), 694 deletions(-) diff --git a/scripts/components/app-header.jsx b/scripts/components/app-header.jsx index 138152c..4ba3238 100644 --- a/scripts/components/app-header.jsx +++ b/scripts/components/app-header.jsx @@ -1,23 +1,19 @@ -import m from 'mithril'; - class AppHeaderComponent { - view() { - return m('.app-header', [ - m('h1', 'Flip Book'), - m('.app-header-mentions', [ - m('span.app-header-mention', [ - 'By ', - m('a[href="https://calebevans.me/"]', 'Caleb Evans'), - '.' - ]), - m('span.app-header-mention', [ - 'For my friend and brother, Bill.' - ]) - ]) - ]); + return ( +
+

Flip Book

+
+ + By Caleb Evans. + + + For my friend and brother, Bill. + +
+
+ ); } - } export default AppHeaderComponent; diff --git a/scripts/components/app.jsx b/scripts/components/app.jsx index 3c6a4c6..f76297b 100644 --- a/scripts/components/app.jsx +++ b/scripts/components/app.jsx @@ -1,12 +1,11 @@ -import m from 'mithril'; -import App from '../models/app.js'; -import UpdateNotificationComponent from './update-notification.jsx'; -import AppHeaderComponent from './app-header.jsx'; -import StoryComponent from './story.jsx'; -import StorageUpgraderComponent from './storage-upgrader.jsx'; +import m from "mithril"; +import App from "../models/app.js"; +import UpdateNotificationComponent from "./update-notification.jsx"; +import AppHeaderComponent from "./app-header.jsx"; +import StoryComponent from "./story.jsx"; +import StorageUpgraderComponent from "./storage-upgrader.jsx"; class AppComponent { - oninit() { App.restore().then((app) => { this.app = app; @@ -14,19 +13,19 @@ class AppComponent { }); } - oncreate({dom}) { + oncreate({ dom }) { dom.focus(); } async navigateFramesViaKeyboard(event) { let story = await this.app.selectedStory; - if (event.key === 'ArrowLeft' && !story.playing) { + if (event.key === "ArrowLeft" && !story.playing) { story.selectPreviousFrame(); await story.save(); - } else if (event.key === 'ArrowRight' && !story.playing) { + } else if (event.key === "ArrowRight" && !story.playing) { story.selectNextFrame(); await story.save(); - } else if (event.key === ' ') { + } else if (event.key === " ") { event.preventDefault(); if (story.playing) { story.pause(); @@ -39,25 +38,22 @@ class AppComponent { } view() { - return m('div.app[tabindex=-1]', { - onkeydown: (event) => this.navigateFramesViaKeyboard(event) - }, [ - - // The UpdateNotificationComponent manages its own visibility - m(UpdateNotificationComponent), - - m(StorageUpgraderComponent), - - m(AppHeaderComponent), - - this.app?.selectedStory ? m(StoryComponent, { - app: this.app, - story: this.app.selectedStory - }) : null - - ]); + return ( +
this.navigateFramesViaKeyboard(event)} + > + {/* The UpdateNotificationComponent manages its own visibility */} + + + + {this.app?.selectedStory ? ( + + ) : null} +
+ ); } - } export default AppComponent; diff --git a/scripts/components/control.jsx b/scripts/components/control.jsx index ac04503..1adc471 100644 --- a/scripts/components/control.jsx +++ b/scripts/components/control.jsx @@ -1,34 +1,56 @@ -import m from 'mithril'; -import clsx from 'clsx'; -import PanelComponent from './panel.jsx'; +import clsx from "clsx"; +import PanelComponent from "./panel.jsx"; class ControlComponent { - - view({attrs: {id, title, icon = null, label = null, action = null, panel = null, panelPosition = 'top'}}) { - return m(`div`, { - class: clsx('control', `control-${id}`, {'control-has-label': label}) - }, [ - panel ? m(PanelComponent, {id, position: panelPosition}, panel) : null, - m('button.control-button', { - title, - onclick: ({target}) => { - if (panel) { - PanelComponent.togglePanel(id); - } else if (action) { - // Do not close the panel where the clicked control resides - if (!target.closest('.panel')) { - PanelComponent.closeAllPanels(); + view({ + attrs: { + id, + title, + icon = null, + label = null, + action = null, + panel = null, + panelPosition = "top", + }, + }) { + return ( +
+ {panel ? ( + + {panel} + + ) : null} + +
+ ); } - } export default ControlComponent; diff --git a/scripts/components/drawing-area.jsx b/scripts/components/drawing-area.jsx index 35325af..c1e5eae 100644 --- a/scripts/components/drawing-area.jsx +++ b/scripts/components/drawing-area.jsx @@ -1,22 +1,20 @@ -import m from 'mithril'; -import FrameComponent from './frame.jsx'; +import FrameComponent from "./frame.jsx"; class DrawingAreaComponent extends FrameComponent { - - oninit({attrs: {story, frame, drawingEnabled = true}}) { + oninit({ attrs: { story, frame, drawingEnabled = true } }) { this.story = story; - super.oninit({attrs: {frame}}); + super.oninit({ attrs: { frame } }); this.drawingEnabled = drawingEnabled; } - oncreate({dom}) { - super.oncreate({dom}); + oncreate({ dom }) { + super.oncreate({ dom }); } - onupdate({attrs: {story, frame, drawingEnabled = true}}) { + onupdate({ attrs: { story, frame, drawingEnabled = true } }) { this.story = story; this.drawingEnabled = drawingEnabled; - super.onupdate({attrs: {frame}}); + super.onupdate({ attrs: { frame } }); } handleDrawStart(event, pageX, pageY) { @@ -30,7 +28,7 @@ class DrawingAreaComponent extends FrameComponent { let startX = (pageX - this.canvasOffsetLeft) * this.canvasScaleFactor; let startY = (pageY - this.canvasOffsetTop) * this.canvasScaleFactor; this.frame.startNewGroup({ - styles: Object.assign({}, this.story.frameStyles) + styles: Object.assign({}, this.story.frameStyles), }); this.frame.addPoint(startX, startY); this.lastX = startX; @@ -68,13 +66,21 @@ class DrawingAreaComponent extends FrameComponent { handleTouchStart(event) { if (event.changedTouches?.length > 0) { - this.handleDrawStart(event, event.changedTouches[0].pageX, event.changedTouches[0].pageY); + this.handleDrawStart( + event, + event.changedTouches[0].pageX, + event.changedTouches[0].pageY, + ); } } handleTouchMove(event) { if (event.changedTouches?.length > 0) { - this.handleDrawMove(event, event.changedTouches[0].pageX, event.changedTouches[0].pageY); + this.handleDrawMove( + event, + event.changedTouches[0].pageX, + event.changedTouches[0].pageY, + ); } } @@ -109,20 +115,22 @@ class DrawingAreaComponent extends FrameComponent { } view() { - return m('canvas.selected-frame', { - width: FrameComponent.width, - height: FrameComponent.height, - // Touch events - ontouchstart: (event) => this.handleTouchStart(event), - ontouchmove: (event) => this.handleTouchMove(event), - ontouchend: (event) => this.handleTouchEnd(event), - // Mouse events - onmousedown: (event) => this.handleMouseDown(event), - onmousemove: (event) => this.handleMouseMove(event), - onmouseup: (event) => this.handleMouseUp(event), - onmouseout: (event) => this.handleMouseUp(event) - }); + return ( + this.handleTouchStart(event)} + ontouchmove={(event) => this.handleTouchMove(event)} + ontouchend={(event) => this.handleTouchEnd(event)} + // Mouse events + onmousedown={(event) => this.handleMouseDown(event)} + onmousemove={(event) => this.handleMouseMove(event)} + onmouseup={(event) => this.handleMouseUp(event)} + onmouseout={(event) => this.handleMouseUp(event)} + /> + ); } - } export default DrawingAreaComponent; diff --git a/scripts/components/export-gif.jsx b/scripts/components/export-gif.jsx index 4fcd730..a7ee38a 100644 --- a/scripts/components/export-gif.jsx +++ b/scripts/components/export-gif.jsx @@ -1,36 +1,50 @@ -import m from 'mithril'; -import clsx from 'clsx'; -import ControlComponent from './control.jsx'; -import ProgressBarComponent from './progress-bar.jsx'; +import clsx from "clsx"; +import ControlComponent from "./control.jsx"; +import ProgressBarComponent from "./progress-bar.jsx"; class ExportGifComponent { - - view({attrs: {isExportingGif, exportProgress, isGifExportFinished, exportedImageUrl, abort}}) { - return m('div', { - class: clsx('export-gif-screen', {'visible': isExportingGif || isGifExportFinished}) - }, [ - m(ControlComponent, { - id: 'close-export-gif-overlay', - title: isGifExportFinished ? 'Close overlay' : 'Abort GIF export', - icon: 'close', - action: () => abort() - }), - m('div.export-gif-overlay'), - m('div.export-gif-heading', exportedImageUrl ? - 'GIF Generated!' : - 'Generating GIF...'), - m('p.export-gif-message', exportedImageUrl ? - 'Right-click the image and choose "Save Image As..." to download.' : - ''), - exportedImageUrl ? m('img.exported-image', { - src: exportedImageUrl, - alt: 'Exported GIF' - }) : m(ProgressBarComponent, { - progress: exportProgress - }) - ]); + view({ + attrs: { + isExportingGif, + exportProgress, + isGifExportFinished, + exportedImageUrl, + abort, + }, + }) { + return ( +
+ abort()} + /> +
+
+ {exportedImageUrl ? "GIF Generated!" : "Generating GIF..."} +
+

+ {exportedImageUrl + ? 'Right-click the image and choose "Save Image As..." to download.' + : ""} +

+ {exportedImageUrl ? ( + Exported GIF + ) : ( + + )} +
+ ); } - } export default ExportGifComponent; diff --git a/scripts/components/export.jsx b/scripts/components/export.jsx index 6046637..2e8783c 100644 --- a/scripts/components/export.jsx +++ b/scripts/components/export.jsx @@ -1,35 +1,36 @@ -import m from 'mithril'; -import GIF from 'gif.js.optimized'; -import GIFWorkerUrl from 'gif.js.optimized/dist/gif.worker.js?url'; -import FrameComponent from './frame.jsx'; -import ControlComponent from './control.jsx'; -import ProgressBarComponent from './progress-bar.jsx'; -import ExportGifComponent from './export-gif.jsx'; +import m from "mithril"; +import GIF from "gif.js.optimized"; +import GIFWorkerUrl from "gif.js.optimized/dist/gif.worker.js?url"; +import FrameComponent from "./frame.jsx"; +import ControlComponent from "./control.jsx"; +import ProgressBarComponent from "./progress-bar.jsx"; +import ExportGifComponent from "./export-gif.jsx"; class ExportComponent { - exportGif(story) { this.gifGenerator = new GIF({ workers: 2, - workerScript: GIFWorkerUrl + workerScript: GIFWorkerUrl, }); story.frames.forEach((frame) => { - let canvas = document.createElement('canvas'); - canvas.width = Math.ceil(FrameComponent.width * (story.exportedGifSize / FrameComponent.height)); + let canvas = document.createElement("canvas"); + canvas.width = Math.ceil( + FrameComponent.width * (story.exportedGifSize / FrameComponent.height), + ); canvas.height = story.exportedGifSize; let frameComponent = new FrameComponent(); - frameComponent.oninit({attrs: {frame}}); - frameComponent.oncreate({dom: canvas}); + frameComponent.oninit({ attrs: { frame } }); + frameComponent.oncreate({ dom: canvas }); frameComponent.render({ - backgroundColor: '#fff' + backgroundColor: "#fff", }); - this.gifGenerator.addFrame(canvas, {delay: story.frameDuration}); + this.gifGenerator.addFrame(canvas, { delay: story.frameDuration }); }); - this.gifGenerator.on('progress', (currentProgress) => { + this.gifGenerator.on("progress", (currentProgress) => { this.exportProgress = currentProgress; m.redraw(); }); - this.gifGenerator.on('finished', (blob) => { + this.gifGenerator.on("finished", (blob) => { let image = new Image(); image.onload = () => { // Aribtrarily wait half a second before loading to give the progress bar @@ -55,7 +56,9 @@ class ExportComponent { isGifExportFinished() { if (this.gifGenerator) { - return this.gifGenerator.finishedFrames === this.gifGenerator.frames.length; + return ( + this.gifGenerator.finishedFrames === this.gifGenerator.frames.length + ); } else { return false; } @@ -73,17 +76,17 @@ class ExportComponent { // not duplicated in localStorage (the story metadata is already stored in // the app manifest); reconstruct the object with the metadata key added // first, since ES6 preserves object key order - let storyJson = Object.assign({metadata: story.metadata}, story.toJSON()); + let storyJson = Object.assign({ metadata: story.metadata }, story.toJSON()); // When we import the story somewhere else, it would be more convenient for // the first frame to be selected delete storyJson.selectedFrameIndex; let slugName = story.metadata.name .toLowerCase() - .replace(/(^\W+)|(\W+$)/gi, '') - .replace(/\W+/gi, '-'); + .replace(/(^\W+)|(\W+$)/gi, "") + .replace(/\W+/gi, "-"); let blob = new Blob([JSON.stringify(storyJson)]); - let a = document.createElement('a'); - a.href = URL.createObjectURL(blob, {type: 'application/json'}); + let a = document.createElement("a"); + a.href = URL.createObjectURL(blob, { type: "application/json" }); a.download = `${slugName}.flipbook`; a.click(); } @@ -93,49 +96,58 @@ class ExportComponent { await story.save(); } - view({attrs: {story}}) { - return m('div.export-options', [ - m('h2', 'Export'), - m('div.exported-gif-controls', [ - m(ControlComponent, { - id: 'export-gif', - title: 'Export GIF', - label: 'Export GIF', - action: () => this.exportGif(story) - }), - m('div.exported-gif-size', [ - m('label[for=exported-gif-size]', 'GIF Size:'), - m('select#exported-gif-size', { - onchange: ({target}) => this.setExportedGifSize(story, target.value) - }, ExportComponent.exportedGifSizes.map((size) => { - return m('option', { - selected: size === story.exportedGifSize, - value: size - }, `${Math.ceil(FrameComponent.width * (size / FrameComponent.height))} x ${size}`); - })) - ]) - ]), - m(ControlComponent, { - id: 'export-project', - title: 'Export Project Data', - label: 'Export Project Data', - action: () => this.exportProject(story) - }), - m(ExportGifComponent, { - isExportingGif: this.isExportingGif(), - isGifExportFinished: this.isGifExportFinished(), - exportProgress: this.exportProgress, - exportedImageUrl: this.exportedImageUrl, - abort: () => this.abortGifExport() - }) - ]); + view({ attrs: { story } }) { + return ( +
+

Export

+
+ this.exportGif(story)} + /> +
+ + +
+
+ this.exportProject(story)} + /> + this.abortGifExport()} + /> +
+ ); } - } // The list of sizes at which the story can be exported as a GIF; each value // corresponds to the height of the exported GIF ExportComponent.exportedGifSizes = [1080, 720, 540, 360]; - export default ExportComponent; diff --git a/scripts/components/frame.jsx b/scripts/components/frame.jsx index 2a60a65..5668d01 100644 --- a/scripts/components/frame.jsx +++ b/scripts/components/frame.jsx @@ -1,125 +1,141 @@ -import m from 'mithril'; - class FrameComponent { + oninit({ attrs: { frame } }) { + this.frame = frame; + // Store the current number of stroke groups in the frame so we can later + // detect changes to the same frame + this.strokeGroupCount = frame.strokeGroups.length; + // Store the current number of points in the last stroke group + this.pointCount = frame.countPointsInLastStrokeGroup(); + } + + oncreate({ dom }) { + this.canvas = dom; + this.ctx = this.canvas.getContext("2d"); + this.lastRenderedStyles = {}; + this.render(); + } - oninit({attrs: {frame}}) { + onupdate({ attrs: { frame } }) { + if ( + frame !== this.frame || + frame.strokeGroups.length !== this.strokeGroupCount || + frame.countPointsInLastStrokeGroup() !== this.pointCount + ) { this.frame = frame; - // Store the current number of stroke groups in the frame so we can later - // detect changes to the same frame this.strokeGroupCount = frame.strokeGroups.length; - // Store the current number of points in the last stroke group this.pointCount = frame.countPointsInLastStrokeGroup(); - } - - oncreate({dom}) { - this.canvas = dom; - this.ctx = this.canvas.getContext('2d'); - this.lastRenderedStyles = {}; this.render(); } + } - onupdate({attrs: {frame}}) { - if (frame !== this.frame || frame.strokeGroups.length !== this.strokeGroupCount || frame.countPointsInLastStrokeGroup() !== this.pointCount) { - this.frame = frame; - this.strokeGroupCount = frame.strokeGroups.length; - this.pointCount = frame.countPointsInLastStrokeGroup(); - this.render(); - } + render({ backgroundColor = null } = {}) { + this.clearCanvas(); + if (backgroundColor) { + this.setBackground(backgroundColor); } - - render({backgroundColor = null} = {}) { - this.clearCanvas(); - if (backgroundColor) { - this.setBackground(backgroundColor); - } - if (this.ctx.canvas.width !== FrameComponent.width || this.ctx.canvas.height !== FrameComponent.height) { - this.scaleCanvas(); - } - this.drawGroups(); + if ( + this.ctx.canvas.width !== FrameComponent.width || + this.ctx.canvas.height !== FrameComponent.height + ) { + this.scaleCanvas(); } + this.drawGroups(); + } - clearCanvas() { - this.ctx.resetTransform(); - this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); - } + clearCanvas() { + this.ctx.resetTransform(); + this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + } - scaleCanvas() { - this.ctx.scale(this.ctx.canvas.width / FrameComponent.width, this.ctx.canvas.height / FrameComponent.height); - } + scaleCanvas() { + this.ctx.scale( + this.ctx.canvas.width / FrameComponent.width, + this.ctx.canvas.height / FrameComponent.height, + ); + } - setBackground(backgroundColor) { - this.ctx.fillStyle = backgroundColor; - this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); - this.ctx.fillStyle = 'transparent'; - } + setBackground(backgroundColor) { + this.ctx.fillStyle = backgroundColor; + this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + this.ctx.fillStyle = "transparent"; + } - setGlobalStyles() { - this.ctx.strokeStyle = this.frame.styles.strokeStyle; - this.ctx.lineCap = this.frame.styles.lineCap; - this.ctx.lineJoin = this.frame.styles.lineJoin; - } + setGlobalStyles() { + this.ctx.strokeStyle = this.frame.styles.strokeStyle; + this.ctx.lineCap = this.frame.styles.lineCap; + this.ctx.lineJoin = this.frame.styles.lineJoin; + } - setGroupStyles(strokeGroup) { - // TODO: implement the ability to change stroke color within the UI; the - // below line isn't needed until that is done - // this.setGroupStyle(strokeGroup, 'strokeStyle'); - this.setGroupStyle(strokeGroup, 'lineWidth'); - } + setGroupStyles(strokeGroup) { + // TODO: implement the ability to change stroke color within the UI; the + // below line isn't needed until that is done + // this.setGroupStyle(strokeGroup, 'strokeStyle'); + this.setGroupStyle(strokeGroup, "lineWidth"); + } - setGroupStyle(strokeGroup, styleName) { - let styleValue = strokeGroup.styles ? strokeGroup.styles[styleName] : this.frame.styles[styleName]; - // this.ctx[styleName] cannot be used to check if styleValue has changed, - // because when setting this.ctx.strokeStyle to #000, this.ctx.strokeStyle - // evaluates to #000000; we therefore need to store the exact value on a - // separate object - if (styleValue !== this.lastRenderedStyles[styleName]) { - this.ctx[styleName] = styleValue; - this.lastRenderedStyles[styleName] = styleValue; - } + setGroupStyle(strokeGroup, styleName) { + let styleValue = strokeGroup.styles + ? strokeGroup.styles[styleName] + : this.frame.styles[styleName]; + // this.ctx[styleName] cannot be used to check if styleValue has changed, + // because when setting this.ctx.strokeStyle to #000, this.ctx.strokeStyle + // evaluates to #000000; we therefore need to store the exact value on a + // separate object + if (styleValue !== this.lastRenderedStyles[styleName]) { + this.ctx[styleName] = styleValue; + this.lastRenderedStyles[styleName] = styleValue; } + } - drawGroups() { - this.setGlobalStyles(); - for (let g = 0; g < this.frame.strokeGroups.length; g += 1) { - let strokeGroup = this.frame.strokeGroups[g]; - let currentX = strokeGroup.points[0][0]; - let currentY = strokeGroup.points[0][1]; - this.setGroupStyles(strokeGroup); - if (strokeGroup.points.length === 1) { - // Draw a circle - this.ctx.fillStyle = strokeGroup.styles ? strokeGroup.styles.strokeStyle : this.frame.styles.strokeStyle; - this.ctx.beginPath(); - this.ctx.arc( - currentX, currentY, - (strokeGroup.styles ? strokeGroup.styles.lineWidth : this.frame.styles.lineWidth) / 2, - 0, Math.PI * 2, - false - ); - this.ctx.fill(); - this.ctx.closePath(); - this.ctx.fillStyle = 'transparent'; - } else { - this.ctx.beginPath(); - this.ctx.moveTo(currentX, currentY); - for (let p = 1; p < strokeGroup.points.length; p += 1) { - currentX += strokeGroup.points[p][0]; - currentY += strokeGroup.points[p][1]; - this.ctx.lineTo(currentX, currentY); - } - this.ctx.stroke(); - this.ctx.closePath(); + drawGroups() { + this.setGlobalStyles(); + for (let g = 0; g < this.frame.strokeGroups.length; g += 1) { + let strokeGroup = this.frame.strokeGroups[g]; + let currentX = strokeGroup.points[0][0]; + let currentY = strokeGroup.points[0][1]; + this.setGroupStyles(strokeGroup); + if (strokeGroup.points.length === 1) { + // Draw a circle + this.ctx.fillStyle = strokeGroup.styles + ? strokeGroup.styles.strokeStyle + : this.frame.styles.strokeStyle; + this.ctx.beginPath(); + this.ctx.arc( + currentX, + currentY, + (strokeGroup.styles + ? strokeGroup.styles.lineWidth + : this.frame.styles.lineWidth) / 2, + 0, + Math.PI * 2, + false, + ); + this.ctx.fill(); + this.ctx.closePath(); + this.ctx.fillStyle = "transparent"; + } else { + this.ctx.beginPath(); + this.ctx.moveTo(currentX, currentY); + for (let p = 1; p < strokeGroup.points.length; p += 1) { + currentX += strokeGroup.points[p][0]; + currentY += strokeGroup.points[p][1]; + this.ctx.lineTo(currentX, currentY); } + this.ctx.stroke(); + this.ctx.closePath(); } } + } - view({attrs: {className, width = FrameComponent.width, height = FrameComponent.height}}) { - return m('canvas', { - class: className, - width, - height - }); - } - + view({ + attrs: { + className, + width = FrameComponent.width, + height = FrameComponent.height, + }, + }) { + return ; + } } FrameComponent.width = 1600; FrameComponent.height = 900; diff --git a/scripts/components/import.jsx b/scripts/components/import.jsx index 0f75bc7..06163f9 100644 --- a/scripts/components/import.jsx +++ b/scripts/components/import.jsx @@ -1,12 +1,11 @@ -import m from 'mithril'; -import Story from '../models/story.js'; -import ControlComponent from './control.jsx'; -import PanelComponent from './panel.jsx'; -import ProgressBarComponent from './progress-bar.jsx'; +import m from "mithril"; +import Story from "../models/story.js"; +import ControlComponent from "./control.jsx"; +import PanelComponent from "./panel.jsx"; +import ProgressBarComponent from "./progress-bar.jsx"; class ImportComponent { - - oninit({attrs: {app}}) { + oninit({ attrs: { app } }) { this.app = app; this.chosenFile = null; this.uploadProgress = null; @@ -49,33 +48,50 @@ class ImportComponent { } view() { - return m('div.import-options', [ - m('h2', 'Import Story'), - m('div.import-control-wrapper', [ - m('input[type=file][accept=.flipbook].import-file-input', { - onchange: ({target}) => this.setChosenFile(target.files[0]) - }), - m(ControlComponent, { - id: 'choose-file', - title: 'Choose File', - label: 'Choose File...' - }), - m('div.import-chosen-file-name', this.chosenFile ? this.chosenFile.name : 'No file chosen') - ]), - m('div.import-footer', [ - this.storyAdded ? m('div.import-success-message', `"${this.storyAdded.metadata.name}" successfully added!`) : - [ - this.uploadProgress === null ? this.chosenFile ? m(ControlComponent, { - id: 'upload-file', - title: 'Upload Now', - label: 'Upload Now', - action: () => this.uploadChosenFile() - }) : null : m(ProgressBarComponent, {progress: this.uploadProgress}) - ] - ]) - ]); + return ( +
+

Import Story

+
+ this.setChosenFile(target.files[0])} + /> + +
+ {this.chosenFile ? this.chosenFile.name : "No file chosen"} +
+
+
+ {this.storyAdded ? ( +
+ "{this.storyAdded.metadata.name}" successfully added! +
+ ) : ( + [ + this.uploadProgress === null ? ( + this.chosenFile ? ( + this.uploadChosenFile()} + /> + ) : null + ) : ( + + ), + ] + )} +
+
+ ); } - } export default ImportComponent; diff --git a/scripts/components/loading.jsx b/scripts/components/loading.jsx index 5b8e89b..47e45ac 100644 --- a/scripts/components/loading.jsx +++ b/scripts/components/loading.jsx @@ -1,12 +1,12 @@ -import m from 'mithril'; - class LoadingComponent { view({ attrs }) { - return m('div.loading', attrs, [ - m('svg[viewBox="0 0 24 24"]', [ - m('path', { d: 'M 3,12 A 6,6 0,0,0 21,12' }) - ]) - ]); + return ( +
+ + + +
+ ); } } diff --git a/scripts/components/modal.jsx b/scripts/components/modal.jsx index fe0d9b4..b3b68c1 100644 --- a/scripts/components/modal.jsx +++ b/scripts/components/modal.jsx @@ -1,20 +1,22 @@ -import m from 'mithril'; -import OverlayComponent from './overlay.jsx'; +import OverlayComponent from "./overlay.jsx"; class ModalComponent { - view(vnode) { - return [ - m(OverlayComponent, { - type: 'modal', - onDismiss: () => { - /* do nothing */ - } - }), - m('div.modal', vnode.attrs, vnode.children) - ]; + return ( + <> + { + /* do nothing */ + }} + /> + , +
+ {vnode.children} +
+ + ); } - } export default ModalComponent; diff --git a/scripts/components/overlay.jsx b/scripts/components/overlay.jsx index 638006c..b8d4c42 100644 --- a/scripts/components/overlay.jsx +++ b/scripts/components/overlay.jsx @@ -1,8 +1,6 @@ -import m from 'mithril'; - class OverlayComponent { view({ attrs: { type, onDismiss } }) { - return m(`div.${type}-overlay`, { onclick: () => onDismiss() }); + return
onDismiss()} />; } } diff --git a/scripts/components/panel.jsx b/scripts/components/panel.jsx index e854bd8..8a28460 100644 --- a/scripts/components/panel.jsx +++ b/scripts/components/panel.jsx @@ -1,28 +1,29 @@ -import m from 'mithril'; -import OverlayComponent from './overlay'; +import OverlayComponent from "./overlay"; class PanelComponent { - - view({attrs: {id, position, dismissable = true}, children}) { - return PanelComponent.panelIsOpen(id) ? [ - m(OverlayComponent, { - type: 'panel', - onDismiss: () => { - if (dismissable) { - PanelComponent.closeAllPanels(); - } - } - }), - m(`div.panel.panel-${id}.panel-position-${position}`, children) - ] : null; + view({ attrs: { id, position, dismissable = true }, children }) { + return PanelComponent.panelIsOpen(id) + ? [ + { + if (dismissable) { + PanelComponent.closeAllPanels(); + } + }} + />, +
+ {children} +
, + ] + : null; } - } // Only one panel can be open at a time PanelComponent.currentlyOpenPanel = null; PanelComponent.panelIsOpen = (id) => { - return (PanelComponent.currentlyOpenPanel === id); + return PanelComponent.currentlyOpenPanel === id; }; PanelComponent.panelIsClosed = (id) => { return !PanelComponent.panelIsOpen(id); diff --git a/scripts/components/progress-bar.jsx b/scripts/components/progress-bar.jsx index d1c8376..e444af2 100644 --- a/scripts/components/progress-bar.jsx +++ b/scripts/components/progress-bar.jsx @@ -1,17 +1,18 @@ -import m from 'mithril'; -import clsx from 'clsx'; +import clsx from "clsx"; class ProgressBarComponent { - - view({attrs: {progress = 0}}) { - return m('div.progress-bar', [ - m('div', { - class: clsx('progress-bar-current-progress', {'no-progress': progress === 0}), - style: {width: `${progress * 100}%`} - }) - ]); + view({ attrs: { progress = 0 } }) { + return ( +
+
+
+ ); } - } ProgressBarComponent.delay = 500; diff --git a/scripts/components/settings.jsx b/scripts/components/settings.jsx index 9169988..5f09473 100644 --- a/scripts/components/settings.jsx +++ b/scripts/components/settings.jsx @@ -1,20 +1,24 @@ -import m from 'mithril'; -import ControlComponent from './control.jsx'; +import ControlComponent from "./control.jsx"; class SettingsComponent { - async setFrameDuration(story, framesPerSecond) { story.setFramesPerSecond(Number(framesPerSecond)); await story.save(); } async incrementNumPreviousFramesToShow(story) { - story.numPreviousFramesToShow = Math.min(story.numPreviousFramesToShow + 1, 4); + story.numPreviousFramesToShow = Math.min( + story.numPreviousFramesToShow + 1, + 4, + ); await story.save(); } async decrementNumPreviousFramesToShow(story) { - story.numPreviousFramesToShow = Math.max(0, story.numPreviousFramesToShow - 1); + story.numPreviousFramesToShow = Math.max( + 0, + story.numPreviousFramesToShow - 1, + ); await story.save(); } @@ -23,44 +27,57 @@ class SettingsComponent { await story.save(); } - view({attrs: {story}}) { - return m('div.settings', [ - m('h2', 'Settings'), - m('div.setting', [ - m('label[for="setting-fps"]', 'FPS'), - m('input[type=range][min=2][max=30][step=2]#setting-fps', { - value: story.getFramesPerSecond(), - oninput: ({target}) => this.setFrameDuration(story, target.value) - }), - m('span.setting-value', story.getFramesPerSecond()) - ]), - m('div.setting', [ - m('label', 'Previous Frames to Show'), - m(ControlComponent, { - id: 'decrement-previous-frames', - title: 'Show One Less Previous Frame', - icon: 'decrement', - action: () => this.decrementNumPreviousFramesToShow(story) - }), - m('span.setting-value.setting-value-previous-frames', story.numPreviousFramesToShow), - m(ControlComponent, { - id: 'increment-previous-frames', - title: 'Show One More Previous Frame', - icon: 'increment', - action: () => this.incrementNumPreviousFramesToShow(story) - }) - ]), - m('div.setting', [ - m('label[for="setting-stroke-width"]', 'Stroke Width'), - m('input[type=range][min=4][max=20][step=4]#setting-stroke-width', { - value: story.frameStyles.lineWidth, - oninput: ({target}) => this.setLineWidth(story, target.value) - }), - m('span.setting-value', story.frameStyles.lineWidth) - ]) - ]); + view({ attrs: { story } }) { + return ( +
+

Settings

+
+ + this.setFrameDuration(story, target.value)} + /> + {story.getFramesPerSecond()} +
+
+ + this.decrementNumPreviousFramesToShow(story)} + /> + + {story.numPreviousFramesToShow} + + this.incrementNumPreviousFramesToShow(story)} + /> +
+
+ + this.setLineWidth(story, target.value)} + /> + {story.frameStyles.lineWidth} +
+
+ ); } - } export default SettingsComponent; diff --git a/scripts/components/storage-upgrader.jsx b/scripts/components/storage-upgrader.jsx index 78b23b1..9510bfc 100644 --- a/scripts/components/storage-upgrader.jsx +++ b/scripts/components/storage-upgrader.jsx @@ -1,9 +1,9 @@ -import m from 'mithril'; -import DismissableOverlayComponent from './overlay.jsx'; -import LoadingComponent from './loading.jsx'; -import StorageUpgrader from '../models/storage-upgrader.js'; -import ModalComponent from './modal.jsx'; -import _ from 'underscore'; +import m from "mithril"; +import DismissableOverlayComponent from "./overlay.jsx"; +import LoadingComponent from "./loading.jsx"; +import StorageUpgrader from "../models/storage-upgrader.js"; +import ModalComponent from "./modal.jsx"; +import _ from "underscore"; class StorageUpgraderComponent { oninit() { @@ -43,27 +43,17 @@ class StorageUpgraderComponent { } view() { - return this.isVisible - ? m( - 'div.storage-upgrader', - { - oncreate: this.blurEditor - }, - [ - - m(ModalComponent, [ - m('h2.storage-upgrader-heading', 'Upgrading Database...'), - - m( - 'p.storage-upgrader-message', - 'Hang tight while we upgrade the database...' - ), - - m(LoadingComponent, { class: 'storage-upgrader-loading' }) - ]) - ] - ) - : null; + return this.isVisible ? ( +
+ +

Upgrading Database...

+

+ Hang tight while we upgrade the database... +

+ +
+
+ ) : null; } } diff --git a/scripts/components/story-controls.jsx b/scripts/components/story-controls.jsx index 8aba9ae..a09dcf8 100644 --- a/scripts/components/story-controls.jsx +++ b/scripts/components/story-controls.jsx @@ -1,11 +1,10 @@ -import m from 'mithril'; -import ControlComponent from './control.jsx'; -import SettingsComponent from './settings.jsx'; -import ExportComponent from './export.jsx'; -import TimelineComponent from './timeline.jsx'; +import m from "mithril"; +import ControlComponent from "./control.jsx"; +import SettingsComponent from "./settings.jsx"; +import ExportComponent from "./export.jsx"; +import TimelineComponent from "./timeline.jsx"; class StoryControlsComponent { - async skipToFirstFrame(story) { story.selectFrame(0); await story.save(); @@ -35,7 +34,11 @@ class StoryControlsComponent { // this will allow Mithril to redraw and close all panels before showing the // modal setTimeout(async () => { - if (!confirm('Are you sure you want to delete this frame? This cannot be undone.')) { + if ( + !confirm( + "Are you sure you want to delete this frame? This cannot be undone.", + ) + ) { return; } story.deleteSelectedFrame(); @@ -54,90 +57,83 @@ class StoryControlsComponent { await story.save(); } - view({attrs: {story}}) { - return m('div.story-controls', [ - - m('div.control-group', [ - m(ControlComponent, { - id: 'settings', - title: 'Settings', - icon: 'settings', - panel: m(SettingsComponent, {story}) - }), - m(ControlComponent, { - id: 'export', - title: 'Export', - icon: 'save', - panel: m(ExportComponent, {story}) - }) - ]), - - m('div.control-group', [ - m(ControlComponent, { - id: 'skip-to-first-frame', - title: 'Skip to First Frame', - icon: 'skip-previous', - action: () => this.skipToFirstFrame(story) - }), - m(ControlComponent, { - id: 'play-story', - title: 'Play Story', - icon: 'play', - action: () => this.playStory(story) - }), - m(ControlComponent, { - id: 'pause-story', - title: 'Pause Story', - icon: 'pause', - action: () => this.pauseStory(story) - }) - ]), - - m('div.control-group.timeline-control-group', [ - - m('div.control-group', [ - m(ControlComponent, { - id: 'add-frame', - title: 'Add Frame', - icon: 'add', - action: () => this.addNewFrame(story) - }), - m(ControlComponent, { - id: 'duplicate-frame', - title: 'Duplicate Current Frame', - icon: 'duplicate', - action: () => this.duplicateCurrentFrame(story) - }), - m(ControlComponent, { - id: 'delete-frame', - title: 'Delete Frame', - icon: 'remove', - action: () => this.deleteSelectedFrame(story) - }) - ]), - - m(TimelineComponent, {story}) - - ]), - - m('div.control-group', [ - m(ControlComponent, { - id: 'undo-stroke', - title: 'Undo Stroke', - icon: 'undo', - action: () => this.undo(story) - }), - m(ControlComponent, { - id: 'redo-stroke', - title: 'Redo Stroke', - icon: 'redo', - action: () => this.redo(story) - }) - ]) - - ]); + view({ attrs: { story } }) { + return ( +
+
+ } + /> + } + /> +
+
+ this.skipToFirstFrame(story)} + /> + this.playStory(story)} + /> + this.pauseStory(story)} + /> +
+
+
+ this.addNewFrame(story)} + /> + this.duplicateCurrentFrame(story)} + /> + this.deleteSelectedFrame(story)} + /> +
+ +
+
+ this.undo(story)} + /> + this.redo(story)} + /> +
+
+ ); } - } export default StoryControlsComponent; diff --git a/scripts/components/story-editor.jsx b/scripts/components/story-editor.jsx index 00afe05..2ef36f9 100644 --- a/scripts/components/story-editor.jsx +++ b/scripts/components/story-editor.jsx @@ -1,37 +1,37 @@ -import m from 'mithril'; -import clsx from 'clsx'; -import FrameComponent from './frame.jsx'; -import DrawingAreaComponent from './drawing-area.jsx'; -import StoryControlsComponent from './story-controls.jsx'; +import clsx from "clsx"; +import FrameComponent from "./frame.jsx"; +import DrawingAreaComponent from "./drawing-area.jsx"; +import StoryControlsComponent from "./story-controls.jsx"; class StoryEditorComponent { - - view({attrs: {story}}) { - return m('div', { - class: clsx('story-editor', {'story-playing': story.playing}) - }, [ - - m('div.story-stage', [ - story.selectedFrameIndex > 0 && story.numPreviousFramesToShow > 0 ? story.getPreviousFramesToShow().map((previousFrame, p, previousFramesToShow) => { - return m(FrameComponent, { - className: `previous-frame previous-frame-${previousFramesToShow.length - p}`, - key: `previous-frame-${previousFrame.temporaryId}`, - frame: previousFrame - }); - }) : null, - m(DrawingAreaComponent, { - className: 'selected-frame', - story, - frame: story.getSelectedFrame(), - drawingEnabled: !story.playing - }) - ]), - - m(StoryControlsComponent, {story}) - - ]); + view({ attrs: { story } }) { + return ( +
+
+ {story.selectedFrameIndex > 0 && story.numPreviousFramesToShow > 0 + ? story + .getPreviousFramesToShow() + .map((previousFrame, p, previousFramesToShow) => { + return ( + + ); + }) + : null} + +
+ +
+ ); } - } export default StoryEditorComponent; diff --git a/scripts/components/story-header.jsx b/scripts/components/story-header.jsx index 048fb8e..d736624 100644 --- a/scripts/components/story-header.jsx +++ b/scripts/components/story-header.jsx @@ -1,12 +1,11 @@ -import m from 'mithril'; -import ControlComponent from './control.jsx'; -import PanelComponent from './panel.jsx'; -import StoryListComponent from './story-list.jsx'; -import ImportComponent from './import.jsx'; +import m from "mithril"; +import ControlComponent from "./control.jsx"; +import PanelComponent from "./panel.jsx"; +import StoryListComponent from "./story-list.jsx"; +import ImportComponent from "./import.jsx"; class StoryHeaderComponent { - - oninit({attrs: {app}}) { + oninit({ attrs: { app } }) { this.app = app; } @@ -15,7 +14,7 @@ class StoryHeaderComponent { // will allow Mithril to redraw and close all panels before showing the // modal setTimeout(async () => { - let storyName = prompt('Please enter a name for your new story:') || ''; + let storyName = prompt("Please enter a name for your new story:") || ""; if (storyName.trim()) { await this.app.createNewStoryWithName(storyName.trim()); PanelComponent.closeAllPanels(); @@ -29,7 +28,11 @@ class StoryHeaderComponent { // this will allow Mithril to redraw and close all panels before showing the // modal setTimeout(async () => { - let newStoryName = prompt('Enter the new name for your story:', await this.app.getSelectedStoryName()) || ''; + let newStoryName = + prompt( + "Enter the new name for your story:", + await this.app.getSelectedStoryName(), + ) || ""; if (newStoryName.trim()) { await this.app.renameSelectedStory(newStoryName.trim()); m.redraw(); @@ -42,63 +45,68 @@ class StoryHeaderComponent { // this will allow Mithril to redraw and close all panels before showing the // modal setTimeout(() => { - if (confirm('Are you sure you want to delete this story? This cannot be undone.')) { + if ( + confirm( + "Are you sure you want to delete this story? This cannot be undone.", + ) + ) { this.app.deleteSelectedStory(); m.redraw(); } }); } - view({attrs: {story}}) { - return m('div.story-header', [ - m('.control-group', [ - m(ControlComponent, { - id: 'create-new-story', - title: 'Create New Story', - icon: 'add', - action: () => this.createNewStoryWithName() - }), - m(ControlComponent, { - id: 'open-story', - title: 'Open Story', - icon: 'folder', - panel: m(StoryListComponent, {app: this.app}), - panelPosition: 'bottom' - }), - m(ControlComponent, { - id: 'import-story', - title: 'Import Story', - icon: 'upload', - panel: m(ImportComponent, {app: this.app}), - panelPosition: 'bottom' - }) - ]), - m('span.selected-story-name', story.metadata.name), - m(ControlComponent, { - id: 'rename-story', - title: 'Rename Story', - icon: 'edit', - action: () => this.renameSelectedStory() - }), - m('.control-group', [ - m(ControlComponent, { - id: 'delete-story', - title: 'Delete Story', - icon: 'delete', - action: () => this.deleteSelectedStory() - }), - m(ControlComponent, { - id: 'help', - title: 'Help', - icon: 'help', - action: () => { - window.open('https://github.com/caleb531/flip-book#how-to-use'); - } - }) - ]) - ]); + view({ attrs: { story } }) { + return ( +
+
+ this.createNewStoryWithName()} + /> + } + panelPosition="bottom" + /> + } + panelPosition="bottom" + /> +
+ {story.metadata.name} + this.renameSelectedStory()} + /> +
+ this.deleteSelectedStory()} + /> + { + window.open("https://github.com/caleb531/flip-book#how-to-use"); + }} + /> +
+
+ ); } - } export default StoryHeaderComponent; diff --git a/scripts/components/story-list.jsx b/scripts/components/story-list.jsx index 9fa7678..8dcad5e 100644 --- a/scripts/components/story-list.jsx +++ b/scripts/components/story-list.jsx @@ -1,9 +1,7 @@ -import m from 'mithril'; -import PanelComponent from './panel.jsx'; +import PanelComponent from "./panel.jsx"; class StoryListComponent { - - oninit({attrs: {app}}) { + oninit({ attrs: { app } }) { this.app = app; } @@ -14,18 +12,26 @@ class StoryListComponent { } view() { - return m('div.story-list-container', [ - m('h2', 'Story List'), - m('ul.story-list', { - onclick: ({target}) => this.selectStory(target.closest('.story-list-item')) - }, this.app.stories.map((storyMetadata, s) => { - return m('li.story-list-item', { - 'data-index': s - }, storyMetadata.name); - })) - ]); + return ( +
+

Story List

+
    + this.selectStory(target.closest(".story-list-item")) + } + > + {this.app.stories.map((storyMetadata, s) => { + return ( +
  • + {storyMetadata.name} +
  • + ); + })} +
+
+ ); } - } export default StoryListComponent; diff --git a/scripts/components/story.jsx b/scripts/components/story.jsx index c729768..4ebdf4a 100644 --- a/scripts/components/story.jsx +++ b/scripts/components/story.jsx @@ -1,16 +1,15 @@ -import m from 'mithril'; -import StoryHeaderComponent from './story-header.jsx'; -import StoryEditorComponent from './story-editor.jsx'; +import StoryHeaderComponent from "./story-header.jsx"; +import StoryEditorComponent from "./story-editor.jsx"; class StoryComponent { - - view({attrs: {app, story}}) { - return m('div.story', [ - m(StoryHeaderComponent, {app, story}), - m(StoryEditorComponent, {story}) - ]); + view({ attrs: { app, story } }) { + return ( +
+ + +
+ ); } - } export default StoryComponent; diff --git a/scripts/components/timeline.jsx b/scripts/components/timeline.jsx index cc24b43..1d4c317 100644 --- a/scripts/components/timeline.jsx +++ b/scripts/components/timeline.jsx @@ -1,12 +1,10 @@ -import m from 'mithril'; -import clsx from 'clsx'; -import FrameComponent from './frame.jsx'; +import clsx from "clsx"; +import FrameComponent from "./frame.jsx"; const FRAME_THUMBNAIL_WIDTH = 128; const FRAME_THUMBNAIL_HEIGHT = 72; class TimelineComponent { - async selectThumbnail(target, story) { if (target.dataset.index) { story.selectFrame(Number(target.dataset.index)); @@ -16,12 +14,13 @@ class TimelineComponent { } scrollSelectedThumbnailIntoView(thumbnailElement) { - if (thumbnailElement.classList.contains('selected')) { + if (thumbnailElement.classList.contains("selected")) { let timelineElement = thumbnailElement.parentElement; let scrollLeft = timelineElement.scrollLeft; let scrollRight = scrollLeft + timelineElement.offsetWidth; let offsetLeft = thumbnailElement.offsetLeft; - let offsetRight = thumbnailElement.offsetLeft + thumbnailElement.offsetWidth; + let offsetRight = + thumbnailElement.offsetLeft + thumbnailElement.offsetWidth; // If the visible timeline is not wide enough to show a full thumbnail, // do nothing @@ -62,7 +61,7 @@ class TimelineComponent { handleFrameDragover(event) { event.preventDefault(); - event.dataTransfer.dropEffect = 'move'; + event.dataTransfer.dropEffect = "move"; event.redraw = false; } @@ -72,33 +71,40 @@ class TimelineComponent { // (simply by virtue of the event listener existing) } - view({attrs: {story}}) { - return m('ol.timeline', { - onclick: ({target}) => this.selectThumbnail(target, story), - ondragstart: (event) => this.handleFrameDragstart(event), - ondragover: (event) => this.handleFrameDragover(event, story), - ondragenter: (event) => this.handleFrameDragenter(event, story), - ondrop: (event) => this.handleFrameDrop(event, story) - }, story.frames.map((frame, f) => { - return m('li', { - draggable: true, - // Keying each thumbnail prevents the canvas redraws from compounding - key: `timeline-thumbnail-${frame.temporaryId}`, - // Scroll newly-added frames into view - oncreate: ({dom}) => this.scrollSelectedThumbnailIntoView(dom), - // Scroll selected frame into view when navigating frames (Prev/Next) - onupdate: ({dom}) => this.scrollSelectedThumbnailIntoView(dom), - class: clsx('timeline-thumbnail', {'selected': story.selectedFrameIndex === f}), - 'data-index': f - }, m(FrameComponent, { - className: 'timeline-thumbnail-canvas', - frame, - width: FRAME_THUMBNAIL_WIDTH, - height: FRAME_THUMBNAIL_HEIGHT - })); - })); + view({ attrs: { story } }) { + return ( +
    this.selectThumbnail(target, story)} + ondragstart={(event) => this.handleFrameDragstart(event)} + ondragover={(event) => this.handleFrameDragover(event, story)} + ondragenter={(event) => this.handleFrameDragenter(event, story)} + ondrop={(event) => this.handleFrameDrop(event, story)} + > + {story.frames.map((frame, f) => { + return ( +
  1. this.scrollSelectedThumbnailIntoView(dom)} + onupdate={({ dom }) => this.scrollSelectedThumbnailIntoView(dom)} + className={clsx("timeline-thumbnail", { + selected: story.selectedFrameIndex === f, + })} + data-index={f} + > + +
  2. + ); + })} +
+ ); } - } export default TimelineComponent; diff --git a/scripts/components/update-notification.jsx b/scripts/components/update-notification.jsx index e20a583..031896e 100644 --- a/scripts/components/update-notification.jsx +++ b/scripts/components/update-notification.jsx @@ -1,16 +1,18 @@ -import m from 'mithril'; -import clsx from 'clsx'; -import { registerSW } from 'virtual:pwa-register'; +import m from "mithril"; +import clsx from "clsx"; +import { registerSW } from "virtual:pwa-register"; class UpdateNotificationComponent { - // Use Vite PWA plugin to manage service worker updates (source: // ) oninit() { if (!navigator.serviceWorker) { return; } - if (window.location.hostname === 'localhost' && !sessionStorage.getItem('sw')) { + if ( + window.location.hostname === "localhost" && + !sessionStorage.getItem("sw") + ) { return; } this.isUpdateAvailable = false; @@ -18,7 +20,7 @@ class UpdateNotificationComponent { onNeedRefresh: () => { this.isUpdateAvailable = true; m.redraw(); - } + }, }); } @@ -29,14 +31,19 @@ class UpdateNotificationComponent { } view() { - return m('div', { - class: clsx('update-notification', { 'update-available': this.isUpdateAvailable }), - onclick: () => this.update() - }, [ - m('span.update-notification-message', 'Update available! Click here to update.') - ]); + return ( +
this.update()} + > + + Update available! Click here to update. + +
+ ); } - } export default UpdateNotificationComponent;