kopia lustrzana https://github.com/zhengkyl/qrframe
chore: mobile drag n drop + increase png size + remove js media query
rodzic
40f63e89c9
commit
151b50c300
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue