Fix webapp mobile layout
This commit is contained in:
@@ -12,7 +12,7 @@ const unsupported = navigator.requestMIDIAccess === undefined
|
|||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<Toaster position="bottom-center" duration={2000} />
|
<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}
|
{#if unsupported}
|
||||||
<Unsupported />
|
<Unsupported />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ $effect(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<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"
|
aria-labelledby="connect-section-label"
|
||||||
>
|
>
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Separator } from '$lib/components/ui/separator'
|
import { Separator } from '$lib/components/ui/separator'
|
||||||
import PreviewSection from './preview/PreviewSection.svelte'
|
import PreviewSection from './preview/PreviewSection.svelte'
|
||||||
import ControlsSection from './controls/ControlsSection.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 mediaLg = new MediaQuery('min-width: 1024px')
|
||||||
const separatorOrientation = $derived(mobileMediaQuery.matches ? 'horizontal' : 'vertical')
|
const separatorOrientation = $derived(mediaLg.current ? 'vertical' : 'horizontal')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grow stack-h max-lg:stack gap-4">
|
<div class="grow flex flex-col lg:flex-row gap-4">
|
||||||
<PreviewSection />
|
<PreviewSection />
|
||||||
|
|
||||||
<Separator decorative orientation={separatorOrientation} />
|
<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 renderedCtx = getRenderedContext()
|
||||||
const hasRendered = $derived(renderedCtx.rendered !== null)
|
const hasRendered = $derived(renderedCtx.rendered !== null)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
scale: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scale }: Props = $props()
|
||||||
|
|
||||||
let canvasEl: HTMLCanvasElement = $state(undefined!)
|
let canvasEl: HTMLCanvasElement = $state(undefined!)
|
||||||
|
|
||||||
const ctx = $derived(freshContext(canvasEl))
|
const ctx = $derived(freshContext(canvasEl))
|
||||||
@@ -27,25 +33,25 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="group" aria-labelledby="preview-1x-label">
|
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="group" aria-labelledby="preview-{scale}x-label">
|
||||||
<div class="p-[3px] shadow-sm shadow-[inset#888]" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}>
|
<div class="shadow-sm shadow-[inset#888]" style:padding="{scale * 3}px" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}>
|
||||||
{#if hasRendered}
|
<div class="aspect-square" style:width="{DEVICE_WIDTH * scale}px">
|
||||||
<canvas
|
{#if hasRendered}
|
||||||
class={cn(hasRendered || 'hidden')}
|
<canvas
|
||||||
style:image-rendering="pixelated"
|
class={cn('size-full', hasRendered || 'hidden')}
|
||||||
style:width="{DEVICE_WIDTH}px"
|
style:image-rendering="pixelated"
|
||||||
style:height="{DEVICE_HEIGHT}px"
|
bind:this={canvasEl}
|
||||||
bind:this={canvasEl}
|
height={DEVICE_HEIGHT}
|
||||||
height={DEVICE_HEIGHT}
|
width={DEVICE_WIDTH}
|
||||||
width={DEVICE_WIDTH}
|
></canvas>
|
||||||
></canvas>
|
{:else}
|
||||||
{:else}
|
<div class="col justify-center text-[#333] size-full">
|
||||||
<div class="col justify-center text-[#333]" style:width="{DEVICE_WIDTH}px" style:height="{DEVICE_HEIGHT}px">
|
<IconHideImage style="font-size: {30 * scale}px" aria-label="No image" />
|
||||||
<IconHideImage class="text-3xl" aria-label="No image" />
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Label id="preview-1x-label">1x Preview</Label>
|
<Label id="preview-{scale}x-label">{scale}x Preview</Label>
|
||||||
</div>
|
</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">
|
<script lang="ts">
|
||||||
import FileSelect from './FileSelect.svelte'
|
import { MediaQuery } from 'svelte/reactivity'
|
||||||
import PreviewCanvas1x from './PreviewCanvas1x.svelte'
|
import FileSelect from './FileSelect.svelte'
|
||||||
import PreviewCanvas2x from './PreviewCanvas2x.svelte'
|
import PreviewCanvas from './PreviewCanvas.svelte'
|
||||||
|
import AdaptivePreviewCanvas from './AdaptivePreviewCanvas.svelte'
|
||||||
|
|
||||||
|
const mediaSm = new MediaQuery('min-width: 640px', false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="stack gap-4" aria-labelledby="preview-section-label">
|
<section class="stack gap-4" aria-labelledby="preview-section-label">
|
||||||
@@ -9,8 +12,12 @@
|
|||||||
|
|
||||||
<FileSelect />
|
<FileSelect />
|
||||||
|
|
||||||
<div class="stack-h gap-4 max-md:stack">
|
<div class="stack-h flex-wrap gap-4">
|
||||||
<PreviewCanvas2x />
|
{#if mediaSm.current}
|
||||||
<PreviewCanvas1x />
|
<PreviewCanvas scale={2} />
|
||||||
|
<PreviewCanvas scale={1} />
|
||||||
|
{:else}
|
||||||
|
<AdaptivePreviewCanvas />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function makeAltText(
|
|||||||
|
|
||||||
const file = filesCtx.files[0]
|
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 (!imageIsCorrectRatio(imageCtx.image)) {
|
||||||
if (config.scaleMode === 'fit') output.push('letterboxed')
|
if (config.scaleMode === 'fit') output.push('letterboxed')
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ async function connectAndWrite() {
|
|||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
toast.success('Wrote pattern to device')
|
||||||
inProgress = false
|
inProgress = false
|
||||||
}, WRITE_TIME)
|
}, WRITE_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success('Wrote pattern to device')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import IconPending from '~icons/material-symbols/pending'
|
import IconPending from '~icons/material-symbols/pending'
|
||||||
import IconArrowUploadProgress from '~icons/material-symbols/arrow-upload-progress'
|
import IconArrowUploadProgress from '~icons/material-symbols/arrow-upload-progress'
|
||||||
import IconWarning from '~icons/material-symbols/warning'
|
import IconWarning from '~icons/material-symbols/warning'
|
||||||
import WriteButton from './WriteButton.svelte'
|
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>
|
</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">
|
<div class="grow">
|
||||||
<h2 class="font-semibold text-xl/8" id="write-section-label">Write pattern to device</h2>
|
<h2 class="font-semibold text-xl/8" id="write-section-label">Write pattern to device</h2>
|
||||||
|
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
{#if imageCtx.image === null}
|
{#if imageCtx.image === null}
|
||||||
<IconPending aria-hidden /> Select an image file in order to write it onto your device.
|
<IconPending aria-hidden /> Select an image file in order to write it onto your device.
|
||||||
{:else if !inProgress}
|
{: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}
|
{:else}
|
||||||
<IconWarning aria-hidden /> Refresh in progress. Do not disconnect device.
|
<IconWarning aria-hidden /> Refresh in progress. Do not disconnect device.
|
||||||
{/if}
|
{/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