aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-08-28 01:41:41 +0800
committercrupest <crupest@outlook.com>2023-08-28 01:41:41 +0800
commitb66a57071316434356e77e294ec22181e4db54d5 (patch)
tree2c000b459165834562cf420990e683e78fadae6c
parent256cc9592a3f31fc392e1ccdb699aa206b7b47ce (diff)
downloadtimeline-b66a57071316434356e77e294ec22181e4db54d5.tar.gz
timeline-b66a57071316434356e77e294ec22181e4db54d5.tar.bz2
timeline-b66a57071316434356e77e294ec22181e4db54d5.zip
...
-rw-r--r--FrontEnd/src/components/BlobImage.tsx15
-rw-r--r--FrontEnd/src/components/ImageCropper.css15
-rw-r--r--FrontEnd/src/components/ImageCropper.tsx564
-rw-r--r--FrontEnd/src/components/LoadFailReload.tsx37
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.css1
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx34
-rw-r--r--FrontEnd/src/pages/timeline/TimelineCard.tsx2
7 files changed, 394 insertions, 274 deletions
diff --git a/FrontEnd/src/components/BlobImage.tsx b/FrontEnd/src/components/BlobImage.tsx
index 259c2210..cccf2f74 100644
--- a/FrontEnd/src/components/BlobImage.tsx
+++ b/FrontEnd/src/components/BlobImage.tsx
@@ -1,12 +1,13 @@
-import { ComponentPropsWithoutRef, useState, useEffect } from "react";
+import { ComponentPropsWithoutRef, useState, useEffect, useMemo } from "react";
type BlobImageProps = Omit<ComponentPropsWithoutRef<"img">, "src"> & {
imgRef?: React.Ref<HTMLImageElement>;
src?: Blob | string | null;
+ keyBySrc?: boolean;
};
export default function BlobImage(props: BlobImageProps) {
- const { imgRef, src, ...otherProps } = props;
+ const { imgRef, src, keyBySrc, ...otherProps } = props;
const [url, setUrl] = useState<string | null | undefined>(undefined);
@@ -22,5 +23,13 @@ export default function BlobImage(props: BlobImageProps) {
}
}, [src]);
- return <img ref={imgRef} {...otherProps} src={url ?? undefined} />;
+ const key = useMemo(() => {
+ if (keyBySrc) {
+ return url == null ? undefined : btoa(url);
+ } else {
+ return undefined;
+ }
+ }, [url, keyBySrc]);
+
+ return <img key={key} ref={imgRef} {...otherProps} src={url ?? undefined} />;
}
diff --git a/FrontEnd/src/components/ImageCropper.css b/FrontEnd/src/components/ImageCropper.css
index 2c4d0a8c..9631cf1d 100644
--- a/FrontEnd/src/components/ImageCropper.css
+++ b/FrontEnd/src/components/ImageCropper.css
@@ -1,18 +1,17 @@
-.image-cropper-container {
+.cru-image-cropper-container {
position: relative;
box-sizing: border-box;
user-select: none;
}
-.image-cropper-container img {
- position: absolute;
+.cru-image-cropper-container img {
left: 0;
top: 0;
width: 100%;
height: 100%;
}
-.image-cropper-mask-container {
+.cru-image-cropper-mask-container {
position: absolute;
left: 0;
top: 0;
@@ -21,18 +20,16 @@
overflow: hidden;
}
-.image-cropper-mask {
+.cru-image-cropper-mask {
position: absolute;
box-shadow: 0 0 0 10000px rgba(255, 255, 255, 0.8);
touch-action: none;
}
-.image-cropper-handler {
+.cru-image-cropper-handler {
position: absolute;
- width: 26px;
- height: 26px;
border: black solid 2px;
border-radius: 50%;
background: white;
touch-action: none;
-}
+} \ No newline at end of file
diff --git a/FrontEnd/src/components/ImageCropper.tsx b/FrontEnd/src/components/ImageCropper.tsx
index f23994e2..1f530004 100644
--- a/FrontEnd/src/components/ImageCropper.tsx
+++ b/FrontEnd/src/components/ImageCropper.tsx
@@ -1,312 +1,396 @@
-import * as React from "react";
+import {
+ useState,
+ useRef,
+ SyntheticEvent,
+ PointerEvent,
+ useMemo,
+ MutableRefObject,
+} from "react";
import classnames from "classnames";
-import { UiLogicError } from "~src/common";
+import { UiLogicError } from "./common";
+import BlobImage from "./BlobImage";
import "./ImageCropper.css";
-import BlobImage from "./BlobImage";
+// All in natural size of image.
export interface Clip {
left: number;
top: number;
width: number;
+ height: number;
}
-interface NormailizedClip extends Clip {
- height: number;
+export function applyClipToImage(
+ image: HTMLImageElement,
+ clip: Clip,
+ mimeType: string,
+): Promise<Blob> {
+ return new Promise((resolve, reject) => {
+ const canvas = document.createElement("canvas");
+ canvas.width = clip.width;
+ canvas.height = clip.height;
+ const context = canvas.getContext("2d");
+
+ if (context == null) throw new Error("Failed to create context.");
+
+ context.drawImage(
+ image,
+ clip.left,
+ clip.top,
+ clip.width,
+ clip.height,
+ 0,
+ 0,
+ clip.width,
+ clip.height,
+ );
+
+ canvas.toBlob((blob) => {
+ if (blob == null) {
+ reject(new Error("canvas.toBlob returns null"));
+ } else {
+ resolve(blob);
+ }
+ }, mimeType);
+ });
+}
+
+interface Movement {
+ x: number;
+ y: number;
}
interface ImageInfo {
+ element: HTMLImageElement;
width: number;
height: number;
- landscape: boolean;
ratio: number;
- maxClipWidth: number;
- maxClipHeight: number;
+ landscape: boolean;
}
-interface ImageCropperSavedState {
- clip: NormailizedClip;
- x: number;
- y: number;
- pointerId: number;
+export interface CropConstraint {
+ ratio?: number;
+ // minClipWidth?: number;
+ // minClipHeight?: number;
+ // maxClipWidth?: number;
+ // maxClipHeight?: number;
}
-export interface ImageCropperProps {
- clip: Clip | null;
- image: string | Blob;
- onChange: (clip: Clip) => void;
- imageElementCallback?: (element: HTMLImageElement | null) => void;
- className?: string;
+function generateImageInfo(
+ imageElement: HTMLImageElement | null,
+): ImageInfo | null {
+ if (imageElement == null) return null;
+
+ const { naturalWidth, naturalHeight } = imageElement;
+ const imageRatio = naturalHeight / naturalWidth;
+
+ return {
+ element: imageElement,
+ width: naturalWidth,
+ height: naturalHeight,
+ ratio: imageRatio,
+ landscape: imageRatio < 1,
+ };
}
-const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
- const { clip, image, onChange, imageElementCallback, className } = props;
+const emptyClip: Clip = {
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0,
+};
- const [oldState, setOldState] = React.useState<ImageCropperSavedState | null>(
- null,
- );
- const [imageInfo, setImageInfo] = React.useState<ImageInfo | null>(null);
+const allClip : Clip = {
+ left: 0,
+ top: 0,
+ width: Number.MAX_VALUE,
+ height: Number.MAX_VALUE,
+}
- const normalizeClip = (c: Clip | null | undefined): NormailizedClip => {
- if (c == null) {
- return { left: 0, top: 0, width: 0, height: 0 };
+// TODO: Continue here... mode...
+function adjustClip(
+ clip: Clip,
+ mode: "move" | "resize" | "both",
+ imageSize: { width: number; height: number },
+ targetRatio?: number | null | undefined,
+): Clip {
+ class ClipGeometry {
+ constructor(
+ public left: number,
+ public top: number,
+ public width: number,
+ public height: number,
+ ) {}
+
+ get right(): number {
+ return this.left + this.width;
}
- return {
- left: c.left || 0,
- top: c.top || 0,
- width: c.width || 0,
- height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0,
- };
- };
+ set right(value: number) {
+ this.width = this.left + value;
+ }
- const c = normalizeClip(clip);
+ get bottom(): number {
+ return this.top + this.height;
+ }
- const imgElementRef = React.useRef<HTMLImageElement | null>(null);
+ set bottom(value: number) {
+ this.height = this.top + value;
+ }
- const onImageRef = React.useCallback(
- (e: HTMLImageElement | null) => {
- imgElementRef.current = e;
- if (imageElementCallback != null && e == null) {
- imageElementCallback(null);
- }
- },
- [imageElementCallback],
- );
+ get ratio(): number {
+ return this.height / this.width;
+ }
- const onImageLoad = React.useCallback(
- (e: React.SyntheticEvent<HTMLImageElement>) => {
- const img = e.currentTarget;
- const landscape = img.naturalWidth >= img.naturalHeight;
-
- const info = {
- width: img.naturalWidth,
- height: img.naturalHeight,
- landscape,
- ratio: img.naturalHeight / img.naturalWidth,
- maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1,
- maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight,
+ toClip(): Clip {
+ return {
+ left: this.left,
+ top: this.top,
+ width: this.width,
+ height: this.height,
};
- setImageInfo(info);
- onChange({ left: 0, top: 0, width: info.maxClipWidth });
- if (imageElementCallback != null) {
- imageElementCallback(img);
- }
- },
- [onChange, imageElementCallback],
- );
+ }
+ }
- const onPointerDown = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState != null) return;
- e.currentTarget.setPointerCapture(e.pointerId);
- setOldState({
- x: e.clientX,
- y: e.clientY,
- clip: c,
- pointerId: e.pointerId,
- });
- },
- [oldState, c],
+ const clipGeometry = new ClipGeometry(
+ clip.left,
+ clip.top,
+ clip.width,
+ clip.height,
);
- const onPointerUp = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null || oldState.pointerId !== e.pointerId) return;
- e.currentTarget.releasePointerCapture(e.pointerId);
- setOldState(null);
- },
- [oldState],
- );
-
- const onPointerMove = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null) return;
-
- const oldClip = oldState.clip;
-
- const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
-
- const { current: imgElement } = imgElementRef;
-
- if (imgElement == null) throw new UiLogicError("Image element is null.");
+ // Make clip in image.
+ clipGeometry.left = Math.max(clipGeometry.left, 0);
+ clipGeometry.top = Math.max(clipGeometry.top, 0);
+ clipGeometry.right = Math.min(clipGeometry.right, imageSize.width);
+ clipGeometry.bottom = Math.min(clipGeometry.bottom, imageSize.height);
+
+ // Make image "positive"
+ if (clipGeometry.right < clipGeometry.left) {
+ clipGeometry.right = clipGeometry.left;
+ }
+ if (clipGeometry.bottom < clipGeometry.top) {
+ clipGeometry.bottom = clipGeometry.top;
+ }
+
+ // Now correct ratio
+ const currentRatio = clipGeometry.ratio;
+ if (targetRatio != null && targetRatio > 0 && currentRatio !== targetRatio) {
+ if (currentRatio < targetRatio) {
+ // too wide
+ clipGeometry.width = clipGeometry.height / targetRatio;
+ } else {
+ clipGeometry.height = clipGeometry.width * targetRatio;
+ }
+ }
- const moveRatio = {
- x: movement.x / imgElement.width,
- y: movement.y / imgElement.height,
- };
+ return clipGeometry.toClip();
+}
- const newRatio = {
- x: oldClip.left + moveRatio.x,
- y: oldClip.top + moveRatio.y,
- };
- if (newRatio.x < 0) {
- newRatio.x = 0;
- } else if (newRatio.x > 1 - oldClip.width) {
- newRatio.x = 1 - oldClip.width;
- }
- if (newRatio.y < 0) {
- newRatio.y = 0;
- } else if (newRatio.y > 1 - oldClip.height) {
- newRatio.y = 1 - oldClip.height;
- }
+interface ImageCropperProps {
+ clip: Clip;
+ image: Blob | string | null;
+ imageElementRef: MutableRefObject<HTMLImageElement | null>;
+ onImageLoad: (event: SyntheticEvent<HTMLImageElement>) => void;
+ onMove: (movement: Movement) => void;
+ onResize: (movement: Movement) => void;
+ containerClassName?: string;
+}
- onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width });
+export function useImageCrop(
+ file: File | null,
+ options?: {
+ constraint?: CropConstraint;
+ },
+): {
+ clip: Clip;
+ setClip: (clip: Clip) => void;
+ canCrop: boolean;
+ crop: () => Promise<Blob>;
+ imageCropperProps: ImageCropperProps;
+} {
+ const targetRatio = options?.constraint?.ratio;
+
+ const imageElementRef = useRef<HTMLImageElement | null>(null);
+ const [image, setImage] = useState<ImageInfo | null>(null);
+ const [clip, setClip] = useState<Clip>(emptyClip );
+
+ if (imageElementRef.current == null && image != null) {
+ setImage(null);
+ setClip(emptyClip);
+ }
+
+ const canCrop = file != null && image != null;
+
+ const adjustedClip = useMemo(() => {
+ return image == null ? emptyClip : adjustClip(clip, image, targetRatio);
+ }, [clip, image, targetRatio]);
+
+ return {
+ clip,
+ setClip,
+ canCrop,
+ crop() {
+ if (!canCrop) throw new UiLogicError();
+ return applyClipToImage(image.element, adjustedClip, file.type);
},
- [oldState, onChange],
- );
-
- const onHandlerPointerMove = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null) return;
+ imageCropperProps: {
+ clip: adjustedClip,
+ image: file,
+ imageElementRef: imageElementRef,
+ // TODO: Continue here...
+ onMove: ,
+ onResize: ,
+ onImageLoad: () => {
+ const image = generateImageInfo(imageElementRef.current);
+ setImage(image);
+ setClip(adjustClip(allClip, "both", image, targetRatio));
+ },
+ },
+ };
+}
- const oldClip = oldState.clip;
+interface PointerState {
+ x: number;
+ y: number;
+ pointerId: number;
+}
- const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
+const imageCropperHandlerSize = 15;
- const ratio = imageInfo == null ? 1 : imageInfo.ratio;
+export function ImageCropper(props: ImageCropperProps) {
+ function convertClipToElement(
+ clip: Clip,
+ imageElement: HTMLImageElement,
+ ): Clip {
+ const xRatio = imageElement.clientWidth / imageElement.naturalWidth;
+ const yRatio = imageElement.clientHeight / imageElement.naturalHeight;
+ return {
+ left: xRatio * clip.left,
+ top: yRatio * clip.top,
+ width: xRatio * clip.width,
+ height: yRatio * clip.height,
+ };
+ }
+
+ function convertMovementFromElement(
+ move: Movement,
+ imageElement: HTMLImageElement,
+ ): Movement {
+ const xRatio = imageElement.naturalWidth / imageElement.clientWidth;
+ const yRatio = imageElement.naturalHeight / imageElement.clientHeight;
+ return {
+ x: xRatio * move.x,
+ y: yRatio * move.y,
+ };
+ }
+
+ const {
+ clip,
+ image,
+ imageElementRef,
+ onImageLoad,
+ onMove,
+ onResize,
+ containerClassName,
+ } = props;
+
+ const pointerStateRef = useRef<PointerState | null>(null);
+
+ const clipInElement =
+ imageElementRef.current != null
+ ? convertClipToElement(clip, imageElementRef.current)
+ : emptyClip;
+
+ const actOnMovement = (
+ e: PointerEvent,
+ change: (movement: Movement) => void,
+ ) => {
+ if (
+ imageElementRef.current == null ||
+ pointerStateRef.current == null ||
+ pointerStateRef.current.pointerId != e.pointerId
+ ) {
+ return;
+ }
- const { current: imgElement } = imgElementRef;
+ const { x, y } = pointerStateRef.current;
- if (imgElement == null) throw new UiLogicError("Image element is null.");
+ const movement = {
+ x: e.clientX - x,
+ y: e.clientY - y,
+ };
- const moveRatio = {
- x: movement.x / imgElement.width,
- y: movement.x / imgElement.width / ratio,
- };
+ change(movement);
+ };
- const newRatio = {
- x: oldClip.width + moveRatio.x,
- y: oldClip.height + moveRatio.y,
- };
+ const onPointerDown = (e: PointerEvent) => {
+ if (imageElementRef.current == null || pointerStateRef.current != null)
+ return;
- const maxRatio = {
- x: Math.min(1 - oldClip.left, newRatio.x),
- y: Math.min(1 - oldClip.top, newRatio.y),
- };
+ e.currentTarget.setPointerCapture(e.pointerId);
- const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio);
+ pointerStateRef.current = {
+ x: e.clientX,
+ y: e.clientY,
+ pointerId: e.pointerId,
+ };
+ };
- let newWidth;
- if (newRatio.x < 0) {
- newWidth = 0;
- } else if (newRatio.x > maxWidthRatio) {
- newWidth = maxWidthRatio;
- } else {
- newWidth = newRatio.x;
- }
+ const onPointerUp = (e: PointerEvent) => {
+ if (
+ pointerStateRef.current == null ||
+ pointerStateRef.current.pointerId != e.pointerId
+ ) {
+ return;
+ }
- onChange({ left: oldClip.left, top: oldClip.top, width: newWidth });
- },
- [imageInfo, oldState, onChange],
- );
+ e.currentTarget.releasePointerCapture(e.pointerId);
+ pointerStateRef.current = null;
+ };
- const toPercentage = (n: number): string => `${n}%`;
+ const onMaskPointerMove = (e: PointerEvent) => {
+ actOnMovement(e, onMove);
+ };
- // fuck!!! I just can't find a better way to implement this in pure css
- const containerStyle: React.CSSProperties = (() => {
- if (imageInfo == null) {
- return { width: "100%", paddingTop: "100%", height: 0 };
- } else {
- if (imageInfo.ratio > 1) {
- return {
- width: toPercentage(100 / imageInfo.ratio),
- paddingTop: "100%",
- height: 0,
- };
- } else {
- return {
- width: "100%",
- paddingTop: toPercentage(100 * imageInfo.ratio),
- height: 0,
- };
- }
- }
- })();
+ const onResizeHandlerPointerMove = (e: PointerEvent) => {
+ actOnMovement(e, onResize);
+ };
return (
<div
- className={classnames("image-cropper-container", className)}
- style={containerStyle}
+ className={classnames("cru-image-cropper-container", containerClassName)}
>
- <BlobImage
- imgRef={onImageRef}
- src={image}
- onLoad={onImageLoad}
- alt="to crop"
- />
- <div className="image-cropper-mask-container">
+ <BlobImage imgRef={imageElementRef} src={image} onLoad={onImageLoad} />
+ <div className="cru-image-cropper-mask-container">
<div
- className="image-cropper-mask"
+ className="cru-image-cropper-mask"
style={{
- left: toPercentage(c.left * 100),
- top: toPercentage(c.top * 100),
- width: toPercentage(c.width * 100),
- height: toPercentage(c.height * 100),
+ left: clipInElement.left,
+ top: clipInElement.top,
+ width: clipInElement.width,
+ height: clipInElement.height,
}}
- onPointerMove={onPointerMove}
+ onPointerMove={onMaskPointerMove}
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
/>
</div>
<div
- className="image-cropper-handler"
+ className="cru-image-cropper-handler"
style={{
- left: `calc(${(c.left + c.width) * 100}% - 15px)`,
- top: `calc(${(c.top + c.height) * 100}% - 15px)`,
+ left:
+ clipInElement.left + clipInElement.width - imageCropperHandlerSize,
+ top:
+ clipInElement.top + clipInElement.height - imageCropperHandlerSize,
+ width: imageCropperHandlerSize * 2,
+ height: imageCropperHandlerSize * 2,
}}
- onPointerMove={onHandlerPointerMove}
+ onPointerMove={onResizeHandlerPointerMove}
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
/>
</div>
);
-};
-
-export default ImageCropper;
-
-export function applyClipToImage(
- image: HTMLImageElement,
- clip: Clip,
- mimeType: string,
-): Promise<Blob> {
- return new Promise((resolve, reject) => {
- const naturalSize = {
- width: image.naturalWidth,
- height: image.naturalHeight,
- };
- const clipArea = {
- x: naturalSize.width * clip.left,
- y: naturalSize.height * clip.top,
- length: naturalSize.width * clip.width,
- };
-
- const canvas = document.createElement("canvas");
- canvas.width = clipArea.length;
- canvas.height = clipArea.length;
- const context = canvas.getContext("2d");
-
- if (context == null) throw new Error("Failed to create context.");
-
- context.drawImage(
- image,
- clipArea.x,
- clipArea.y,
- clipArea.length,
- clipArea.length,
- 0,
- 0,
- clipArea.length,
- clipArea.length,
- );
-
- canvas.toBlob((blob) => {
- if (blob == null) {
- reject(new Error("canvas.toBlob returns null"));
- } else {
- resolve(blob);
- }
- }, mimeType);
- });
}
diff --git a/FrontEnd/src/components/LoadFailReload.tsx b/FrontEnd/src/components/LoadFailReload.tsx
new file mode 100644
index 00000000..81ba1f67
--- /dev/null
+++ b/FrontEnd/src/components/LoadFailReload.tsx
@@ -0,0 +1,37 @@
+import * as React from "react";
+import { Trans } from "react-i18next";
+
+export interface LoadFailReloadProps {
+ className?: string;
+ style?: React.CSSProperties;
+ onReload: () => void;
+}
+
+const LoadFailReload: React.FC<LoadFailReloadProps> = ({
+ onReload,
+ className,
+ style,
+}) => {
+ return (
+ <Trans
+ i18nKey="loadFailReload"
+ parent="div"
+ className={className}
+ style={style}
+ >
+ 0
+ <a
+ href="#"
+ onClick={(e) => {
+ onReload();
+ e.preventDefault();
+ }}
+ >
+ 1
+ </a>
+ 2
+ </Trans>
+ );
+};
+
+export default LoadFailReload;
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.css b/FrontEnd/src/pages/setting/ChangeAvatarDialog.css
index 2aa0bb54..c9eb8011 100644
--- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.css
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.css
@@ -11,6 +11,7 @@
}
.change-avatar-cropper {
+ max-width: 400px;
max-height: 400px;
}
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
index 011c5059..2fcfef2c 100644
--- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
@@ -6,10 +6,7 @@ import { useUser } from "~src/services/user";
import { getHttpUserClient } from "~src/http/user";
-import ImageCropper, {
- Clip,
- applyClipToImage,
-} from "~src/components/ImageCropper";
+import { ImageCropper, useImageCrop } from "~src/components/ImageCropper";
import BlobImage from "~src/components/BlobImage";
import { ButtonRowV2 } from "~src/components/button";
import { Dialog, DialogContainer } from "~src/components/dialog";
@@ -40,10 +37,13 @@ export default function ChangeAvatarDialog({
const [state, setState] = useState<State>("select");
const [file, setFile] = useState<File | null>(null);
- const [clip, setClip] = useState<Clip | null>(null);
- const [cropImgElement, setCropImgElement] = useState<HTMLImageElement | 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",
@@ -65,18 +65,13 @@ export default function ChangeAvatarDialog({
};
const onCropNext = () => {
- if (
- cropImgElement == null ||
- clip == null ||
- clip.width === 0 ||
- file == null
- ) {
+ if (!canCrop) {
throw new UiLogicError();
}
setState("process-crop");
- void applyClipToImage(cropImgElement, clip, file.type).then((b) => {
+ void crop().then((b) => {
setState("preview");
setResultBlob(b);
});
@@ -151,7 +146,7 @@ export default function ChangeAvatarDialog({
action: "primary",
text: "operationDialog.nextStep",
onClick: onCropNext,
- disabled: cropImgElement == null || clip == null || clip.width === 0,
+ disabled: !canCrop,
},
],
"process-crop": [cancelButton, createPreviousButton(onPreviewPrevious)],
@@ -214,11 +209,8 @@ export default function ChangeAvatarDialog({
return (
<div className="change-avatar-dialog-container">
<ImageCropper
- className="change-avatar-cropper"
- clip={clip}
- onChange={setClip}
- image={file}
- imageElementCallback={setCropImgElement}
+ {...imageCropperProps}
+ containerClassName="change-avatar-cropper"
/>
<div className="change-avatar-dialog-prompt">
{c("settings.dialogChangeAvatar.prompt.crop")}
diff --git a/FrontEnd/src/pages/timeline/TimelineCard.tsx b/FrontEnd/src/pages/timeline/TimelineCard.tsx
index 82d6d350..f17a3ce9 100644
--- a/FrontEnd/src/pages/timeline/TimelineCard.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineCard.tsx
@@ -7,7 +7,7 @@ import { pushAlert } from "~src/services/alert";
import { HttpTimelineInfo } from "~src/http/timeline";
import { getHttpBookmarkClient } from "~src/http/bookmark";
-import { useMobile } from "~src/components/common";
+import { useMobile } from "~src/components/hooks";
import { Dialog, useDialog } from "~src/components/dialog";
import UserAvatar from "~src/components/user/UserAvatar";
import PopupMenu from "~src/components/menu/PopupMenu";