@wener/console
Version:
Base console UI toolkit
295 lines (294 loc) • 11.6 kB
JavaScript
"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);
}
}
};
}