Skip to content

Commit

Permalink
✨🚧Completed ImageField
Browse files Browse the repository at this point in the history
  • Loading branch information
carefree0910 committed May 24, 2023
1 parent 929c2be commit 75a5c11
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 14 deletions.
5 changes: 5 additions & 0 deletions cfdraw/.web/src/assets/icons/import.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions cfdraw/.web/src/lang/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ export enum UI_Words {
"list-field-empty-caption" = "list-field-empty-caption",
"add-object-to-list-tooltip" = "add-object-to-list-tooltip",
"text-editor-align-label" = "text-editor-align-label",
"image-field-url-placeholder" = "image-field-url-placeholder",
"image-field-image-picker-tooltip" = "image-field-image-picker-tooltip",
"image-field-image-picker" = "image-field-image-picker",
"image-field-import-image-caption" = "image-field-import-image-caption",
"image-field-loading-caption" = "image-field-loading-caption",
"image-field-no-more-caption" = "image-field-no-more-caption",
}

export const uiLangRecords: Record<Lang, Record<UI_Words, string>> = {
Expand All @@ -22,7 +27,12 @@ export const uiLangRecords: Record<Lang, Record<UI_Words, string>> = {
[UI_Words["list-field-empty-caption"]]: "无",
[UI_Words["add-object-to-list-tooltip"]]: "添加",
[UI_Words["text-editor-align-label"]]: "对齐方式",
[UI_Words["image-field-url-placeholder"]]: "请选择目标图片",
[UI_Words["image-field-image-picker-tooltip"]]: "请用右边的图片选择器选择目标图片",
[UI_Words["image-field-image-picker"]]: "选择目标图片",
[UI_Words["image-field-import-image-caption"]]: "导入",
[UI_Words["image-field-loading-caption"]]: "加载中...",
[UI_Words["image-field-no-more-caption"]]: "没有更多了",
},
en: {
[UI_Words["submit-task"]]: "Submit",
Expand All @@ -33,6 +43,12 @@ export const uiLangRecords: Record<Lang, Record<UI_Words, string>> = {
[UI_Words["list-field-empty-caption"]]: "Empty",
[UI_Words["add-object-to-list-tooltip"]]: "Add",
[UI_Words["text-editor-align-label"]]: "Align",
[UI_Words["image-field-image-picker-tooltip"]]:
"Please use the image picker on the right to pick an image",
[UI_Words["image-field-url-placeholder"]]: "Please pick an image",
[UI_Words["image-field-image-picker"]]: "Pick an image",
[UI_Words["image-field-import-image-caption"]]: "Import",
[UI_Words["image-field-loading-caption"]]: "Loading...",
[UI_Words["image-field-no-more-caption"]]: "No more",
},
};
199 changes: 186 additions & 13 deletions cfdraw/.web/src/plugins/components/Fields/ImageField/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import {
Center,
Expand All @@ -15,45 +15,188 @@ import {
useDisclosure,
} from "@chakra-ui/react";

import { getRandomHash } from "@carefree0910/core";
import { langStore, translate } from "@carefree0910/business";
import { BoardStore, langStore, translate, useSelecting } from "@carefree0910/business";

import "./index.scss";
import ImageIcon from "@/assets/icons/image.svg";
import { ReactComponent as ImportIcon } from "@/assets/icons/import.svg";
import { ReactComponent as ArrowDownIcon } from "@/assets/icons/arrow-down.svg";

import { IImageNode, checkEqual } from "@carefree0910/core";

import type { IField } from "@/schema/plugins";
import type { IImageField } from "@/schema/fields";
import { UI_Words } from "@/lang/ui";
import { Toast_Words } from "@/lang/toast";
import { genBlock } from "@/utils/bem";
import { toastWord } from "@/utils/toast";
import { titleCaseWord } from "@/utils/misc";
import { EXPAND_TRANSITION } from "@/utils/constants";
import { themeStore } from "@/stores/theme";
import { EXPAND_TRANSITION, IMAGE_PLACEHOLDER } from "@/utils/constants";
import { themeStore, useActiveBorderProps, useScrollBarSx } from "@/stores/theme";
import { getMetaField, setMetaField } from "@/stores/meta";
import { parseIStr } from "@/actions/i18n";
import CFIcon from "@/components/CFIcon";
import CFText from "@/components/CFText";
import CFText, { CFCaption } from "@/components/CFText";
import CFTooltip, { CFFormLabel } from "@/components/CFTooltip";
import CFImageUploader from "@/components/CFImageUploader";
import { useDefaultFieldValue } from "../utils";

const GalleryContainer = (props: ButtonProps) => <Box as="button" w="100px" h="120px" {...props} />;
interface IGalleryUpload {
setValueAndMeta: (url: string) => void;
}
const GalleryUpload = observer(({ setValueAndMeta }: IGalleryUpload) => {
const lang = langStore.tgt;
const { captionColor, dividerColor } = themeStore.styles;

return (
<CFImageUploader
addToBoard={false}
onUpload={(res) => {
if (res.safe) {
toastWord("success", Toast_Words["upload-image-success-message"]);
setValueAndMeta(res.url);
} else {
toastWord("warning", Toast_Words["nsfw-image-detected-warning-message"], {
appendix: ` (${res.reason})`,
});
}
}}>
<GalleryContainer
borderWidth="3px"
borderStyle="dashed"
borderRadius="16px"
borderColor={dividerColor}>
<Flex
w="100%"
h="100%"
gap="4px"
align="center"
justifyContent="center"
color={captionColor}
direction="column">
<CFIcon squared={false} svg={ImportIcon} strokeByCurrentColor />
<CFCaption>{translate(UI_Words["image-field-import-image-caption"], lang)}</CFCaption>
</Flex>
</GalleryContainer>
</CFImageUploader>
);
});
interface IGalleryItem extends ImageProps {
src: string;
active: boolean;
setValueAndMeta: (url: string) => void;
}
const GalleryItem = observer(({ src, active, setValueAndMeta, ...others }: IGalleryItem) => {
const {
selectColors: { activeBorderColor },
} = themeStore.styles;

return (
<GalleryContainer
p="2px"
borderWidth="1px"
_hover={{ borderColor: activeBorderColor }}
onClick={() => setValueAndMeta(src)}
{...(active ? useActiveBorderProps(activeBorderColor) : {})}>
<Image
w="100%"
h="100%"
objectFit="contain"
src={src}
fallbackSrc={IMAGE_PLACEHOLDER}
{...others}
/>
</GalleryContainer>
);
});

const block = genBlock("c-image-field");
function ImageField({ definition, ...fieldKeys }: IField<IImageField>) {
useDefaultFieldValue({ definition, ...fieldKeys });
const id = getRandomHash().toString();
const label = parseIStr(definition.label ?? titleCaseWord(fieldKeys.field));
const tooltip = parseIStr(definition.tooltip ?? "");
const lang = langStore.tgt;
const { panelBg } = themeStore.styles;
const [value, setValue] = useState(getMetaField(fieldKeys) ?? definition.default);
const { isOpen, onToggle, onClose } = useDisclosure();
const setValueAndMeta = (value: string) => {
setValue(value);
setMetaField(fieldKeys, value);
};
const [imageNodes, setImageNodes] = useState<IImageNode[]>([]);
const [hasMore, setHasMore] = useState(true);
const numFetchEvery = 5;
const fetchImages = () => {
// fetch current image nodes
const { type, nodes: selectingNodes } = useSelecting("raw");
const selectingAliases = new Set(selectingNodes.map((n) => n.alias));
const allRenderNodes = BoardStore.graph.allRenderNodes.filter(
(node) => !selectingAliases.has(node.alias),
);
const currentImageNodes = (type === "none" ? [] : selectingNodes)
.concat(allRenderNodes)
.filter((node) => node.type === "image") as IImageNode[];
// set new image nodes
setImageNodes((prevImageNodes) => {
if (
checkEqual(
prevImageNodes.map((n) => n.alias),
currentImageNodes.map((n) => n.alias),
)
) {
setHasMore(false);
return prevImageNodes;
}
setHasMore(true);
const newNumItems = Math.min(prevImageNodes.length + numFetchEvery, currentImageNodes.length);
return currentImageNodes.slice(0, newNumItems);
});
};
const [loader, setLoader] = useState<HTMLElement | null>(null);
const handleObserver = (entries: IntersectionObserverEntry[]) => {
const target = entries[0];
if (target.isIntersecting && hasMore) {
fetchImages();
}
};
useEffect(() => {
const observer = new IntersectionObserver(handleObserver, {
root: null,
rootMargin: "20px",
threshold: 1.0,
});
if (loader) {
observer.observe(loader);
}
return () => {
if (loader) {
observer.unobserve(loader);
}
};
}, [loader]);
const { isOpen, onToggle, onClose } = useDisclosure({ onOpen: fetchImages });

return (
<Flex w="100%" align="center" {...definition.props}>
<CFFormLabel label={label} tooltip={{ label: tooltip }} />
<CFTooltip label={<Image my="6px" src={value} />}>
<CFText flex={1} noOfLines={1}>
{value}
</CFText>
<CFTooltip
bg={value ? panelBg : undefined}
hasArrow={!value}
borderWidth="1px"
label={
value ? (
<Image my="6px" src={value} fallbackSrc={IMAGE_PLACEHOLDER} />
) : (
translate(UI_Words["image-field-image-picker-tooltip"], lang)
)
}>
{value ? (
<CFText flex={1} noOfLines={1}>
{value}
</CFText>
) : (
<CFCaption flex={1}>{translate(UI_Words["image-field-url-placeholder"], lang)}</CFCaption>
)}
</CFTooltip>
<Popover isOpen={isOpen} onClose={onClose}>
<PopoverTrigger>
Expand All @@ -73,7 +216,37 @@ function ImageField({ definition, ...fieldKeys }: IField<IImageField>) {
</Center>
</PopoverTrigger>
<Portal>
<PopoverContent w="320px" h="320px" bg={`${panelBg}cc`}></PopoverContent>
<PopoverContent w="358px" h="320px" bg={`${panelBg}cc`}>
<PopoverArrow />
<Flex
w="100%"
h="100%"
p="16px"
wrap="wrap"
gap="12px"
align="center"
alignContent="flex-start"
overflow="hidden"
sx={useScrollBarSx()}>
<GalleryUpload setValueAndMeta={setValueAndMeta} />
{imageNodes.map((node, i) => {
const src = node.renderParams.src;
const active = src === value;
return (
<GalleryItem
src={node.renderParams.src}
active={active}
setValueAndMeta={setValueAndMeta}
key={`gallery-item-${i}`}
/>
);
})}
<div ref={setLoader} />
{!hasMore && (
<CFCaption>{translate(UI_Words["image-field-no-more-caption"], lang)}</CFCaption>
)}
</Flex>
</PopoverContent>
</Portal>
</Popover>
</Flex>
Expand Down
2 changes: 1 addition & 1 deletion tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
tooltip="label1",
),
label2=IImageField(
default="https://ailab-huawei-cdn.nolibox.com/aigc/images/cfdraw-demo/e9a76231ae474c2098b2d7b44843b409.png",
default="",
label="label2",
tooltip="label2",
),
Expand Down

0 comments on commit 75a5c11

Please sign in to comment.