diff options
author | crupest <crupest@outlook.com> | 2023-09-20 20:26:42 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-20 20:26:42 +0800 |
commit | f836d77e73f3ea0af45c5f71dae7268143d6d86f (patch) | |
tree | 573cfafd972106d69bef0d41ff5f270ec3c43ec2 /FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | |
parent | 4a069bf1268f393d5467166356f691eb89963152 (diff) | |
parent | 901fe3d7c032d284da5c9bce24c4aaee9054c7ac (diff) | |
download | timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.gz timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.bz2 timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.zip |
Merge pull request #1395 from crupest/dev
Refector 2023 v0.1
Diffstat (limited to 'FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx')
-rw-r--r-- | FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx new file mode 100644 index 00000000..0df10411 --- /dev/null +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -0,0 +1,276 @@ +import { useState, ChangeEvent, ComponentPropsWithoutRef } from "react"; + +import { useC, Text, UiLogicError } from "~src/common"; + +import { useUser } from "~src/services/user"; + +import { getHttpUserClient } from "~src/http/user"; + +import { ImageCropper, useImageCrop } from "~src/components/ImageCropper"; +import BlobImage from "~src/components/BlobImage"; +import { ButtonRowV2 } from "~src/components/button"; +import { + Dialog, + DialogContainer, + useDialogController, +} from "~src/components/dialog"; + +import "./ChangeAvatarDialog.css"; + +export default function ChangeAvatarDialog() { + const c = useC(); + + const user = useUser(); + + const controller = useDialogController(); + + type State = + | "select" + | "crop" + | "process-crop" + | "preview" + | "uploading" + | "success" + | "error"; + const [state, setState] = useState<State>("select"); + + const [file, setFile] = useState<File | null>(null); + + const { canCrop, crop, imageCropperProps } = useImageCrop(file, { + constraint: { + ratio: 1, + }, + }); + + const [resultBlob, setResultBlob] = useState<Blob | null>(null); + const [message, setMessage] = useState<Text>( + "settings.dialogChangeAvatar.prompt.select", + ); + + const close = controller.closeDialog; + + const onSelectFile = (e: ChangeEvent<HTMLInputElement>): void => { + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); + } else { + setFile(files[0]); + } + }; + + const onCropNext = () => { + if (!canCrop) { + throw new UiLogicError(); + } + + setState("process-crop"); + + void crop().then((b) => { + setState("preview"); + setResultBlob(b); + }); + }; + + const onCropPrevious = () => { + setFile(null); + setState("select"); + }; + + const onPreviewPrevious = () => { + setState("crop"); + }; + + const upload = () => { + if (resultBlob == null) { + throw new UiLogicError(); + } + + if (user == null) { + throw new UiLogicError(); + } + + setState("uploading"); + controller.setCanSwitchDialog(false); + getHttpUserClient() + .putAvatar(user.username, resultBlob) + .then( + () => { + setState("success"); + }, + () => { + setState("error"); + setMessage("operationDialog.error"); + }, + ) + .finally(() => { + controller.setCanSwitchDialog(true); + }); + }; + + const cancelButton = { + key: "cancel", + text: "operationDialog.cancel", + onClick: close, + } as const; + + const createPreviousButton = (onClick: () => void) => + ({ + key: "previous", + text: "operationDialog.previousStep", + onClick, + }) as const; + + const buttonsMap: Record< + State, + ComponentPropsWithoutRef<typeof ButtonRowV2>["buttons"] + > = { + select: [ + cancelButton, + { + key: "next", + action: "major", + text: "operationDialog.nextStep", + onClick: () => setState("crop"), + disabled: file == null, + }, + ], + crop: [ + cancelButton, + createPreviousButton(onCropPrevious), + { + key: "next", + action: "major", + text: "operationDialog.nextStep", + onClick: onCropNext, + disabled: !canCrop, + }, + ], + "process-crop": [cancelButton, createPreviousButton(onPreviewPrevious)], + preview: [ + cancelButton, + createPreviousButton(onPreviewPrevious), + { + key: "upload", + action: "major", + text: "settings.dialogChangeAvatar.upload", + onClick: upload, + }, + ], + uploading: [], + success: [ + { + key: "ok", + text: "operationDialog.ok", + color: "create", + onClick: close, + }, + ], + error: [ + cancelButton, + { + key: "retry", + action: "major", + text: "operationDialog.retry", + onClick: upload, + }, + ], + }; + + return ( + <Dialog> + <DialogContainer + title="settings.dialogChangeAvatar.title" + titleColor="primary" + buttonsV2={buttonsMap[state]} + > + {(() => { + if (state === "select") { + return ( + <div className="change-avatar-dialog-container"> + <div className="change-avatar-dialog-prompt"> + {c("settings.dialogChangeAvatar.prompt.select")} + </div> + <input + className="change-avatar-select-input" + type="file" + accept="image/*" + onChange={onSelectFile} + /> + </div> + ); + } else if (state === "crop") { + if (file == null) { + throw new UiLogicError(); + } + return ( + <div className="change-avatar-dialog-container"> + <ImageCropper + {...imageCropperProps} + containerClassName="change-avatar-cropper" + /> + <div className="change-avatar-dialog-prompt"> + {c("settings.dialogChangeAvatar.prompt.crop")} + </div> + </div> + ); + } else if (state === "process-crop") { + return ( + <div className="change-avatar-dialog-container"> + <div className="change-avatar-dialog-prompt"> + {c("settings.dialogChangeAvatar.prompt.processingCrop")} + </div> + </div> + ); + } else if (state === "preview") { + return ( + <div className="change-avatar-dialog-container"> + <BlobImage + className="change-avatar-preview-image" + src={resultBlob} + alt={ + c("settings.dialogChangeAvatar.previewImgAlt") ?? undefined + } + /> + <div className="change-avatar-dialog-prompt"> + {c("settings.dialogChangeAvatar.prompt.preview")} + </div> + </div> + ); + } else if (state === "uploading") { + return ( + <div className="change-avatar-dialog-container"> + <BlobImage + className="change-avatar-preview-image" + src={resultBlob} + /> + <div className="change-avatar-dialog-prompt"> + {c("settings.dialogChangeAvatar.prompt.uploading")} + </div> + </div> + ); + } else if (state === "success") { + return ( + <div className="change-avatar-dialog-container"> + <div className="change-avatar-dialog-prompt success"> + {c("operationDialog.success")} + </div> + </div> + ); + } else { + return ( + <div className="change-avatar-dialog-container"> + <BlobImage + className="change-avatar-preview-image" + src={resultBlob} + /> + <div className="change-avatar-dialog-prompt error"> + {c(message)} + </div> + </div> + ); + } + })()} + </DialogContainer> + </Dialog> + ); +} |