diff --git a/frontend/index.html b/frontend/index.html index 7067084..30519e2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,7 +6,6 @@ - @@ -23,34 +22,61 @@ +
+ + + + + + +
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ this project on GitHub +
+ +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bf4fa1f..2481208 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "chunk": "^0.0.3", "ofetch": "^1.4.1", "omggif": "^1.0.10", + "reset-css": "^5.0.2", "typescript": "~5.6.2", "vite": "^6.0.5" } @@ -945,6 +946,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/reset-css": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/reset-css/-/reset-css-5.0.2.tgz", + "integrity": "sha512-YtgUGSq5z5W0NPSjsBW7ys7rtWa8P8AiE7S6Fg3d1TQCPpAodgYyLuZYlU0AOsLtprk/fC9ormHN/0pAavVIDw==", + "dev": true + }, "node_modules/rollup": { "version": "4.30.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", @@ -1063,9 +1070,9 @@ "peer": true }, "node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", + "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", "dev": true, "dependencies": { "esbuild": "^0.24.2", diff --git a/frontend/package.json b/frontend/package.json index 1360d52..6d61c15 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,8 @@ "chunk": "^0.0.3", "ofetch": "^1.4.1", "omggif": "^1.0.10", + "reset-css": "^5.0.2", "typescript": "~5.6.2", - "vite": "^6.0.5" + "vite": "^6.0.11" } } diff --git a/frontend/remote.html b/frontend/remote.html index b346a22..2e62818 100644 --- a/frontend/remote.html +++ b/frontend/remote.html @@ -22,6 +22,7 @@ diff --git a/frontend/src/consts.ts b/frontend/src/consts.ts index 4d3744b..8b1461c 100644 --- a/frontend/src/consts.ts +++ b/frontend/src/consts.ts @@ -20,3 +20,10 @@ export const BASIC_POLL_DELAY = 10; export const LOCALSTORAGE_SCALE_KEY = 'pico-printer-save-scale'; export const LOCALSTORAGE_FPS_KEY = 'pico-printer-save-fps'; +export const LOCALSTORAGE_GIF_DIR_KEY = 'pico-printer-gif-direction'; + +export enum Direction { + FORWARD = 'fwd', + REVERSE = 'rev', + YOYO = 'yoyo', +} diff --git a/frontend/src/functions/canvas/imageDatasToBlob.ts b/frontend/src/functions/canvas/imageDatasToBlob.ts index a5d0411..5662543 100644 --- a/frontend/src/functions/canvas/imageDatasToBlob.ts +++ b/frontend/src/functions/canvas/imageDatasToBlob.ts @@ -1,5 +1,6 @@ import chunk from 'chunk'; import { GifWriter } from 'omggif'; +import { showToast } from '../settings/toast.ts'; export interface GifFrameData { palette: number[], @@ -50,7 +51,7 @@ export const imageDatasToBlob = (frames: ImageData[], fps: number): Blob => { if (frameCount !== frames.length) { const msg = 'Some frames in your image contain more than 256 colors, which makes creating a GIF impossible'; - alert(msg); + showToast(msg); throw new Error(msg); } diff --git a/frontend/src/functions/gallery/buttons.ts b/frontend/src/functions/gallery/buttons.ts index 6f66e4d..b9c0901 100644 --- a/frontend/src/functions/gallery/buttons.ts +++ b/frontend/src/functions/gallery/buttons.ts @@ -1,6 +1,7 @@ import chunk from 'chunk'; -import { LOCALSTORAGE_FPS_KEY, LOCALSTORAGE_SCALE_KEY } from '../../consts.ts'; +import { Direction, LOCALSTORAGE_FPS_KEY, LOCALSTORAGE_GIF_DIR_KEY } from '../../consts.ts'; import { imageDatasToBlob } from '../canvas/imageDatasToBlob.ts'; +import { showToast } from '../settings/toast.ts'; import { DataType, DbAccess } from '../storage/database.ts'; import { sortBySelectionOrder, updateSelectionOrder } from './selectionOrder.ts'; @@ -10,8 +11,7 @@ const selectAllBtn = document.getElementById("select_all_btn") as HTMLButtonElem const averageSelectedBtn = document.getElementById("average_selected_btn") as HTMLButtonElement; const gifSelectedBtn = document.getElementById("gif_selected_btn") as HTMLButtonElement; const rgbSelectedBtn = document.getElementById("rgb_selected_btn") as HTMLButtonElement; -const scaleSelect = document.getElementById("download_size") as HTMLSelectElement; -const fpsSelect = document.getElementById("download_fps") as HTMLSelectElement; + export const updateButtons = () => { const numSelectedItems = document.querySelectorAll('.marked-for-action').length; @@ -21,9 +21,6 @@ export const updateButtons = () => { averageSelectedBtn.disabled = numSelectedItems < 2 || numSelectedItemsFinal !== 0; gifSelectedBtn.disabled = numSelectedItems < 2 || numSelectedItemsFinal !== 0; rgbSelectedBtn.disabled = numSelectedItems !== 3 || numSelectedItemsFinal !== 0; - - scaleSelect.value = localStorage.getItem(LOCALSTORAGE_SCALE_KEY) || '1'; - fpsSelect.value = localStorage.getItem(LOCALSTORAGE_FPS_KEY) || '12'; } interface Dimensions { @@ -112,7 +109,7 @@ export const initButtons = (store: DbAccess) => { const dimensions = getCommonSize(items); if (!dimensions) { - alert("Image dimensions must be the same to create an average"); + showToast('Image dimensions must be the same for all selected images to create an average'); return; } @@ -159,6 +156,7 @@ export const initButtons = (store: DbAccess) => { gifSelectedBtn.addEventListener('click', async () => { const items = [...gallery.querySelectorAll('.marked-for-action')] as HTMLDivElement[]; const fps = parseInt(localStorage.getItem(LOCALSTORAGE_FPS_KEY) || '12', 10); + const dir = localStorage.getItem(LOCALSTORAGE_GIF_DIR_KEY) as Direction || Direction.FORWARD; if (items.length < 2) { return; @@ -171,7 +169,7 @@ export const initButtons = (store: DbAccess) => { const dimensions = getCommonSize(images); if (!dimensions) { - alert("Image dimensions must be the same to create an animation"); + showToast('Image dimensions must be the same for all selected images to create an animation'); return; } @@ -184,7 +182,27 @@ export const initButtons = (store: DbAccess) => { return ctx.getImageData(0, 0, canvas.width, canvas.height); }); - unselectAll(); + switch (dir) { + case Direction.FORWARD: + break; + + case Direction.REVERSE: + frames.reverse(); + break; + + case Direction.YOYO: { + console.log(frames); + if (frames.length > 2) { + frames.push(...frames.slice(1, -1).reverse()); + } + console.log(frames); + break; + } + + default: + break; + } + const timestamp = Date.now(); store.add({ @@ -192,6 +210,8 @@ export const initButtons = (store: DbAccess) => { timestamp, data: imageDatasToBlob(frames, fps), }); + + unselectAll(); }); rgbSelectedBtn.addEventListener('click', async () => { @@ -208,7 +228,7 @@ export const initButtons = (store: DbAccess) => { const dimensions = getCommonSize(images); if (!dimensions) { - alert("Image dimensions must be the same to create a RGB image"); + showToast('Image dimensions must be the same for all selected images to create a RGB image'); return; } @@ -248,16 +268,5 @@ export const initButtons = (store: DbAccess) => { unselectAll(); }); - - scaleSelect.addEventListener('change', () => { - const scale = parseInt(scaleSelect.value || '0', 10) || 1; - localStorage.setItem(LOCALSTORAGE_SCALE_KEY, scale.toString(10)); - }); - - fpsSelect.addEventListener('change', () => { - const fps = parseInt(fpsSelect.value || '0', 10) || 12; - localStorage.setItem(LOCALSTORAGE_FPS_KEY, fps.toString(10)); - }); - updateButtons(); } diff --git a/frontend/src/functions/settings/index.ts b/frontend/src/functions/settings/index.ts new file mode 100644 index 0000000..895908b --- /dev/null +++ b/frontend/src/functions/settings/index.ts @@ -0,0 +1,53 @@ +import { LOCALSTORAGE_GIF_DIR_KEY, LOCALSTORAGE_FPS_KEY, LOCALSTORAGE_SCALE_KEY, Direction } from '../../consts.ts'; + +const settingsMenu = document.getElementById('settings') as HTMLDivElement; +const settingsBackdrop = document.getElementById('settings_backdrop') as HTMLButtonElement; +const settingsCloseBtn = document.getElementById('settings_close') as HTMLButtonElement; +const scaleSelect = document.getElementById('download_size') as HTMLSelectElement; +const fpsSelect = document.getElementById('download_fps') as HTMLSelectElement; +const gifDirection = document.getElementById('gif_direction') as HTMLSelectElement; +const settingsBtn = document.getElementById('open_settings') as HTMLButtonElement; + +const updateSettings = () => { + scaleSelect.value = localStorage.getItem(LOCALSTORAGE_SCALE_KEY) || '1'; + fpsSelect.value = localStorage.getItem(LOCALSTORAGE_FPS_KEY) || '12'; + gifDirection.value = localStorage.getItem(LOCALSTORAGE_GIF_DIR_KEY) || Direction.FORWARD; +} + +export const initSettings = () => { + updateSettings(); + + scaleSelect.addEventListener('change', () => { + const scale = parseInt(scaleSelect.value || '0', 10) || 1; + localStorage.setItem(LOCALSTORAGE_SCALE_KEY, scale.toString(10)); + }); + + fpsSelect.addEventListener('change', () => { + const fps = parseInt(fpsSelect.value || '0', 10) || 12; + localStorage.setItem(LOCALSTORAGE_FPS_KEY, fps.toString(10)); + }); + + gifDirection.addEventListener('change', () => { + const dir = gifDirection.value || Direction.FORWARD; + localStorage.setItem(LOCALSTORAGE_GIF_DIR_KEY, dir); + }); + + + settingsBtn.addEventListener('click', () => { + document.body.classList.add('fixed'); + settingsMenu.classList.add('visible'); + settingsBackdrop.classList.add('visible'); + }); + + settingsBackdrop.addEventListener('click', () => { + document.body.classList.remove('fixed'); + settingsMenu.classList.remove('visible'); + settingsBackdrop.classList.remove('visible'); + }); + + settingsCloseBtn.addEventListener('click', () => { + document.body.classList.remove('fixed'); + settingsMenu.classList.remove('visible'); + settingsBackdrop.classList.remove('visible'); + }); +} diff --git a/frontend/src/functions/settings/toast.ts b/frontend/src/functions/settings/toast.ts new file mode 100644 index 0000000..260f95f --- /dev/null +++ b/frontend/src/functions/settings/toast.ts @@ -0,0 +1,17 @@ +const toastTarget = document.querySelector('.toast-target') as HTMLDivElement; + +export const showToast = (message: string) => { + const toast = document.createElement('div'); + toast.classList.add('toast'); + toast.innerText = message; + toastTarget.appendChild(toast); + + const closeTimeout = setTimeout(() => { + toastTarget.removeChild(toast); + }, 10000); + + toast.addEventListener('click', () => { + clearTimeout(closeTimeout); + toastTarget.removeChild(toast); + }) +} diff --git a/frontend/src/index.ts b/frontend/src/index.ts index d65ebbf..14984a7 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,8 +1,12 @@ import { initGallery } from './functions/gallery'; +import { initSettings } from './functions/settings'; import { initDb } from './functions/storage/database.ts'; import { startPolling } from './functions/pollLoop.ts'; import { webappConnect } from './functions/remote/webappConnect.ts'; +import 'reset-css/reset.css'; +import './style.css'; + (async () => { const store = await initDb(); @@ -11,6 +15,7 @@ import { webappConnect } from './functions/remote/webappConnect.ts'; await webappConnect(store, window.opener); } } else { + await initSettings(); await initGallery(store); } diff --git a/frontend/src/style.css b/frontend/src/style.css index a5024f7..523d9e3 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,14 +1,25 @@ -* { - box-sizing: border-box; -} +* { box-sizing: border-box; } +button { font-family: inherit; } +a { color: inherit; } +svg { fill: currentColor; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; text-align: center; + margin: 0 0 16px 0; + min-width: 250px; + overflow-y: auto; + overflow-x: hidden; +} + +body.fixed { + overflow: hidden; } header { - margin-bottom: 24px; + margin: 0; + padding: 8px; + position: relative; } .buttons { @@ -16,12 +27,18 @@ header { flex-wrap: wrap; flex-direction: row; justify-content: center; - gap: 8px; + gap: 5px; + position: sticky; + top: 0; + z-index: 1; + padding: 8px; + background: #ffffffe0; } button, -.select { - display: inline-flex; +a.button { + display: flex; + flex-direction: column; padding: 0; justify-content: center; align-items: center; @@ -30,32 +47,124 @@ button, background: #b7b7b7; height: 32px; font-size: 16px; - line-height: 12px; - gap: 8px + line-height: 14px; + gap: 8px; + text-decoration: none; } -button span, -.select select { - padding: 0 16px; +.buttons button, +.buttons a.button { + height: 56px; + width: 56px; + padding: 4px; + gap: 2px; } -#download_fps, -#download_size { - height: 32px; +button span.title, +a.button span.title { + font-size: 11px; +} + +#settings { + position: absolute; + top: 0; + display: flex; + flex-direction: column; + justify-content: space-between; + left: -300px; + z-index: 3; + background: #fff; + padding: 48px 0 0 0; + max-width: 300px; + width: 100%; + height: 100vh; + border-right: 2px solid #eeeeee; + transition: left 300ms ease-in-out; +} + +#settings.visible { + left: 0; +} + +#settings_backdrop { + z-index: 2; + position: absolute; + top: 0; + left: -100vw; + width: 100vw; + height: 100vh; + background: #333333; + opacity: 0; + transition: opacity 300ms ease-in-out, left 0ms linear 300ms; +} + +#settings_backdrop.visible { + opacity: 30%; + left: 0; + transition: opacity 300ms ease-in-out, left 0ms linear 0ms; +} + +#settings_close { + position: absolute; + top: 16px; + right: 16px; background: none; + width: 48px; + height: 48px; +} + +#settings_close:hover { + color: #999999; +} + +.settings-options { + display: flex; + flex-direction: column; + gap: 16px; + overflow-y: auto; + padding: 16px; + flex-grow: 2; +} + +.select label { + font-size: 14px; + text-align: left; + padding-bottom: 4px; +} + +.select .box { + border-radius: 4px; + background: #b7b7b7; +} + +.select * { + display: block; + width: 100%; +} + +.setting-select { border: none; + display: block; + height: 32px; + background: none; font-size: 16px; + line-height: 14px; + padding: 0 8px; } -.select:hover, +a.button:not(:disabled):hover, button:not(:disabled):hover { cursor: pointer; background: #a0a0a0; } h1 { - font-size: 2rem; + font-weight: bold; + font-size: 28px; + line-height: 34px; position: relative; + margin: 0; + padding: 0 28px; } :root { --grid-gap: 6px; --num-cols: 1; } @@ -73,7 +182,7 @@ h1 { display: grid; grid-column-gap: var(--grid-gap); grid-row-gap: var(--grid-gap); - margin: 0 auto; + margin: 16px auto 0; } .gallery-image { @@ -165,3 +274,41 @@ h1 { --color-light: #ff9c9c; --color-dark: #440606; } + +.about-link { + font-size: 12px; + display: block; + width: 100%; + text-align: right; + padding: 16px; +} + +.toast-target { + padding: 40px; + position: fixed; + z-index: 4; + bottom: 0; + right: 0; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 12px; +} + +.toast { + color: #ff9898; + background: #770000; + padding: 16px; + text-align: left; + border-radius: 8px; + border: 2px solid currentColor; + font-size: 16px; + line-height: 18px; + max-width: 300px; + cursor: pointer; +} + +::-webkit-scrollbar { width: 8px; } +::-webkit-scrollbar-track { background: #dddddde0; } +::-webkit-scrollbar-thumb { background: #888888e0; } +::-webkit-scrollbar-thumb:hover { background: #aaaaaae0; }