From f5dfd52f6efece2f4cad227044ecf4dd66301bbc Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 26 Aug 2023 21:36:58 +0800 Subject: ... --- FrontEnd/src/components/LoadFailReload.tsx | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 FrontEnd/src/components/LoadFailReload.tsx (limited to 'FrontEnd/src/components/LoadFailReload.tsx') 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 = ({ + onReload, + className, + style, +}) => { + return ( + + 0 + { + onReload(); + e.preventDefault(); + }} + > + 1 + + 2 + + ); +}; + +export default LoadFailReload; -- cgit v1.2.3 From 256cc9592a3f31fc392e1ccdb699aa206b7b47ce Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 26 Aug 2023 23:49:28 +0800 Subject: ... --- FrontEnd/src/common.ts | 2 - FrontEnd/src/components/AppBar.tsx | 3 +- FrontEnd/src/components/Card.tsx | 1 + FrontEnd/src/components/LoadFailReload.tsx | 37 ------------------ FrontEnd/src/components/LoadingPage.tsx | 13 ------- FrontEnd/src/components/SearchInput.tsx | 2 +- FrontEnd/src/components/button/ButtonRowV2.tsx | 3 +- FrontEnd/src/components/button/LoadingButton.tsx | 1 - FrontEnd/src/components/common.ts | 1 - FrontEnd/src/components/dialog/ConfirmDialog.css | 0 FrontEnd/src/components/dialog/ConfirmDialog.tsx | 1 - FrontEnd/src/components/dialog/OperationDialog.tsx | 7 +--- FrontEnd/src/components/dialog/index.ts | 1 + FrontEnd/src/components/hooks.ts | 14 ------- FrontEnd/src/components/hooks/index.ts | 3 ++ FrontEnd/src/components/hooks/responsive.ts | 7 ++++ FrontEnd/src/components/hooks/useClickOutside.ts | 38 ++++++++++++++++++ FrontEnd/src/components/hooks/useScrollToBottom.ts | 44 +++++++++++++++++++++ FrontEnd/src/components/input/InputGroup.tsx | 16 ++++---- FrontEnd/src/components/menu/Menu.tsx | 2 +- FrontEnd/src/components/menu/PopupMenu.tsx | 6 +-- FrontEnd/src/pages/login/index.tsx | 14 ++----- FrontEnd/src/pages/register/index.tsx | 26 +++++-------- FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 5 +-- .../src/pages/setting/ChangePasswordDialog.tsx | 17 ++++---- FrontEnd/src/pages/setting/index.tsx | 11 +++--- FrontEnd/src/pages/timeline/Timeline.tsx | 3 +- .../src/pages/timeline/TimelineDeleteDialog.tsx | 4 +- FrontEnd/src/pages/timeline/TimelinePostView.tsx | 8 ++-- FrontEnd/src/utilities/hooks.ts | 5 --- FrontEnd/src/utilities/hooks/mediaQuery.ts | 5 --- FrontEnd/src/utilities/hooks/useClickOutside.ts | 38 ------------------ FrontEnd/src/utilities/hooks/useScrollToBottom.ts | 45 ---------------------- 33 files changed, 149 insertions(+), 234 deletions(-) delete mode 100644 FrontEnd/src/components/LoadFailReload.tsx delete mode 100644 FrontEnd/src/components/LoadingPage.tsx delete mode 100644 FrontEnd/src/components/dialog/ConfirmDialog.css delete mode 100644 FrontEnd/src/components/hooks.ts create mode 100644 FrontEnd/src/components/hooks/index.ts create mode 100644 FrontEnd/src/components/hooks/responsive.ts create mode 100644 FrontEnd/src/components/hooks/useClickOutside.ts create mode 100644 FrontEnd/src/components/hooks/useScrollToBottom.ts delete mode 100644 FrontEnd/src/utilities/hooks.ts delete mode 100644 FrontEnd/src/utilities/hooks/mediaQuery.ts delete mode 100644 FrontEnd/src/utilities/hooks/useClickOutside.ts delete mode 100644 FrontEnd/src/utilities/hooks/useScrollToBottom.ts (limited to 'FrontEnd/src/components/LoadFailReload.tsx') diff --git a/FrontEnd/src/common.ts b/FrontEnd/src/common.ts index 7c053140..1ca796c3 100644 --- a/FrontEnd/src/common.ts +++ b/FrontEnd/src/common.ts @@ -3,8 +3,6 @@ // This error should never occur. If it does, it indicates there is some logic bug in codes. export class UiLogicError extends Error {} -export const highlightTimelineUsername = "crupest"; - export type { I18nText } from "./i18n"; export type { I18nText as Text } from "./i18n"; export { c, convertI18nText } from "./i18n"; diff --git a/FrontEnd/src/components/AppBar.tsx b/FrontEnd/src/components/AppBar.tsx index da3a946f..1a5c1941 100644 --- a/FrontEnd/src/components/AppBar.tsx +++ b/FrontEnd/src/components/AppBar.tsx @@ -2,9 +2,10 @@ import { useState } from "react"; import classnames from "classnames"; import { Link, NavLink } from "react-router-dom"; -import { I18nText, useC, useMobile } from "./common"; import { useUser } from "~src/services/user"; +import { I18nText, useC } from "./common"; +import { useMobile } from "./hooks"; import TimelineLogo from "./TimelineLogo"; import { IconButton } from "./button"; import UserAvatar from "./user/UserAvatar"; diff --git a/FrontEnd/src/components/Card.tsx b/FrontEnd/src/components/Card.tsx index a8f0d3cc..5d3ef630 100644 --- a/FrontEnd/src/components/Card.tsx +++ b/FrontEnd/src/components/Card.tsx @@ -2,6 +2,7 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; import { ThemeColor } from "./common"; + import "./Card.css"; interface CardProps extends ComponentPropsWithoutRef<"div"> { diff --git a/FrontEnd/src/components/LoadFailReload.tsx b/FrontEnd/src/components/LoadFailReload.tsx deleted file mode 100644 index 81ba1f67..00000000 --- a/FrontEnd/src/components/LoadFailReload.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from "react"; -import { Trans } from "react-i18next"; - -export interface LoadFailReloadProps { - className?: string; - style?: React.CSSProperties; - onReload: () => void; -} - -const LoadFailReload: React.FC = ({ - onReload, - className, - style, -}) => { - return ( - - 0 - { - onReload(); - e.preventDefault(); - }} - > - 1 - - 2 - - ); -}; - -export default LoadFailReload; diff --git a/FrontEnd/src/components/LoadingPage.tsx b/FrontEnd/src/components/LoadingPage.tsx deleted file mode 100644 index 35ee1aa8..00000000 --- a/FrontEnd/src/components/LoadingPage.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from "react"; - -import Spinner from "./Spinner"; - -const LoadingPage: React.FC = () => { - return ( -
- -
- ); -}; - -export default LoadingPage; diff --git a/FrontEnd/src/components/SearchInput.tsx b/FrontEnd/src/components/SearchInput.tsx index e3216b86..71820bfa 100644 --- a/FrontEnd/src/components/SearchInput.tsx +++ b/FrontEnd/src/components/SearchInput.tsx @@ -1,7 +1,7 @@ import classNames from "classnames"; import { useC, Text } from "./common"; -import LoadingButton from "./button/LoadingButton"; +import { LoadingButton } from "./button"; import "./SearchInput.css"; diff --git a/FrontEnd/src/components/button/ButtonRowV2.tsx b/FrontEnd/src/components/button/ButtonRowV2.tsx index 3467ad52..5129e7f1 100644 --- a/FrontEnd/src/components/button/ButtonRowV2.tsx +++ b/FrontEnd/src/components/button/ButtonRowV2.tsx @@ -1,13 +1,14 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; +import { Text, ThemeColor } from "../common"; + import Button from "./Button"; import FlatButton from "./FlatButton"; import IconButton from "./IconButton"; import LoadingButton from "./LoadingButton"; import "./ButtonRow.css"; -import { Text, ThemeColor } from "../common"; interface ButtonRowV2ButtonBase { key: string | number; diff --git a/FrontEnd/src/components/button/LoadingButton.tsx b/FrontEnd/src/components/button/LoadingButton.tsx index 7e7d08e6..d9d41ddb 100644 --- a/FrontEnd/src/components/button/LoadingButton.tsx +++ b/FrontEnd/src/components/button/LoadingButton.tsx @@ -1,7 +1,6 @@ import classNames from "classnames"; import { I18nText, ThemeColor, useC } from "../common"; - import Spinner from "../Spinner"; import "./LoadingButton.css"; diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts index e6f7319f..b96388ab 100644 --- a/FrontEnd/src/components/common.ts +++ b/FrontEnd/src/components/common.ts @@ -11,4 +11,3 @@ export const themeColors = [ export type ThemeColor = (typeof themeColors)[number]; export { breakpoints } from "./breakpoints"; -export { useMobile } from "./hooks"; diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.css b/FrontEnd/src/components/dialog/ConfirmDialog.css deleted file mode 100644 index e69de29b..00000000 diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.tsx b/FrontEnd/src/components/dialog/ConfirmDialog.tsx index 26939c9b..1d997305 100644 --- a/FrontEnd/src/components/dialog/ConfirmDialog.tsx +++ b/FrontEnd/src/components/dialog/ConfirmDialog.tsx @@ -1,5 +1,4 @@ import { useC, Text, ThemeColor } from "../common"; - import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx index e5db7f4f..96766825 100644 --- a/FrontEnd/src/components/dialog/OperationDialog.tsx +++ b/FrontEnd/src/components/dialog/OperationDialog.tsx @@ -2,23 +2,18 @@ import { useState, ReactNode, ComponentProps } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; - import { useInputs, InputGroup, Initializer as InputInitializer, - InputValueDict, - InputErrorDict, InputConfirmValueDict, } from "../input"; +import { ButtonRow } from "../button"; import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; -import { ButtonRow } from "../button"; import "./OperationDialog.css"; -export type { InputInitializer, InputValueDict, InputErrorDict }; - interface OperationDialogPromptProps { message?: Text; customMessage?: Text; diff --git a/FrontEnd/src/components/dialog/index.ts b/FrontEnd/src/components/dialog/index.ts index 59f15791..17db8fd0 100644 --- a/FrontEnd/src/components/dialog/index.ts +++ b/FrontEnd/src/components/dialog/index.ts @@ -4,6 +4,7 @@ export { default as Dialog } from "./Dialog"; export { default as FullPageDialog } from "./FullPageDialog"; export { default as OperationDialog } from "./OperationDialog"; export { default as ConfirmDialog } from "./ConfirmDialog"; +export { default as DialogContainer } from "./DialogContainer"; type DialogMap = { [K in D]: V; diff --git a/FrontEnd/src/components/hooks.ts b/FrontEnd/src/components/hooks.ts deleted file mode 100644 index 523a4538..00000000 --- a/FrontEnd/src/components/hooks.ts +++ /dev/null @@ -1,14 +0,0 @@ -// TODO: Migrate hooks - -export { - useIsSmallScreen, - useClickOutside, - useScrollToBottom, -} from "~src/utilities/hooks"; - -import { useMediaQuery } from "react-responsive"; -import { breakpoints } from "./breakpoints"; - -export function useMobile(): boolean { - return useMediaQuery({ maxWidth: breakpoints.sm }); -} diff --git a/FrontEnd/src/components/hooks/index.ts b/FrontEnd/src/components/hooks/index.ts new file mode 100644 index 00000000..3c9859bc --- /dev/null +++ b/FrontEnd/src/components/hooks/index.ts @@ -0,0 +1,3 @@ +export { useMobile } from "./responsive"; +export { default as useClickOutside } from "./useClickOutside"; +export { default as useScrollToBottom } from "./useScrollToBottom"; diff --git a/FrontEnd/src/components/hooks/responsive.ts b/FrontEnd/src/components/hooks/responsive.ts new file mode 100644 index 00000000..6bcce96c --- /dev/null +++ b/FrontEnd/src/components/hooks/responsive.ts @@ -0,0 +1,7 @@ +import { useMediaQuery } from "react-responsive"; + +import { breakpoints } from "../breakpoints"; + +export function useMobile(): boolean { + return useMediaQuery({ maxWidth: breakpoints.sm }); +} diff --git a/FrontEnd/src/components/hooks/useClickOutside.ts b/FrontEnd/src/components/hooks/useClickOutside.ts new file mode 100644 index 00000000..828ce7e3 --- /dev/null +++ b/FrontEnd/src/components/hooks/useClickOutside.ts @@ -0,0 +1,38 @@ +import { useRef, useEffect } from "react"; + +export default function useClickOutside( + element: HTMLElement | null | undefined, + onClickOutside: () => void, + nextTick?: boolean, +): void { + const onClickOutsideRef = useRef<() => void>(onClickOutside); + + useEffect(() => { + onClickOutsideRef.current = onClickOutside; + }, [onClickOutside]); + + useEffect(() => { + if (element != null) { + const handler = (event: MouseEvent): void => { + let e: HTMLElement | null = event.target as HTMLElement; + while (e) { + if (e == element) { + return; + } + e = e.parentElement; + } + onClickOutsideRef.current(); + }; + if (nextTick) { + setTimeout(() => { + document.addEventListener("click", handler); + }); + } else { + document.addEventListener("click", handler); + } + return () => { + document.removeEventListener("click", handler); + }; + } + }, [element, nextTick]); +} diff --git a/FrontEnd/src/components/hooks/useScrollToBottom.ts b/FrontEnd/src/components/hooks/useScrollToBottom.ts new file mode 100644 index 00000000..79fcda16 --- /dev/null +++ b/FrontEnd/src/components/hooks/useScrollToBottom.ts @@ -0,0 +1,44 @@ +import { useRef, useEffect } from "react"; +import { fromEvent, filter, throttleTime } from "rxjs"; + +function useScrollToBottom( + handler: () => void, + enable = true, + option = { + maxOffset: 5, + throttle: 1000, + }, +): void { + const handlerRef = useRef<(() => void) | null>(null); + + useEffect(() => { + handlerRef.current = handler; + + return () => { + handlerRef.current = null; + }; + }, [handler]); + + useEffect(() => { + const subscription = fromEvent(window, "scroll") + .pipe( + filter( + () => + window.scrollY >= + document.body.scrollHeight - window.innerHeight - option.maxOffset, + ), + throttleTime(option.throttle), + ) + .subscribe(() => { + if (enable) { + handlerRef.current?.(); + } + }); + + return () => { + subscription.unsubscribe(); + }; + }, [enable, option.maxOffset, option.throttle]); +} + +export default useScrollToBottom; diff --git a/FrontEnd/src/components/input/InputGroup.tsx b/FrontEnd/src/components/input/InputGroup.tsx index 4f487344..47a43b38 100644 --- a/FrontEnd/src/components/input/InputGroup.tsx +++ b/FrontEnd/src/components/input/InputGroup.tsx @@ -72,12 +72,9 @@ export type InputDirtyDict = Record; // use never so you don't have to cast everywhere export type InputConfirmValueDict = Record; -export type GeneralInputErrorDict = - | { - [key: string]: Text | null | undefined; - } - | null - | undefined; +export type GeneralInputErrorDict = { + [key: string]: Text | null | undefined; +}; type MakeInputInfo = Omit; @@ -87,8 +84,9 @@ export type InputInfo = { export type Validator = ( values: InputValueDict, + errors: GeneralInputErrorDict, inputs: InputInfo[], -) => GeneralInputErrorDict; +) => void; export type InputScheme = { inputs: InputInfo[]; @@ -157,7 +155,9 @@ function validate( values: InputValueDict, inputs: InputInfo[], ): InputErrorDict { - return cleanObject(validator?.(values, inputs) ?? {}); + const errors: GeneralInputErrorDict = {}; + validator?.(values, errors, inputs); + return cleanObject(errors); } export function useInputs(options: { init: Initializer }): { diff --git a/FrontEnd/src/components/menu/Menu.tsx b/FrontEnd/src/components/menu/Menu.tsx index e8099c76..c01c6cfb 100644 --- a/FrontEnd/src/components/menu/Menu.tsx +++ b/FrontEnd/src/components/menu/Menu.tsx @@ -2,9 +2,9 @@ import { CSSProperties } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; +import Icon from "../Icon"; import "./Menu.css"; -import Icon from "../Icon"; export type MenuItem = | { diff --git a/FrontEnd/src/components/menu/PopupMenu.tsx b/FrontEnd/src/components/menu/PopupMenu.tsx index 23a67f79..9d90799d 100644 --- a/FrontEnd/src/components/menu/PopupMenu.tsx +++ b/FrontEnd/src/components/menu/PopupMenu.tsx @@ -3,11 +3,9 @@ import classNames from "classnames"; import { createPortal } from "react-dom"; import { usePopper } from "react-popper"; -import { useClickOutside } from "~src/utilities/hooks"; - -import Menu, { MenuItems } from "./Menu"; - import { ThemeColor } from "../common"; +import { useClickOutside } from "../hooks"; +import Menu, { MenuItems } from "./Menu"; import "./PopupMenu.css"; diff --git a/FrontEnd/src/pages/login/index.tsx b/FrontEnd/src/pages/login/index.tsx index 582ebd0f..39ea3831 100644 --- a/FrontEnd/src/pages/login/index.tsx +++ b/FrontEnd/src/pages/login/index.tsx @@ -6,11 +6,7 @@ import { useUser, userService } from "~src/services/user"; import { useC } from "~src/components/common"; import LoadingButton from "~src/components/button/LoadingButton"; -import { - InputErrorDict, - InputGroup, - useInputs, -} from "~src/components/input/InputGroup"; +import { InputGroup, useInputs } from "~src/components/input/InputGroup"; import Page from "~src/components/Page"; import "./index.css"; @@ -47,15 +43,13 @@ export default function LoginPage() { label: "user.rememberMe", }, ], - validator: ({ username, password }) => { - const result: InputErrorDict = {}; + validator: ({ username, password }, errors) => { if (username === "") { - result["username"] = "login.emptyUsername"; + errors["username"] = "login.emptyUsername"; } if (password === "") { - result["password"] = "login.emptyPassword"; + errors["password"] = "login.emptyPassword"; } - return result; }, }, dataInit: {}, diff --git a/FrontEnd/src/pages/register/index.tsx b/FrontEnd/src/pages/register/index.tsx index 9e478612..fa25c2c2 100644 --- a/FrontEnd/src/pages/register/index.tsx +++ b/FrontEnd/src/pages/register/index.tsx @@ -7,11 +7,7 @@ import { getHttpTokenClient } from "~src/http/token"; import { userService, useUser } from "~src/services/user"; import { LoadingButton } from "~src/components/button"; -import { - useInputs, - InputErrorDict, - InputGroup, -} from "~src/components/input/InputGroup"; +import { useInputs, InputGroup } from "~src/components/input/InputGroup"; import "./index.css"; @@ -51,26 +47,22 @@ export default function RegisterPage() { label: "register.registerCode", }, ], - validator: ({ - username, - password, - confirmPassword, - registerCode, - }) => { - const result: InputErrorDict = {}; + validator: ( + { username, password, confirmPassword, registerCode }, + errors, + ) => { if (username === "") { - result["username"] = "register.error.usernameEmpty"; + errors["username"] = "register.error.usernameEmpty"; } if (password === "") { - result["password"] = "register.error.passwordEmpty"; + errors["password"] = "register.error.passwordEmpty"; } if (confirmPassword !== password) { - result["confirmPassword"] = "register.error.confirmPasswordWrong"; + errors["confirmPassword"] = "register.error.confirmPasswordWrong"; } if (registerCode === "") { - result["registerCode"] = "register.error.registerCodeEmpty"; + errors["registerCode"] = "register.error.registerCodeEmpty"; } - return result; }, }, dataInit: {}, diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index c34bcf4f..011c5059 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -11,9 +11,8 @@ import ImageCropper, { applyClipToImage, } from "~src/components/ImageCropper"; import BlobImage from "~src/components/BlobImage"; -import ButtonRowV2 from "~src/components/button/ButtonRowV2"; -import Dialog from "~src/components/dialog/Dialog"; -import DialogContainer from "~src/components/dialog/DialogContainer"; +import { ButtonRowV2 } from "~src/components/button"; +import { Dialog, DialogContainer } from "~src/components/dialog"; import "./ChangeAvatarDialog.css"; diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index bfcea92d..946b9fbe 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -3,9 +3,7 @@ import { useNavigate } from "react-router-dom"; import { userService } from "~src/services/user"; -import OperationDialog, { - InputErrorDict, -} from "~src/components/dialog/OperationDialog"; +import { OperationDialog } from "~src/components/dialog"; interface ChangePasswordDialogProps { open: boolean; @@ -47,21 +45,22 @@ export function ChangePasswordDialog(props: ChangePasswordDialogProps) { password: true, }, ], - validator: ({ oldPassword, newPassword, retypedNewPassword }) => { - const result: InputErrorDict = {}; + validator: ( + { oldPassword, newPassword, retypedNewPassword }, + errors, + ) => { if (oldPassword === "") { - result["oldPassword"] = + errors["oldPassword"] = "settings.dialogChangePassword.errorEmptyOldPassword"; } if (newPassword === "") { - result["newPassword"] = + errors["newPassword"] = "settings.dialogChangePassword.errorEmptyNewPassword"; } if (retypedNewPassword !== newPassword) { - result["retypedNewPassword"] = + errors["retypedNewPassword"] = "settings.dialogChangePassword.errorRetypeNotMatch"; } - return result; }, }} onProcess={async ({ oldPassword, newPassword }) => { diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 67416a08..918a77b5 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -4,25 +4,26 @@ import { ReactNode, ComponentPropsWithoutRef, } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; // For change language. import { useNavigate } from "react-router-dom"; import classNames from "classnames"; -import { useC, Text } from "~src/common"; import { useUser, userService } from "~src/services/user"; import { getHttpUserClient } from "~src/http/user"; +import { pushAlert } from "~src/services/alert"; + +import { useC, Text } from "~src/common"; -import { useDialog } from "~src/components/dialog"; -import ConfirmDialog from "~src/components/dialog/ConfirmDialog"; +import { useDialog, ConfirmDialog } from "~src/components/dialog"; import Card from "~src/components/Card"; import Spinner from "~src/components/Spinner"; import Page from "~src/components/Page"; + import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; import "./index.css"; -import { pushAlert } from "~src/services/alert"; interface SettingSectionProps extends Omit, "title"> { diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx index f266ec9d..caf4f502 100644 --- a/FrontEnd/src/pages/timeline/Timeline.tsx +++ b/FrontEnd/src/pages/timeline/Timeline.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from "react"; import classnames from "classnames"; -import { useScrollToBottom } from "~src/utilities/hooks"; import { HubConnectionState } from "@microsoft/signalr"; import { @@ -16,6 +15,8 @@ import { import { getTimelinePostUpdate$ } from "~src/services/timeline"; +import { useScrollToBottom } from "~src/components/hooks"; + import TimelinePostList from "./TimelinePostList"; import TimelinePostEdit from "./TimelinePostCreateView"; import TimelineCard from "./TimelineCard"; diff --git a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx index 7b7b8e8c..a7209e75 100644 --- a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx @@ -39,9 +39,9 @@ const TimelineDeleteDialog: React.FC = (props) => { label: "", }, ], - validator: ({ name }) => { + validator: ({ name }, errors) => { if (name !== timeline.nameV2) { - return { name: "timeline.deleteDialog.notMatch" }; + errors.name = "timeline.deleteDialog.notMatch"; } }, }} diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx index 2a8c5947..6b87ef2a 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx @@ -1,11 +1,13 @@ import { useState } from "react"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline"; +import { + getHttpTimelineClient, + HttpTimelinePostInfo, +} from "~src/http/timeline"; import { pushAlert } from "~src/services/alert"; -import { useClickOutside } from "~src/utilities/hooks"; - +import { useClickOutside } from "~src/components/hooks"; import UserAvatar from "~src/components/user/UserAvatar"; import { useDialog } from "~src/components/dialog"; import FlatButton from "~src/components/button/FlatButton"; diff --git a/FrontEnd/src/utilities/hooks.ts b/FrontEnd/src/utilities/hooks.ts deleted file mode 100644 index a59f7167..00000000 --- a/FrontEnd/src/utilities/hooks.ts +++ /dev/null @@ -1,5 +0,0 @@ -import useClickOutside from "./hooks/useClickOutside"; -import useScrollToBottom from "./hooks/useScrollToBottom"; -import { useIsSmallScreen } from "./hooks/mediaQuery"; - -export { useClickOutside, useScrollToBottom, useIsSmallScreen }; diff --git a/FrontEnd/src/utilities/hooks/mediaQuery.ts b/FrontEnd/src/utilities/hooks/mediaQuery.ts deleted file mode 100644 index ad55c3c0..00000000 --- a/FrontEnd/src/utilities/hooks/mediaQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useMediaQuery } from "react-responsive"; - -export function useIsSmallScreen(): boolean { - return useMediaQuery({ maxWidth: 576 }); -} diff --git a/FrontEnd/src/utilities/hooks/useClickOutside.ts b/FrontEnd/src/utilities/hooks/useClickOutside.ts deleted file mode 100644 index 6dcbf7b3..00000000 --- a/FrontEnd/src/utilities/hooks/useClickOutside.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useRef, useEffect } from "react"; - -export default function useClickOutside( - element: HTMLElement | null | undefined, - onClickOutside: () => void, - nextTick?: boolean -): void { - const onClickOutsideRef = useRef<() => void>(onClickOutside); - - useEffect(() => { - onClickOutsideRef.current = onClickOutside; - }, [onClickOutside]); - - useEffect(() => { - if (element != null) { - const handler = (event: MouseEvent): void => { - let e: HTMLElement | null = event.target as HTMLElement; - while (e) { - if (e == element) { - return; - } - e = e.parentElement; - } - onClickOutsideRef.current(); - }; - if (nextTick) { - setTimeout(() => { - document.addEventListener("click", handler); - }); - } else { - document.addEventListener("click", handler); - } - return () => { - document.removeEventListener("click", handler); - }; - } - }, [element, nextTick]); -} diff --git a/FrontEnd/src/utilities/hooks/useScrollToBottom.ts b/FrontEnd/src/utilities/hooks/useScrollToBottom.ts deleted file mode 100644 index 216746f4..00000000 --- a/FrontEnd/src/utilities/hooks/useScrollToBottom.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useRef, useEffect } from "react"; -import { fromEvent } from "rxjs"; -import { filter, throttleTime } from "rxjs/operators"; - -function useScrollToBottom( - handler: () => void, - enable = true, - option = { - maxOffset: 5, - throttle: 1000, - } -): void { - const handlerRef = useRef<(() => void) | null>(null); - - useEffect(() => { - handlerRef.current = handler; - - return () => { - handlerRef.current = null; - }; - }, [handler]); - - useEffect(() => { - const subscription = fromEvent(window, "scroll") - .pipe( - filter( - () => - window.scrollY >= - document.body.scrollHeight - window.innerHeight - option.maxOffset - ), - throttleTime(option.throttle) - ) - .subscribe(() => { - if (enable) { - handlerRef.current?.(); - } - }); - - return () => { - subscription.unsubscribe(); - }; - }, [enable, option.maxOffset, option.throttle]); -} - -export default useScrollToBottom; -- cgit v1.2.3 From b66a57071316434356e77e294ec22181e4db54d5 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 28 Aug 2023 01:41:41 +0800 Subject: ... --- FrontEnd/src/components/BlobImage.tsx | 15 +- FrontEnd/src/components/ImageCropper.css | 15 +- FrontEnd/src/components/ImageCropper.tsx | 564 +++++++++++++--------- FrontEnd/src/components/LoadFailReload.tsx | 37 ++ FrontEnd/src/pages/setting/ChangeAvatarDialog.css | 1 + FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 34 +- FrontEnd/src/pages/timeline/TimelineCard.tsx | 2 +- 7 files changed, 394 insertions(+), 274 deletions(-) create mode 100644 FrontEnd/src/components/LoadFailReload.tsx (limited to 'FrontEnd/src/components/LoadFailReload.tsx') 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, "src"> & { imgRef?: React.Ref; 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(undefined); @@ -22,5 +23,13 @@ export default function BlobImage(props: BlobImageProps) { } }, [src]); - return ; + const key = useMemo(() => { + if (keyBySrc) { + return url == null ? undefined : btoa(url); + } else { + return undefined; + } + }, [url, keyBySrc]); + + return ; } 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 { + 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( - null, - ); - const [imageInfo, setImageInfo] = React.useState(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(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) => { - 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; + onImageLoad: (event: SyntheticEvent) => 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; + imageCropperProps: ImageCropperProps; +} { + const targetRatio = options?.constraint?.ratio; + + const imageElementRef = useRef(null); + const [image, setImage] = useState(null); + const [clip, setClip] = useState(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(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 (
- -
+ +
); -}; - -export default ImageCropper; - -export function applyClipToImage( - image: HTMLImageElement, - clip: Clip, - mimeType: string, -): Promise { - 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 = ({ + onReload, + className, + style, +}) => { + return ( + + 0 + { + onReload(); + e.preventDefault(); + }} + > + 1 + + 2 + + ); +}; + +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("select"); const [file, setFile] = useState(null); - const [clip, setClip] = useState(null); - const [cropImgElement, setCropImgElement] = useState( - null, - ); + + const { canCrop, crop, imageCropperProps } = useImageCrop(file, { + constraint: { + ratio: 1, + }, + }); + const [resultBlob, setResultBlob] = useState(null); const [message, setMessage] = useState( "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 (
{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"; -- cgit v1.2.3