chore: mobile drag n drop + increase png size + remove js media query

main
Kyle Zheng 2024-10-13 04:03:13 -04:00
rodzic 40f63e89c9
commit 151b50c300
5 zmienionych plików z 115 dodań i 140 usunięć

Wyświetl plik

@ -3,18 +3,17 @@ import { NumberField } from "@kobalte/core/number-field";
import { Popover } from "@kobalte/core/popover";
import ChevronDown from "lucide-solid/icons/chevron-down";
import Download from "lucide-solid/icons/download";
import { createSignal, Show } from "solid-js";
import { createSignal } from "solid-js";
import { FillButton } from "./Button";
type Props = {
onPng: (resizeWidth, resizeHeight) => void;
onSvg: () => void;
compact: boolean;
disabled: boolean;
};
export function SplitButton(props: Props) {
const [customWidth, setCustomWidth] = createSignal(1000);
const [customHeight, setCustomHeight] = createSignal(1000);
const [customWidth, setCustomWidth] = createSignal(2000);
const [customHeight, setCustomHeight] = createSignal(2000);
const onPng = (resizeWidth, resizeHeight) => {
props.onPng(resizeWidth, resizeHeight);
@ -33,7 +32,8 @@ export function SplitButton(props: Props) {
disabled={props.disabled}
>
<Download size={20} />
{props.compact ? "Download" : "PNG"}
<span class="md:hidden">Download</span>
<span class="hidden md:inline">PNG</span>
</Button>
<Popover gutter={4} open={open()} onOpenChange={setOpen}>
<Popover.Trigger
@ -48,31 +48,24 @@ export function SplitButton(props: Props) {
<Popover.Portal>
<Popover.Content class="z-50 bg-back-base rounded-md border p-2 outline-none min-w-150px leading-tight">
<div class="flex flex-col gap-2">
<Show
when={props.compact}
fallback={
<>
<div class="text-sm font-bold">Select size</div>
<FillButton
class="w-full p-2"
onClick={() => onPng(300, 300)}
>
300x300
</FillButton>
<FillButton
class="w-full p-2"
onClick={() => onPng(500, 500)}
>
500x500
</FillButton>
</>
}
>
<div class="hidden md:contents">
<div class="text-sm font-bold">Select size</div>
<FillButton class="w-full p-2" onClick={() => onPng(500, 500)}>
500x500
</FillButton>
<FillButton
class="w-full p-2"
onClick={() => onPng(1000, 1000)}
>
1000x1000
</FillButton>
</div>
<div class="contents md:hidden">
<div class="text-sm font-bold">Alternate file type</div>
<FillButton class="w-full p-2" onClick={onSvg}>
SVG
</FillButton>
</Show>
</div>
<hr />
<div class="text-sm font-bold">Custom size</div>
<div class="flex gap-2">

Wyświetl plik

@ -125,7 +125,7 @@ function ArrayParam({ label, other }) {
value={v()}
setValue={(v: any) => setParams(label, i, v)}
/>
<div class="px-1 cursor-move" {...sortable.dragActivators}>
<div class="px-1 cursor-move touch-none" {...sortable.dragActivators}>
<GripVertical />
</div>
</div>

Wyświetl plik

@ -498,7 +498,7 @@ function Preview(props: PreviewProps) {
>
<div
classList={{
"h-24 w-24 rounded-sm checkboard": true,
"h-24 w-24 rounded-sm checkerboard": true,
"ring-2 ring-fore-base ring-offset-4 ring-offset-back-base":
props.active,
}}

Wyświetl plik

@ -18,7 +18,6 @@ import { Popover } from "@kobalte/core/popover";
type Props = {
classList: JSX.CustomAttributes<HTMLDivElement>["classList"];
compact: boolean;
ref: HTMLDivElement;
};
@ -27,7 +26,7 @@ export function QrPreview(props: Props) {
return (
<div classList={props.classList} ref={props.ref}>
<div classList={{ "max-w-[300px] md:max-w-full w-full self-center": props.compact }}>
<div class="max-w-[300px] md:max-w-full w-full self-center">
<Show
when={output().state === QrState.Ready}
fallback={
@ -65,23 +64,28 @@ export function QrPreview(props: Props) {
<RenderedQrCode />
</Show>
</div>
<DownloadButtons title={!props.compact} compact={props.compact} />
<Show when={!props.compact}>
<Metadata />
</Show>
<DownloadButtons />
<Metadata class="hidden md:block" />
</div>
);
}
function RenderedQrCode() {
const { render, error, svgParentRefs, addSvgParentRef, canvasRefs, addCanvasRef } = useRenderContext();
const {
render,
error,
svgParentRefs,
addSvgParentRef,
canvasRefs,
addCanvasRef,
} = useRenderContext();
let i = svgParentRefs.length
let j = canvasRefs.length
let i = svgParentRefs.length;
let j = canvasRefs.length;
onCleanup(() => {
svgParentRefs.splice(i, 1)
canvasRefs.splice(j, 1)
})
svgParentRefs.splice(i, 1);
canvasRefs.splice(j, 1);
});
return (
<>
@ -107,11 +111,14 @@ function RenderedQrCode() {
);
}
function Metadata() {
const { output } = useQrContext();
type MetadataProps = {
class?: string;
};
function Metadata(props: MetadataProps) {
const { output } = useQrContext();
return (
<div>
<div class={props.class}>
<div class="font-bold text-sm pb-2">QR Metadata</div>
<Show when={output().state === QrState.Ready}>
<div class="grid grid-cols-2 gap-2 text-sm">
@ -146,20 +153,15 @@ function Metadata() {
);
}
type DownloadProps = {
title?: boolean;
compact: boolean;
};
function DownloadButtons(props: DownloadProps) {
function DownloadButtons() {
const { output } = useQrContext();
const { render, svgParentRefs, canvasRefs } = useRenderContext();
const filename = () => output().qr!.text.slice(0, 32);
const disabled = () => output().state !== QrState.Ready;
const pngBlob = async (resizeWidth, resizeHeight) => {
// 10px per module assuming 2 module margin
const minWidth = (output().qr!.version * 4 + 17 + 4) * 10;
// roughly 20px per module, ranges from 500 to 3620px
const minWidth = (output().qr!.version * 4 + 17 + 4) * 20;
let outCanvas: HTMLCanvasElement;
if (render()?.type === "canvas") {
@ -217,17 +219,12 @@ function DownloadButtons(props: DownloadProps) {
URL.revokeObjectURL(url);
};
const disabled = () => output().state !== QrState.Ready;
return (
<div>
<Show when={props.title}>
<div class="font-bold text-sm pb-2">Downloads</div>
</Show>
<div class={props.compact ? "flex gap-2" : "grid grid-cols-2 gap-2"}>
<div class="font-bold text-sm pb-2 md:hidden">Downloads</div>
<div class="flex gap-2 md:(grid grid-cols-2)">
<SplitButton
disabled={disabled()}
compact={props.compact}
onPng={async (resizeWidth, resizeHeight) => {
try {
const blob = await pngBlob(resizeWidth, resizeHeight);
@ -243,9 +240,9 @@ function DownloadButtons(props: DownloadProps) {
}}
onSvg={downloadSvg}
/>
<Show when={!props.compact && render()?.type === "svg"}>
<Show when={render()?.type === "svg"}>
<FlatButton
class="flex-1 inline-flex justify-center items-center gap-1 px-3 py-2"
class="hidden md:inline-flex flex-1 justify-center items-center gap-1 px-3 py-2"
disabled={disabled()}
onClick={downloadSvg}
>
@ -253,60 +250,59 @@ function DownloadButtons(props: DownloadProps) {
SVG
</FlatButton>
</Show>
<Show when={props.compact}>
<FlatButton
class="inline-flex justify-center items-center gap-1 px-6 py-2"
<FlatButton
class="md:hidden inline-flex justify-center items-center gap-1 px-6 py-2"
disabled={disabled()}
title="Share"
onClick={async () => {
let blob;
try {
blob = await pngBlob(0, 0);
if (blob == null) throw "toBlob returned null";
} catch (e) {
toastError(
"Failed to create image",
typeof e === "string" ? e : "pngBlob failed"
);
return;
}
try {
const shareData = {
files: [
new File([blob], `${filename()}.png`, {
type: "image/png",
}),
],
};
if (!navigator.canShare(shareData)) {
throw new Error();
}
navigator.share(shareData);
} catch (e) {
console.log(e);
toastError(
"Native sharing failed",
"File sharing not supported by browser"
);
}
}}
>
<Share2 size={20} />
</FlatButton>
<Popover gutter={4}>
<Popover.Trigger
class="md:hidden border rounded-md hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) p-2 disabled:(pointer-events-none opacity-50)"
disabled={disabled()}
title="Share"
onClick={async () => {
let blob;
try {
blob = await pngBlob(0, 0);
if (blob == null) throw "toBlob returned null";
} catch (e) {
toastError("Failed to create image", e as string);
return;
}
try {
const shareData = {
files: [
new File([blob], `${filename()}.png`, {
type: "image/png",
}),
],
};
if (!navigator.canShare(shareData)) {
throw new Error();
}
navigator.share(shareData);
} catch (e) {
console.log(e);
toastError(
"Native sharing failed",
"File sharing not supported by browser"
);
}
}}
title="QR Metadata"
>
<Share2 size={20} />
</FlatButton>
</Show>
<Show when={props.compact}>
<Popover gutter={4}>
<Popover.Trigger
class="border rounded-md hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) p-2 disabled:(pointer-events-none opacity-50)"
disabled={disabled()}
title="QR Metadata"
>
<Info size={20} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content class="z-50 bg-back-base rounded-md border p-2 outline-none min-w-150px leading-tight">
<Metadata />
</Popover.Content>
</Popover.Portal>
</Popover>
</Show>
<Info size={20} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content class="z-50 bg-back-base rounded-md border p-2 outline-none min-w-150px leading-tight">
<Metadata />
</Popover.Content>
</Popover.Portal>
</Popover>
</div>
</div>
);

Wyświetl plik

@ -1,11 +1,10 @@
import { isServer, Portal } from "solid-js/web";
import { createSignal, onCleanup, onMount } from "solid-js";
import { Portal } from "solid-js/web";
import { Editor } from "~/components/editor/QrEditor";
import { ErrorToasts } from "~/components/ErrorToasts";
import { QrPreview } from "~/components/preview/QrPreview";
import { RenderContextProvider } from "~/lib/RenderContext";
import { createSignal, onCleanup, onMount } from "solid-js";
import { QrContextProvider } from "~/lib/QrContext";
import { RenderContextProvider } from "~/lib/RenderContext";
export default function Home() {
return (
@ -18,21 +17,13 @@ export default function Home() {
}
function Temp() {
// tracking mediaquery in js b/c rendering step draws to all mounted elements
const [desktop, setDesktop] = createSignal(false);
onMount(() => {
const mql = window.matchMedia("(min-width: 768px)");
const callback = (e) => setDesktop(e.matches);
mql.addEventListener("change", callback);
setDesktop(mql.matches);
const viewport = window.visualViewport!;
let prevHeight = viewport.height;
const detectMobileKeyboard = () => {
const prev = prevHeight;
prevHeight = viewport.height;
if (desktop() || !textFocused()) return;
if (!textFocused()) return;
if (viewport.height === prev) return;
if (viewport.height > prev) {
// closing mobile keyboard
@ -40,9 +31,7 @@ function Temp() {
}
};
viewport.addEventListener("resize", detectMobileKeyboard);
onCleanup(() => {
mql.removeEventListener("change", callback);
window.removeEventListener("resize", detectMobileKeyboard);
});
});
@ -55,12 +44,12 @@ function Temp() {
setTextFocused(true);
};
const onBlur = () => {
if (!desktop()) {
// firefox scrolls input behind sticky QrPreview
// adding/removing sticky + this scroll + animation
// gives roughly equivalent ux as chrome default
const before = `${qrPreview.getBoundingClientRect().top}px`;
qrPreview.animate([{ top: before }, { top: 0 }], {
// firefox scrolls input behind sticky QrPreview
// adding/removing sticky + this scroll + animation
// gives roughly equivalent ux as chrome default
const before = qrPreview.getBoundingClientRect().top;
if (before !== 0) {
qrPreview.animate([{ top: `${before}px` }, { top: 0 }], {
// slow animation to prevent bounce
duration: 1000,
easing: "ease-out",
@ -86,13 +75,10 @@ function Temp() {
<QrPreview
ref={qrPreview!}
classList={{
"top-0 flex flex-col gap-4 px-4": true,
"py-4 rounded-b-[1rem] border-b shadow-2xl bg-back-base z-10 [transition:top]":
!desktop(),
sticky: !desktop() && !textFocused(),
"md:(sticky flex-1 flex-grow-2 min-w-300px self-start py-8)": true,
"top-0 flex flex-col gap-4 p-4 rounded-b-[1rem] border-b shadow-2xl bg-back-base z-10 [transition:top] md:(sticky flex-1 flex-grow-2 min-w-300px self-start py-8 border-none shadow-none)":
true,
sticky: !textFocused(),
}}
compact={!desktop()}
/>
</div>
<Portal>