-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0986a9c
commit 96c7a87
Showing
4 changed files
with
239 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import {Dialog as HeadlessDialog, Transition} from '@headlessui/react'; | ||
import React, {Fragment} from 'react'; | ||
import {MdOutlineClose} from 'react-icons/md'; | ||
|
||
import {Button} from '@/app/reacjin/Button'; | ||
|
||
export function Dialog({ | ||
isOpen, | ||
handleClose, | ||
children, | ||
title, | ||
showCloseButton, | ||
}: { | ||
isOpen: boolean; | ||
handleClose: () => void; | ||
children: React.ReactNode; | ||
title: React.ReactNode; | ||
showCloseButton?: boolean; | ||
}) { | ||
const closeButton = showCloseButton && ( | ||
<Button icon={<MdOutlineClose />} onClick={handleClose} /> | ||
); | ||
return ( | ||
<Transition appear show={isOpen} as={Fragment}> | ||
<HeadlessDialog as="div" className="relative z-10" onClose={handleClose}> | ||
<Transition.Child | ||
as={Fragment} | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0" | ||
enterTo="opacity-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100" | ||
leaveTo="opacity-0" | ||
> | ||
<div className="fixed inset-0 bg-background bg-opacity-25 backdrop-blur-sm" /> | ||
</Transition.Child> | ||
|
||
<div className="fixed inset-0 overflow-y-auto"> | ||
<div className="flex min-h-full items-center justify-center p-4 text-center"> | ||
<Transition.Child | ||
as={Fragment} | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0 -translate-y-2" | ||
enterTo="opacity-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100" | ||
leaveTo="opacity-0 -translate-y-2" | ||
> | ||
<HeadlessDialog.Panel className="w-full max-w-md transform overflow-hidden rounded-lg ring-2 ring-brand-100/20 bg-background shadow-black shadow-lg p-4 pb-6 text-left align-middle transition-all"> | ||
<HeadlessDialog.Title | ||
as="div" | ||
className="flex flex-row justify-between items-center text-xl font-semibold text-fg-300" | ||
> | ||
<h3>{title}</h3> | ||
<div>{closeButton}</div> | ||
</HeadlessDialog.Title> | ||
<div className="text-md font-medium text-fg-200 mt-6"> | ||
{children} | ||
</div> | ||
</HeadlessDialog.Panel> | ||
</Transition.Child> | ||
</div> | ||
</div> | ||
</HeadlessDialog> | ||
</Transition> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import {useCallback, useMemo, useState} from 'react'; | ||
import {MdOutlineDownload} from 'react-icons/md'; | ||
|
||
import {Button} from '@/app/reacjin/Button'; | ||
import {ComboRange} from '@/app/reacjin/ComboRange'; | ||
import {Dialog} from '@/app/reacjin/Dialog'; | ||
|
||
const exportTypes = ['webp', 'png', 'jpeg']; | ||
|
||
const getSupportedExportTypes = (() => { | ||
let canvas: HTMLCanvasElement | undefined; | ||
return (allTypes: string[]) => { | ||
if (!canvas) canvas = document.createElement('canvas'); | ||
return allTypes.filter((type) => | ||
canvas!.toDataURL(`image/${type}`).startsWith(`data:image/${type}`), | ||
); | ||
}; | ||
})(); | ||
|
||
function estimateFileSizeB(dataURL: string) { | ||
const {b64} = | ||
dataURL.match(/data:image\/[^;]+;base64,(?<b64>.+)/)?.groups ?? {}; | ||
if (!b64) throw new Error('Invalid data URL'); | ||
const binary = atob(b64); | ||
return binary.length; | ||
} | ||
|
||
const bytesFormat = new Intl.NumberFormat(navigator.language, { | ||
maximumFractionDigits: 2, | ||
}); | ||
function formatBytes(bytes: number): string { | ||
if (bytes === 0) return '0B'; | ||
|
||
const k = 1024; | ||
const sizes = ['B', 'KB', 'MB', 'GB']; | ||
const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||
|
||
return `${bytesFormat.format(bytes / Math.pow(k, i))} ${sizes[i]}`; | ||
} | ||
|
||
function generateDataURL( | ||
canvas: HTMLCanvasElement | undefined | null, | ||
exportType: string, | ||
quality: number, | ||
) { | ||
if (!canvas) return null; | ||
return canvas.toDataURL(`image/${exportType}`, quality); | ||
} | ||
|
||
export function ExportDialog({ | ||
isOpen, | ||
handleClose, | ||
canvas, | ||
filename, | ||
}: { | ||
isOpen: boolean; | ||
handleClose: () => void; | ||
canvas: HTMLCanvasElement | undefined | null; | ||
filename: string; | ||
}) { | ||
const [exportFilename, setExportFilename] = useState(filename); | ||
const [quality, setQuality] = useState(1); | ||
const [exportType, setExportType] = useState(exportTypes[0]); | ||
const dataURL = generateDataURL(canvas, exportType, quality); | ||
|
||
const supportedExportTypes = useMemo( | ||
() => getSupportedExportTypes(exportTypes), | ||
[], | ||
); | ||
|
||
const fileSize = useMemo(() => { | ||
if (!dataURL) return null; | ||
return formatBytes(estimateFileSizeB(dataURL)); | ||
}, [dataURL]); | ||
|
||
const handleDownload = useCallback(() => { | ||
if (!dataURL) return; | ||
const a = document.createElement('a'); | ||
a.href = dataURL; | ||
a.download = `${exportFilename}.${exportType}`; | ||
a.click(); | ||
handleClose(); | ||
}, [dataURL, filename, handleClose]); | ||
|
||
return ( | ||
<Dialog | ||
isOpen={isOpen} | ||
handleClose={handleClose} | ||
title="Export" | ||
showCloseButton | ||
> | ||
<div className="flex flex-col gap-2"> | ||
<div className="flex flex-col gap-2"> | ||
<label className="form-control"> | ||
Filename | ||
<div className="flex flex-row items-center gap-2"> | ||
<input | ||
type="text" | ||
value={exportFilename} | ||
onChange={(e) => setExportFilename(e.target.value)} | ||
className="input input-bordered input-sm grow" | ||
/> | ||
<select | ||
id="exportType" | ||
value={exportType} | ||
onChange={(e) => setExportType(e.target.value)} | ||
className="select select-bordered select-sm shrink" | ||
> | ||
{supportedExportTypes.map((type) => ( | ||
<option key={type} value={type}> | ||
.{type} | ||
</option> | ||
))} | ||
</select> | ||
</div> | ||
</label> | ||
<label className="form-control"> | ||
Quality | ||
<ComboRange | ||
value={quality} | ||
onChange={setQuality} | ||
min={0} | ||
max={1} | ||
step={0.01} | ||
/> | ||
</label> | ||
<div> | ||
<div>Preview</div> | ||
{dataURL && ( | ||
<img | ||
src={dataURL} | ||
alt="Preview" | ||
className="mx-auto select-none h-[256px]" | ||
onDragStart={(e) => e.preventDefault()} | ||
/> | ||
)} | ||
<div>Size: {fileSize}</div> | ||
</div> | ||
<Button | ||
size="lg" | ||
icon={<MdOutlineDownload />} | ||
onClick={handleDownload} | ||
> | ||
Download | ||
</Button> | ||
</div> | ||
</div> | ||
</Dialog> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters