UNPKG

@wener/console

Version:

Base console UI toolkit

295 lines (294 loc) 11.6 kB
"use client"; import React, { useContext, useEffect, useState } from "react"; import { HiChevronDoubleLeft, HiChevronDoubleRight, HiXMark } from "react-icons/hi2"; import { PiArrowClockwise, PiArrowCounterClockwise, PiCopy, PiCornersIn, PiCornersOut, PiDownloadSimple } from "react-icons/pi"; import { TbTextRecognition } from "react-icons/tb"; import { createReactContext, useAbortController } from "@wener/reaction"; import { copy, download, formatBytes, getGlobalThis, loadScripts } from "@wener/utils"; import clsx from "clsx"; import { createStore, useStore } from "zustand"; import { mutative } from "zustand-mutative"; import { useShallow } from "zustand/react/shallow"; import { showErrorToast, showSuccessToast } from "../../../toast/index.js"; function createImagePreviewStore(initial) { const state = { detail: false, enlarge: false, rotate: 0, info: {}, ...initial }; return createStore(mutative(() => state)); } const Context = createReactContext("ImagePreviewStoreContext", createImagePreviewStore()); export function useImagePreviewStore() { return useContext(Context); } export function useImagePreviewContext() { let store = useContext(Context); return { store }; } const DetailButton = () => { let { store } = useImagePreviewContext(); const showDetail = useStore(store, (s) => s.detail); const setShowDetail = (v) => { store.setState({ detail: v }); }; return /*#__PURE__*/ React.createElement(ActionButton, { className: "absolute right-4 top-4 z-10", onClick: () => { setShowDetail(!showDetail); } }, showDetail && /*#__PURE__*/ React.createElement(HiChevronDoubleRight, { className: "h-6 w-6 text-white/75" }), !showDetail && /*#__PURE__*/ React.createElement(HiChevronDoubleLeft, { className: "h-6 w-6 text-white/75" })); }; export const ImagePreview = ({ onOpenChange, info, src }) => { const [store] = useState(() => createImagePreviewStore({ src, info: info, detail: true })); useEffect(() => { store.setState((s) => { s.src = src; s.info = { ...s.info, ...info }; }); }, [ src, info ]); return /*#__PURE__*/ React.createElement(Context.Provider, { value: store }, /*#__PURE__*/ React.createElement("div", { className: "relative z-10 h-full w-full overflow-hidden bg-black/90" }, /*#__PURE__*/ React.createElement("div", { className: "absolute inset-0" }, /*#__PURE__*/ React.createElement("div", { className: "flex h-full" }, /*#__PURE__*/ React.createElement("div", { className: "relative flex h-full flex-1 flex-col" }, /*#__PURE__*/ React.createElement(ActionButton, { className: "absolute left-4 top-4 z-10", onClick: () => onOpenChange?.(false) }, /*#__PURE__*/ React.createElement(HiXMark, { className: "h-6 w-6 text-white/75" })), /*#__PURE__*/ React.createElement(DetailButton, null), /*#__PURE__*/ React.createElement("div", { className: "relative flex flex-1 items-center justify-center overflow-hidden" }, /*#__PURE__*/ React.createElement(_Image, null)), /*#__PURE__*/ React.createElement("div", { className: "flex justify-center pb-4 text-white" }, /*#__PURE__*/ React.createElement(Actions, null))), /*#__PURE__*/ React.createElement(DetailPanel, null))))); }; const _Image = () => { const { store } = useImagePreviewContext(); const { rotate, enlarge, src } = useStore(store, useShallow(({ rotate, enlarge, src }) => ({ rotate, enlarge, src }))); if (!src) { return /*#__PURE__*/ React.createElement("div", null, "\u65E0\u56FE\u7247"); } return /*#__PURE__*/ React.createElement("img", { crossOrigin: "anonymous", className: "max-h-full object-contain", src: src, alt: "preview image", ref: (imgRef) => { if (imgRef) { if (store.getState().imgRef !== imgRef) { store.setState({ imgRef }); } } }, style: { transform: `rotate(${rotate}deg) scale(${enlarge ? 2 : 1})` }, onLoad: (e) => { let w = e.currentTarget.naturalWidth; let h = e.currentTarget.naturalHeight; store.setState((s) => { s.info.width = w; s.info.height = h; }); } }); }; const Actions = () => { let { store } = useImagePreviewContext(); let { enlarge, src } = useStore(store, useShallow(({ enlarge, src }) => ({ enlarge, src }))); return /*#__PURE__*/ React.createElement("div", { className: "flex gap-2" }, /*#__PURE__*/ React.createElement(FuncButton, { onClick: () => { store.setState({ enlarge: !enlarge }); } }, !enlarge && /*#__PURE__*/ React.createElement(PiCornersOut, { className: "h-6 w-6" }), enlarge && /*#__PURE__*/ React.createElement(PiCornersIn, { className: "h-6 w-6" })), /*#__PURE__*/ React.createElement(FuncButton, { onClick: () => { store.setState((s) => { s.rotate = (s.rotate + 90) % 360; }); } }, /*#__PURE__*/ React.createElement(PiArrowCounterClockwise, { className: "h-6 w-6" })), /*#__PURE__*/ React.createElement(FuncButton, { onClick: () => { store.setState((s) => { s.rotate = (s.rotate + 270) % 360; }); } }, /*#__PURE__*/ React.createElement(PiArrowClockwise, { className: "h-6 w-6" })), src && /*#__PURE__*/ React.createElement(FuncButton, { onClick: () => { download("img.jpg", src); } }, /*#__PURE__*/ React.createElement(PiDownloadSimple, { className: "h-6 w-6" }))); }; const DetailPanel = () => { let store = useContext(Context); const showDetail = useStore(store, (s) => s.detail); const { width, height, size, mimeType, format } = useStore(store, (s) => s.info || {}); if (!showDetail) { return; } return /*#__PURE__*/ React.createElement("div", { className: "w-60 bg-base-100" }, /*#__PURE__*/ React.createElement("header", { className: "px-2 py-4" }, /*#__PURE__*/ React.createElement("h2", { className: "text-lg font-semibold" }, "\u56FE\u7247\u4FE1\u606F")), /*#__PURE__*/ React.createElement("hr", null), /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("div", { className: clsx("flex flex-col gap-2 px-2 py-2", "[&>div>span:first-child]:inline-block", "[&>div>span:first-child]:w-[5ch]", "[&>div>span:last-child]:font-semibold") }, /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("span", null, "\u5C3A\u5BF8"), /*#__PURE__*/ React.createElement("span", null, width, "x", height)), size && /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("span", null, "\u5927\u5C0F"), /*#__PURE__*/ React.createElement("span", null, formatBytes(size, true))), format && /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("span", null, "\u683C\u5F0F"), /*#__PURE__*/ React.createElement("span", null, format)), mimeType && /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("span", null, "MIME"), /*#__PURE__*/ React.createElement("span", null, mimeType)))), /*#__PURE__*/ React.createElement("hr", { className: "my-2" }), /*#__PURE__*/ React.createElement("div", { className: "p-2" }, /*#__PURE__*/ React.createElement(_TextRecognitionButton, null)), /*#__PURE__*/ React.createElement("hr", { className: "my-2" }), /*#__PURE__*/ React.createElement(_ExtraInfo, null)); }; const _ExtraInfo = () => { const { store } = useImagePreviewContext(); const text = useStore(store, (s) => s.text); return /*#__PURE__*/ React.createElement("div", { className: "p-2" }, text && /*#__PURE__*/ React.createElement("label", { className: "form-control w-full max-w-xs" }, /*#__PURE__*/ React.createElement("div", { className: "label" }, /*#__PURE__*/ React.createElement("span", { className: "label-text" }, "\u8BC6\u522B\u7ED3\u679C"), /*#__PURE__*/ React.createElement("span", { className: "label-text-alt" }, /*#__PURE__*/ React.createElement("button", { type: "button", className: "btn btn-xs", onClick: async () => { try { await copy(text); showSuccessToast("\u5DF2\u590D\u5236"); } catch (e) { showErrorToast(e); } } }, /*#__PURE__*/ React.createElement(PiCopy, { className: "h-4 w-4" }), "\u590D\u5236"))), /*#__PURE__*/ React.createElement("textarea", { className: "textarea textarea-bordered textarea-sm h-24 w-full", value: text, readOnly: true }))); }; const _TextRecognitionButton = () => { let { store } = useImagePreviewContext(); const imgRef = useStore(store, (s) => s.imgRef); const { recognize, loading } = useTesseract(); return /*#__PURE__*/ React.createElement("button", { type: "button", className: clsx("btn btn-sm"), disabled: loading, onClick: async () => { if (!imgRef) { return; } try { const { text } = await recognize(imgRef); store.setState({ text }); } catch (e) { showErrorToast(e); } } }, /*#__PURE__*/ React.createElement(TbTextRecognition, { className: clsx("h-4 w-4", loading && "loading loading-spinner") }), "\u6587\u5B57\u8BC6\u522B"); }; const ActionButton = ({ className, type, ...props }) => { return /*#__PURE__*/ React.createElement("button", { type: "button", className: clsx("btn btn-circle border-none bg-black/20 hover:bg-black/75", className), ...props }); }; const FuncButton = ({ className, type, ...props }) => { const A = props.href ? "a" : "button"; return /*#__PURE__*/ React.createElement(A, { type: "button", className: clsx("btn btn-circle border-none bg-black/45 text-white hover:bg-black/85", className), ...props }); }; function useTesseract() { const [loading, setLoading] = useState(false); const ac = useAbortController(); const doLoad = async () => { await loadScripts("https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"); }; return { loading, recognize: async (img) => { setLoading(true); try { await doLoad(); const worker = await getGlobalThis().Tesseract.createWorker("chi_sim"); ac.signal.addEventListener("abort", () => { worker.terminate(); }); let raw = await worker.recognize(img); return { raw, text: raw.data.text }; } finally { setLoading(false); } } }; }