From d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 19 Aug 2023 02:13:26 +0800 Subject: ... --- FrontEnd/src/pages/setting/ChangeAvatarDialog.css | 21 ++ FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 334 ++++++++------------- FrontEnd/src/pages/setting/index.css | 10 - FrontEnd/src/pages/setting/index.tsx | 63 ++-- FrontEnd/src/views/common/BlobImage.tsx | 33 +- FrontEnd/src/views/common/ImageCropper.tsx | 30 +- FrontEnd/src/views/common/button/ButtonRowV2.tsx | 143 +++++++++ FrontEnd/src/views/common/button/IconButton.tsx | 1 + FrontEnd/src/views/common/button/index.tsx | 10 +- .../src/views/common/dialog/DialogContainer.tsx | 71 +++-- 10 files changed, 413 insertions(+), 303 deletions(-) create mode 100644 FrontEnd/src/pages/setting/ChangeAvatarDialog.css create mode 100644 FrontEnd/src/views/common/button/ButtonRowV2.tsx diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.css b/FrontEnd/src/pages/setting/ChangeAvatarDialog.css new file mode 100644 index 00000000..2aa0bb54 --- /dev/null +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.css @@ -0,0 +1,21 @@ +.change-avatar-dialog-prompt { + margin: 0.5em 0; +} + +.change-avatar-dialog-prompt.success { + color: var(--cru-create-color); +} + +.change-avatar-dialog-prompt.error { + color: var(--cru-danger-color); +} + +.change-avatar-cropper { + max-height: 400px; +} + +.change-avatar-preview-image { + min-width: 50%; + max-width: 100%; + max-height: 300px; +} \ No newline at end of file diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index 7ac7dcad..f35fc5a7 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -1,10 +1,4 @@ -import { - useState, - useEffect, - ChangeEvent, - ComponentPropsWithoutRef, -} from "react"; -import { AxiosError } from "axios"; +import { useState, ChangeEvent, ComponentPropsWithoutRef } from "react"; import { useC, Text, UiLogicError } from "@/common"; @@ -16,11 +10,13 @@ import ImageCropper, { Clip, applyClipToImage, } from "@/views/common/ImageCropper"; -import Button from "@/views/common/button/Button"; -import ButtonRow from "@/views/common/button/ButtonRow"; +import BlobImage from "@/views/common/BlobImage"; +import ButtonRowV2 from "@/views/common/button/ButtonRowV2"; import Dialog from "@/views/common/dialog/Dialog"; import DialogContainer from "@/views/common/dialog/DialogContainer"; +import "./ChangeAvatarDialog.css"; + interface ChangeAvatarDialogProps { open: boolean; onClose: () => void; @@ -34,15 +30,6 @@ export default function ChangeAvatarDialog({ const user = useUser(); - const [file, setFile] = useState(null); - const [fileUrl, setFileUrl] = useState(null); - const [clip, setClip] = useState(null); - const [cropImgElement, setCropImgElement] = useState( - null, - ); - const [resultBlob, setResultBlob] = useState(null); - const [resultUrl, setResultUrl] = useState(null); - type State = | "select" | "crop" @@ -53,46 +40,22 @@ export default function ChangeAvatarDialog({ | "error"; const [state, setState] = useState("select"); + const [file, setFile] = useState(null); + const [clip, setClip] = useState(null); + const [cropImgElement, setCropImgElement] = useState( + null, + ); + const [resultBlob, setResultBlob] = useState(null); const [message, setMessage] = useState( "settings.dialogChangeAvatar.prompt.select", ); - const trueMessage = c(message); - const close = (): void => { - if (!(state === "uploading")) { + if (state !== "uploading") { onClose(); } }; - useEffect(() => { - if (file != null) { - const url = URL.createObjectURL(file); - setClip(null); - setFileUrl(url); - setState("crop"); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setFileUrl(null); - setState("select"); - } - }, [file]); - - useEffect(() => { - if (resultBlob != null) { - const url = URL.createObjectURL(resultBlob); - setResultUrl(url); - setState("preview"); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setResultUrl(null); - } - }, [resultBlob]); - const onSelectFile = (e: ChangeEvent): void => { const files = e.target.files; if (files == null || files.length === 0) { @@ -113,7 +76,9 @@ export default function ChangeAvatarDialog({ } setState("process-crop"); + void applyClipToImage(cropImgElement, clip, file.type).then((b) => { + setState("preview"); setResultBlob(b); }); }; @@ -124,7 +89,6 @@ export default function ChangeAvatarDialog({ }; const onPreviewPrevious = () => { - setResultBlob(null); setState("crop"); }; @@ -144,77 +108,80 @@ export default function ChangeAvatarDialog({ () => { setState("success"); }, - (e: unknown) => { + () => { setState("error"); - setMessage({ type: "custom", value: (e as AxiosError).message }); + setMessage("operationDialog.error"); }, ); }; - const createPreviewRow = (): React.ReactElement => { - if (resultUrl == null) { - throw new UiLogicError(); - } - return ( -
-
- {c("settings.dialogChangeAvatar.previewImgAlt") -
-
- ); - }; + const cancelButton = { + key: "cancel", + action: "secondary", + text: "operationDialog.cancel", + onClick: close, + } as const; + + const createPreviousButton = (onClick: () => void) => + ({ + key: "previous", + action: "secondary", + text: "operationDialog.previousStep", + onClick, + }) as const; const buttonsMap: Record< State, - ComponentPropsWithoutRef["buttons"] + ComponentPropsWithoutRef["buttons"] > = { select: [ + cancelButton, { - key: "cancel", - type: "normal", - props: { - outline: true, - color: "secondary", - text: "operationDialog.cancel", - onClick: close, - }, + key: "next", + action: "primary", + text: "operationDialog.nextStep", + onClick: () => setState("crop"), + disabled: file == null, }, ], crop: [ + cancelButton, + createPreviousButton(onCropPrevious), { - key: "cancel", - type: "normal", - props: { - outline: true, - color: "secondary", - text: "operationDialog.cancel", - onClick: close, - }, + key: "next", + action: "primary", + text: "operationDialog.nextStep", + onClick: onCropNext, + disabled: cropImgElement == null || clip == null || clip.width === 0, }, + ], + "process-crop": [cancelButton, createPreviousButton(onPreviewPrevious)], + preview: [ + cancelButton, + createPreviousButton(onPreviewPrevious), { - key: "previous", - type: "normal", - props: { - outline: true, - color: "secondary", - text: "operationDialog.previousStep", - onClick: onCropPrevious, - }, + key: "upload", + action: "primary", + text: "settings.dialogChangeAvatar.upload", + onClick: upload, }, + ], + uploading: [], + success: [ { - key: "next", - type: "normal", - props: { - color: "primary", - text: "operationDialog.nextStep", - onClick: onCropNext, - disabled: cropImgElement == null || clip == null || clip.width === 0, - }, + key: "ok", + text: "operationDialog.ok", + color: "create", + onClick: close, + }, + ], + error: [ + cancelButton, + { + key: "retry", + action: "primary", + text: "operationDialog.retry", + onClick: upload, }, ], }; @@ -224,150 +191,95 @@ export default function ChangeAvatarDialog({ {(() => { if (state === "select") { return ( -
-
+
+
{c("settings.dialogChangeAvatar.prompt.select")}
-
- -
+
); } else if (state === "crop") { - if (fileUrl == null) { + if (file == null) { throw new UiLogicError(); } return ( -
-
- -
-
+
+ +
{c("settings.dialogChangeAvatar.prompt.crop")}
); } else if (state === "process-crop") { return ( - <> -
-
- {c("settings.dialogChangeAvatar.prompt.processingCrop")} -
+
+
+ {c("settings.dialogChangeAvatar.prompt.processingCrop")}
-
-
-
- +
); } else if (state === "preview") { return ( - <> -
- {createPreviewRow()} -
- {t("settings.dialogChangeAvatar.prompt.preview")} -
-
-
-
-
+ ); +} diff --git a/FrontEnd/src/views/common/button/IconButton.tsx b/FrontEnd/src/views/common/button/IconButton.tsx index 60050e0d..126f4263 100644 --- a/FrontEnd/src/views/common/button/IconButton.tsx +++ b/FrontEnd/src/views/common/button/IconButton.tsx @@ -9,6 +9,7 @@ interface IconButtonProps extends ComponentPropsWithoutRef<"i"> { icon: string; color?: ThemeColor; large?: boolean; + disabled?: boolean; // TODO: Not implemented } export default function IconButton(props: IconButtonProps) { diff --git a/FrontEnd/src/views/common/button/index.tsx b/FrontEnd/src/views/common/button/index.tsx index 73038849..b5aa5470 100644 --- a/FrontEnd/src/views/common/button/index.tsx +++ b/FrontEnd/src/views/common/button/index.tsx @@ -3,5 +3,13 @@ import FlatButton from "./FlatButton"; import IconButton from "./IconButton"; import LoadingButton from "./LoadingButton"; import ButtonRow from "./ButtonRow"; +import ButtonRowV2 from "./ButtonRowV2"; -export { Button, FlatButton, IconButton, LoadingButton, ButtonRow }; +export { + Button, + FlatButton, + IconButton, + LoadingButton, + ButtonRow, + ButtonRowV2, +}; diff --git a/FrontEnd/src/views/common/dialog/DialogContainer.tsx b/FrontEnd/src/views/common/dialog/DialogContainer.tsx index b0a87ea5..afee2669 100644 --- a/FrontEnd/src/views/common/dialog/DialogContainer.tsx +++ b/FrontEnd/src/views/common/dialog/DialogContainer.tsx @@ -2,11 +2,11 @@ import { ComponentProps, Ref, ReactNode } from "react"; import classNames from "classnames"; import { ThemeColor, Text, useC } from "../common"; -import { ButtonRow } from "../button"; +import { ButtonRow, ButtonRowV2 } from "../button"; import "./DialogContainer.css"; -interface DialogContainerProps { +interface DialogContainerBaseProps { className?: string; title: Text; titleColor?: ThemeColor; @@ -14,25 +14,37 @@ interface DialogContainerProps { titleRef?: Ref; bodyContainerClassName?: string; bodyContainerRef?: Ref; - buttons: ComponentProps["buttons"]; buttonsClassName?: string; buttonsContainerRef?: ComponentProps["containerRef"]; children: ReactNode; } -export default function DialogContainer({ - className, - title, - titleColor, - titleClassName, - titleRef, - bodyContainerClassName, - bodyContainerRef, - buttons, - buttonsClassName, - buttonsContainerRef, - children, -}: DialogContainerProps) { +interface DialogContainerWithButtonsProps extends DialogContainerBaseProps { + buttons: ComponentProps["buttons"]; +} + +interface DialogContainerWithButtonsV2Props extends DialogContainerBaseProps { + buttonsV2: ComponentProps["buttons"]; +} + +type DialogContainerProps = + | DialogContainerWithButtonsProps + | DialogContainerWithButtonsV2Props; + +export default function DialogContainer(props: DialogContainerProps) { + const { + className, + title, + titleColor, + titleClassName, + titleRef, + bodyContainerClassName, + bodyContainerRef, + buttonsClassName, + buttonsContainerRef, + children, + } = props; + const c = useC(); return ( @@ -57,12 +69,27 @@ export default function DialogContainer({ {children}

- + {"buttons" in props ? ( + + ) : ( + + )}
); } -- cgit v1.2.3