Fix webapp mobile layout
This commit is contained in:
@@ -12,7 +12,7 @@ const unsupported = navigator.requestMIDIAccess === undefined
|
||||
<ModeWatcher />
|
||||
<Toaster position="bottom-center" duration={2000} />
|
||||
|
||||
<div class="max-w-screen-xl min-h-dvh m-auto p-8 stack gap-4">
|
||||
<div class="max-w-screen-xl min-h-dvh m-auto p-6 stack gap-4">
|
||||
{#if unsupported}
|
||||
<Unsupported />
|
||||
{:else}
|
||||
|
||||
@@ -35,7 +35,7 @@ $effect(() => {
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="row gap-2 max-lg:stack max-lg:items-stretch"
|
||||
class="flex flex-col lg:flex-row lg:items-center gap-2"
|
||||
aria-labelledby="connect-section-label"
|
||||
>
|
||||
<div class="grow">
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from '$lib/components/ui/separator'
|
||||
import PreviewSection from './preview/PreviewSection.svelte'
|
||||
import ControlsSection from './controls/ControlsSection.svelte'
|
||||
import { Separator } from '$lib/components/ui/separator'
|
||||
import PreviewSection from './preview/PreviewSection.svelte'
|
||||
import ControlsSection from './controls/ControlsSection.svelte'
|
||||
|
||||
import { createMediaQuery } from '$lib/utils/media.svelte'
|
||||
import { MediaQuery } from 'svelte/reactivity'
|
||||
|
||||
const mobileMediaQuery = createMediaQuery('(max-width: 1024px)')
|
||||
const separatorOrientation = $derived(mobileMediaQuery.matches ? 'horizontal' : 'vertical')
|
||||
const mediaLg = new MediaQuery('min-width: 1024px')
|
||||
const separatorOrientation = $derived(mediaLg.current ? 'vertical' : 'horizontal')
|
||||
</script>
|
||||
|
||||
<div class="grow stack-h max-lg:stack gap-4">
|
||||
<div class="grow flex flex-col lg:flex-row gap-4">
|
||||
<PreviewSection />
|
||||
|
||||
<Separator decorative orientation={separatorOrientation} />
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label'
|
||||
import IconHideImage from '~icons/material-symbols/hide-image'
|
||||
|
||||
import { cn } from '$lib/utils'
|
||||
import { drawQuantizedData, freshContext, makeAltText } from './common.svelte'
|
||||
import { getRenderedContext } from '$lib/contexts/rendered.svelte'
|
||||
import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants'
|
||||
import { getFilesContext } from '$lib/contexts/files.svelte'
|
||||
import { getConversionConfig } from '$lib/contexts/config.svelte'
|
||||
import { getImageContext } from '$lib/contexts/image.svelte'
|
||||
|
||||
const filesCtx = getFilesContext()
|
||||
const imageCtx = getImageContext()
|
||||
const config = getConversionConfig()
|
||||
const renderedCtx = getRenderedContext()
|
||||
const hasRendered = $derived(renderedCtx.rendered !== null)
|
||||
|
||||
let canvasEl: HTMLCanvasElement = $state(undefined!)
|
||||
|
||||
const ctx = $derived(freshContext(canvasEl))
|
||||
|
||||
$effect(() => {
|
||||
if (renderedCtx.rendered === null) return
|
||||
drawQuantizedData(ctx, renderedCtx.rendered)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-full aspect-square relative" role="group">
|
||||
<div
|
||||
class="shadow-sm shadow-[inset#888] p-1 size-full"
|
||||
role="img"
|
||||
aria-label={makeAltText(filesCtx, imageCtx, config)}
|
||||
>
|
||||
{#if hasRendered}
|
||||
<canvas
|
||||
class={cn('size-full', hasRendered || 'hidden')}
|
||||
style:image-rendering="pixelated"
|
||||
bind:this={canvasEl}
|
||||
height={DEVICE_HEIGHT}
|
||||
width={DEVICE_WIDTH}
|
||||
></canvas>
|
||||
{:else}
|
||||
<div class="col justify-center text-[#333] size-full">
|
||||
<IconHideImage class="text-5xl" aria-label="No image" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
+24
-18
@@ -16,6 +16,12 @@
|
||||
const renderedCtx = getRenderedContext()
|
||||
const hasRendered = $derived(renderedCtx.rendered !== null)
|
||||
|
||||
interface Props {
|
||||
scale: number
|
||||
}
|
||||
|
||||
const { scale }: Props = $props()
|
||||
|
||||
let canvasEl: HTMLCanvasElement = $state(undefined!)
|
||||
|
||||
const ctx = $derived(freshContext(canvasEl))
|
||||
@@ -27,25 +33,25 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="group" aria-labelledby="preview-1x-label">
|
||||
<div class="p-[3px] shadow-sm shadow-[inset#888]" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}>
|
||||
{#if hasRendered}
|
||||
<canvas
|
||||
class={cn(hasRendered || 'hidden')}
|
||||
style:image-rendering="pixelated"
|
||||
style:width="{DEVICE_WIDTH}px"
|
||||
style:height="{DEVICE_HEIGHT}px"
|
||||
bind:this={canvasEl}
|
||||
height={DEVICE_HEIGHT}
|
||||
width={DEVICE_WIDTH}
|
||||
></canvas>
|
||||
{:else}
|
||||
<div class="col justify-center text-[#333]" style:width="{DEVICE_WIDTH}px" style:height="{DEVICE_HEIGHT}px">
|
||||
<IconHideImage class="text-3xl" aria-label="No image" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="group" aria-labelledby="preview-{scale}x-label">
|
||||
<div class="shadow-sm shadow-[inset#888]" style:padding="{scale * 3}px" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}>
|
||||
<div class="aspect-square" style:width="{DEVICE_WIDTH * scale}px">
|
||||
{#if hasRendered}
|
||||
<canvas
|
||||
class={cn('size-full', hasRendered || 'hidden')}
|
||||
style:image-rendering="pixelated"
|
||||
bind:this={canvasEl}
|
||||
height={DEVICE_HEIGHT}
|
||||
width={DEVICE_WIDTH}
|
||||
></canvas>
|
||||
{:else}
|
||||
<div class="col justify-center text-[#333] size-full">
|
||||
<IconHideImage style="font-size: {30 * scale}px" aria-label="No image" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Label id="preview-1x-label">1x Preview</Label>
|
||||
<Label id="preview-{scale}x-label">{scale}x Preview</Label>
|
||||
</div>
|
||||
@@ -1,54 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label'
|
||||
import IconHideImage from '~icons/material-symbols/hide-image'
|
||||
|
||||
import { cn } from '$lib/utils'
|
||||
import { drawQuantizedData, freshContext, makeAltText } from './common.svelte'
|
||||
import { getRenderedContext } from '$lib/contexts/rendered.svelte'
|
||||
import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants'
|
||||
import { getFilesContext } from '$lib/contexts/files.svelte'
|
||||
import { getConversionConfig } from '$lib/contexts/config.svelte'
|
||||
import { getImageContext } from '$lib/contexts/image.svelte'
|
||||
|
||||
const filesCtx = getFilesContext()
|
||||
const imageCtx = getImageContext()
|
||||
const config = getConversionConfig()
|
||||
const renderedCtx = getRenderedContext()
|
||||
const hasRendered = $derived(renderedCtx.rendered !== null)
|
||||
|
||||
let canvasEl: HTMLCanvasElement = $state(undefined!)
|
||||
|
||||
const ctx = $derived(freshContext(canvasEl))
|
||||
|
||||
$effect(() => {
|
||||
if (renderedCtx.rendered === null) return
|
||||
drawQuantizedData(ctx, renderedCtx.rendered)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="bg-[#ccc] shadow-lg rounded-2xl p-4 w-fit relative" role="group" aria-labelledby="preview-2x-label">
|
||||
<div class="p-[6px] shadow shadow-[inset#888]" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}>
|
||||
{#if hasRendered}
|
||||
<canvas
|
||||
class={cn(hasRendered || 'hidden')}
|
||||
style:image-rendering="pixelated"
|
||||
style:width="{DEVICE_WIDTH * 2}px"
|
||||
style:height="{DEVICE_HEIGHT * 2}px"
|
||||
bind:this={canvasEl}
|
||||
height={DEVICE_HEIGHT}
|
||||
width={DEVICE_WIDTH}
|
||||
></canvas>
|
||||
{:else}
|
||||
<div
|
||||
class="col justify-center text-[#333]"
|
||||
style:width="{DEVICE_WIDTH * 2}px"
|
||||
style:height="{DEVICE_HEIGHT * 2}px"
|
||||
>
|
||||
<IconHideImage class="text-6xl" aria-label="No image" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Label id="preview-2x-label">2x Preview</Label>
|
||||
</div>
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import FileSelect from './FileSelect.svelte'
|
||||
import PreviewCanvas1x from './PreviewCanvas1x.svelte'
|
||||
import PreviewCanvas2x from './PreviewCanvas2x.svelte'
|
||||
import { MediaQuery } from 'svelte/reactivity'
|
||||
import FileSelect from './FileSelect.svelte'
|
||||
import PreviewCanvas from './PreviewCanvas.svelte'
|
||||
import AdaptivePreviewCanvas from './AdaptivePreviewCanvas.svelte'
|
||||
|
||||
const mediaSm = new MediaQuery('min-width: 640px', false)
|
||||
</script>
|
||||
|
||||
<section class="stack gap-4" aria-labelledby="preview-section-label">
|
||||
@@ -9,8 +12,12 @@
|
||||
|
||||
<FileSelect />
|
||||
|
||||
<div class="stack-h gap-4 max-md:stack">
|
||||
<PreviewCanvas2x />
|
||||
<PreviewCanvas1x />
|
||||
<div class="stack-h flex-wrap gap-4">
|
||||
{#if mediaSm.current}
|
||||
<PreviewCanvas scale={2} />
|
||||
<PreviewCanvas scale={1} />
|
||||
{:else}
|
||||
<AdaptivePreviewCanvas />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function makeAltText(
|
||||
|
||||
const file = filesCtx.files[0]
|
||||
|
||||
const output = [`${DEVICE_WIDTH}-by-${DEVICE_HEIGHT}-pixels e-paper preview of "${file.name}"`]
|
||||
const output = [`${DEVICE_WIDTH}-by-${DEVICE_HEIGHT}-pixel e-paper preview of "${file.name}"`]
|
||||
|
||||
if (!imageIsCorrectRatio(imageCtx.image)) {
|
||||
if (config.scaleMode === 'fit') output.push('letterboxed')
|
||||
|
||||
@@ -59,11 +59,11 @@ async function connectAndWrite() {
|
||||
return
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
toast.success('Wrote pattern to device')
|
||||
inProgress = false
|
||||
}, WRITE_TIME)
|
||||
}
|
||||
|
||||
toast.success('Wrote pattern to device')
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import IconPending from '~icons/material-symbols/pending'
|
||||
import IconArrowUploadProgress from '~icons/material-symbols/arrow-upload-progress'
|
||||
import IconWarning from '~icons/material-symbols/warning'
|
||||
import WriteButton from './WriteButton.svelte'
|
||||
import IconPending from '~icons/material-symbols/pending'
|
||||
import IconArrowUploadProgress from '~icons/material-symbols/arrow-upload-progress'
|
||||
import IconWarning from '~icons/material-symbols/warning'
|
||||
import WriteButton from './WriteButton.svelte'
|
||||
|
||||
import { getImageContext } from '$lib/contexts/image.svelte'
|
||||
import { getImageContext } from '$lib/contexts/image.svelte'
|
||||
|
||||
const imageCtx = getImageContext()
|
||||
const imageCtx = getImageContext()
|
||||
|
||||
let inProgress = $state(false)
|
||||
let inProgress = $state(false)
|
||||
</script>
|
||||
|
||||
<section class="row gap-2 max-lg:stack max-lg:items-stretch" aria-labelledby="write-section-label">
|
||||
<section class="flex flex-col lg:flex-row gap-2" aria-labelledby="write-section-label">
|
||||
<div class="grow">
|
||||
<h2 class="font-semibold text-xl/8" id="write-section-label">Write pattern to device</h2>
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
{#if imageCtx.image === null}
|
||||
<IconPending aria-hidden /> Select an image file in order to write it onto your device.
|
||||
{:else if !inProgress}
|
||||
<IconArrowUploadProgress aria-hidden /> Write the pattern onto your device if you have finished editing the image.
|
||||
<IconArrowUploadProgress aria-hidden /> Write the pattern onto your device if you have finished
|
||||
editing the image.
|
||||
{:else}
|
||||
<IconWarning aria-hidden /> Refresh in progress. Do not disconnect device.
|
||||
{/if}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
export interface ReactiveMediaQuery {
|
||||
readonly matches: boolean
|
||||
}
|
||||
|
||||
export function createMediaQuery(query: string): ReactiveMediaQuery {
|
||||
const queryList = matchMedia(query)
|
||||
|
||||
const queryState = $state({ matches: queryList.matches })
|
||||
|
||||
const updateQueryState = (e: MediaQueryListEvent) => (queryState.matches = e.matches)
|
||||
|
||||
queryList.addEventListener('change', updateQueryState)
|
||||
onDestroy(() => queryList.removeEventListener('change', updateQueryState))
|
||||
|
||||
return queryState
|
||||
}
|
||||
Reference in New Issue
Block a user