From c7934c59cb17a4266ea882cdb40be79f22043d10 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 24 Jul 2023 00:31:02 +0800 Subject: ... --- FrontEnd/src/utilities/base64.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'FrontEnd/src/utilities') diff --git a/FrontEnd/src/utilities/base64.ts b/FrontEnd/src/utilities/base64.ts index 59de7512..6eece979 100644 --- a/FrontEnd/src/utilities/base64.ts +++ b/FrontEnd/src/utilities/base64.ts @@ -1,8 +1,19 @@ -import { Base64 } from "js-base64"; +function bytesToBase64(bytes: Uint8Array): string { + const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join(""); + return btoa(binString); +} + +export default function base64( + data: Blob | Uint8Array | string, +): Promise { + if (typeof data === "string") { + // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem + const binString = new TextEncoder().encode(data); + return Promise.resolve(bytesToBase64(binString)); + } -export default function base64(blob: Blob | string): Promise { - if (typeof blob === "string") { - return Promise.resolve(Base64.encode(blob)); + if (data instanceof Uint8Array) { + return Promise.resolve(bytesToBase64(data)); } return new Promise((resolve) => { @@ -10,6 +21,6 @@ export default function base64(blob: Blob | string): Promise { reader.onload = function () { resolve((reader.result as string).replace(/^data:.*;base64,/, "")); }; - reader.readAsDataURL(blob); + reader.readAsDataURL(data); }); } -- 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/utilities') 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 877f4ff87c39e3484ae2e7e6c920fc7fb8c04c23 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 28 Aug 2023 21:14:20 +0800 Subject: ... --- FrontEnd/src/components/ImageCropper.tsx | 262 ++++++++++------------------ FrontEnd/src/components/common.ts | 2 + FrontEnd/src/utilities/geometry.ts | 291 +++++++++++++++++++++++++++++++ 3 files changed, 388 insertions(+), 167 deletions(-) create mode 100644 FrontEnd/src/utilities/geometry.ts (limited to 'FrontEnd/src/utilities') diff --git a/FrontEnd/src/components/ImageCropper.tsx b/FrontEnd/src/components/ImageCropper.tsx index 1f530004..acbbbe0c 100644 --- a/FrontEnd/src/components/ImageCropper.tsx +++ b/FrontEnd/src/components/ImageCropper.tsx @@ -1,29 +1,20 @@ -import { - useState, - useRef, - SyntheticEvent, - PointerEvent, - useMemo, - MutableRefObject, -} from "react"; +import { useState, useRef, PointerEvent } from "react"; import classnames from "classnames"; -import { UiLogicError } from "./common"; +import { UiLogicError, geometry } from "./common"; + import BlobImage from "./BlobImage"; import "./ImageCropper.css"; -// All in natural size of image. -export interface Clip { - left: number; - top: number; - width: number; - height: number; -} +const { Rect } = geometry; + +type Rect = geometry.Rect; +type Movement = geometry.Movement; -export function applyClipToImage( +export function crop( image: HTMLImageElement, - clip: Clip, + clip: Rect, mimeType: string, ): Promise { return new Promise((resolve, reject) => { @@ -56,17 +47,13 @@ export function applyClipToImage( }); } -interface Movement { - x: number; - y: number; -} - interface ImageInfo { element: HTMLImageElement; width: number; height: number; ratio: number; landscape: boolean; + rect: Rect; } export interface CropConstraint { @@ -77,11 +64,7 @@ export interface CropConstraint { // maxClipHeight?: number; } -function generateImageInfo( - imageElement: HTMLImageElement | null, -): ImageInfo | null { - if (imageElement == null) return null; - +function generateImageInfo(imageElement: HTMLImageElement): ImageInfo { const { naturalWidth, naturalHeight } = imageElement; const imageRatio = naturalHeight / naturalWidth; @@ -91,108 +74,15 @@ function generateImageInfo( height: naturalHeight, ratio: imageRatio, landscape: imageRatio < 1, + rect: new Rect(0, 0, naturalWidth, naturalHeight), }; } -const emptyClip: Clip = { - left: 0, - top: 0, - width: 0, - height: 0, -}; - -const allClip : Clip = { - left: 0, - top: 0, - width: Number.MAX_VALUE, - height: Number.MAX_VALUE, -} - -// 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; - } - - set right(value: number) { - this.width = this.left + value; - } - - get bottom(): number { - return this.top + this.height; - } - - set bottom(value: number) { - this.height = this.top + value; - } - - get ratio(): number { - return this.height / this.width; - } - - toClip(): Clip { - return { - left: this.left, - top: this.top, - width: this.width, - height: this.height, - }; - } - } - - const clipGeometry = new ClipGeometry( - clip.left, - clip.top, - clip.width, - clip.height, - ); - - // 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; - } - } - - return clipGeometry.toClip(); -} - interface ImageCropperProps { - clip: Clip; + clip: Rect; image: Blob | string | null; - imageElementRef: MutableRefObject; - onImageLoad: (event: SyntheticEvent) => void; + imageElementCallback: (element: HTMLImageElement | null) => void; + onImageLoad: () => void; onMove: (movement: Movement) => void; onResize: (movement: Movement) => void; containerClassName?: string; @@ -204,28 +94,26 @@ export function useImageCrop( constraint?: CropConstraint; }, ): { - clip: Clip; - setClip: (clip: Clip) => void; + clip: Rect; + setClip: (clip: Rect) => 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 ); + const [imageElement, setImageElement] = useState( + null, + ); + const [imageInfo, setImageInfo] = useState(null); + const [clip, setClip] = useState(Rect.empty); - if (imageElementRef.current == null && image != null) { - setImage(null); - setClip(emptyClip); + if (imageElement == null && imageInfo != null) { + setImageInfo(null); + setClip(Rect.empty); } - const canCrop = file != null && image != null; - - const adjustedClip = useMemo(() => { - return image == null ? emptyClip : adjustClip(clip, image, targetRatio); - }, [clip, image, targetRatio]); + const canCrop = file != null && imageElement != null && imageInfo != null; return { clip, @@ -233,19 +121,43 @@ export function useImageCrop( canCrop, crop() { if (!canCrop) throw new UiLogicError(); - return applyClipToImage(image.element, adjustedClip, file.type); + return crop(imageElement, clip, file.type); }, imageCropperProps: { - clip: adjustedClip, + clip, image: file, - imageElementRef: imageElementRef, - // TODO: Continue here... - onMove: , - onResize: , + imageElementCallback: setImageElement, + onMove: (movement) => { + if (imageInfo == null) return; + const newClip = geometry.adjustRectToContainer( + clip.copy().move(movement), + imageInfo.rect, + "move", + { + targetRatio, + }, + ); + setClip(newClip); + }, + onResize: (movement) => { + if (imageInfo == null) return; + const newClip = geometry.adjustRectToContainer( + clip.copy().expand(movement), + imageInfo.rect, + "resize", + { targetRatio, resizeNoFlip: true, ratioCorrectBasedOn: "width" }, + ); + setClip(newClip); + }, onImageLoad: () => { - const image = generateImageInfo(imageElementRef.current); - setImage(image); - setClip(adjustClip(allClip, "both", image, targetRatio)); + if (imageElement == null) throw new UiLogicError(); + const image = generateImageInfo(imageElement); + setImageInfo(image); + setClip( + geometry.adjustRectToContainer(Rect.max, image.rect, "both", { + targetRatio, + }), + ); }, }, }; @@ -261,17 +173,17 @@ const imageCropperHandlerSize = 15; export function ImageCropper(props: ImageCropperProps) { function convertClipToElement( - clip: Clip, + clip: Rect, imageElement: HTMLImageElement, - ): Clip { + ): Rect { const xRatio = imageElement.clientWidth / imageElement.naturalWidth; const yRatio = imageElement.clientHeight / imageElement.naturalHeight; - return { + return Rect.from({ left: xRatio * clip.left, top: yRatio * clip.top, width: xRatio * clip.width, height: yRatio * clip.height, - }; + }); } function convertMovementFromElement( @@ -289,7 +201,7 @@ export function ImageCropper(props: ImageCropperProps) { const { clip, image, - imageElementRef, + imageElementCallback, onImageLoad, onMove, onResize, @@ -297,18 +209,21 @@ export function ImageCropper(props: ImageCropperProps) { } = props; const pointerStateRef = useRef(null); + const [imageElement, setImageElement] = useState( + null, + ); - const clipInElement = - imageElementRef.current != null - ? convertClipToElement(clip, imageElementRef.current) - : emptyClip; + const clipInElement: Rect = + imageElement != null + ? convertClipToElement(clip, imageElement) + : Rect.empty; const actOnMovement = ( e: PointerEvent, change: (movement: Movement) => void, ) => { if ( - imageElementRef.current == null || + imageElement == null || pointerStateRef.current == null || pointerStateRef.current.pointerId != e.pointerId ) { @@ -322,12 +237,14 @@ export function ImageCropper(props: ImageCropperProps) { y: e.clientY - y, }; - change(movement); + pointerStateRef.current.x = e.clientX; + pointerStateRef.current.y = e.clientY; + + change(convertMovementFromElement(movement, imageElement)); }; const onPointerDown = (e: PointerEvent) => { - if (imageElementRef.current == null || pointerStateRef.current != null) - return; + if (imageElement == null || pointerStateRef.current != null) return; e.currentTarget.setPointerCapture(e.pointerId); @@ -362,16 +279,27 @@ export function ImageCropper(props: ImageCropperProps) {
- + { + setImageElement(element); + imageElementCallback(element); + }} + src={image} + onLoad={onImageLoad} + />
= 0 ? this.left : this.right; + } + + get normalizedTop(): number { + return this.height >= 0 ? this.top : this.bottom; + } + + get normalizedRight(): number { + return this.width >= 0 ? this.right : this.left; + } + + get normalizedBottom(): number { + return this.height >= 0 ? this.bottom : this.top; + } + + get normalizedWidth(): number { + return Math.abs(this.width); + } + + get normalizedHeight(): number { + return Math.abs(this.height); + } + + get normalizedSize(): Size { + return { + width: this.normalizedWidth, + height: this.normalizedHeight, + }; + } + + get normalizedRatio(): number { + return Math.abs(this.ratio); + } + + normalize(): Rect { + if (this.width < 0) { + this.width = -this.width; + this.left -= this.width; + } + if (this.height < 0) { + this.height = -this.height; + this.top -= this.height; + } + return this; + } + + move(movement: Movement): Rect { + this.left += movement.x; + this.top += movement.y; + return this; + } + + expand(size: Size | Point): Rect { + if ("x" in size) { + this.width += size.x; + this.height += size.y; + } else { + this.width += size.width; + this.height += size.height; + } + return this; + } + + copy(): Rect { + return new Rect(this.left, this.top, this.width, this.height); + } +} + +export function adjustRectToContainer( + rect: Rect, + container: Rect, + mode: "move" | "resize" | "both", + options?: { + targetRatio?: number; + resizeNoFlip?: boolean; + ratioCorrectBasedOn?: "bigger" | "smaller" | "width" | "height"; + }, +): Rect { + rect = rect.copy(); + container = container.copy().normalize(); + + if (process.env.NODE_ENV === "development") { + if (mode === "move") { + if (rect.normalizedWidth > container.width) { + console.warn( + "adjust rect (move): rect.normalizedWidth > container.normalizedWidth", + ); + } + if (rect.normalizedHeight > container.height) { + console.warn( + "adjust rect (move): rect.normalizedHeight > container.normalizedHeight", + ); + } + } + if (mode === "resize") { + if (rect.left < container.left) { + console.warn( + "adjust rect (resize): rect.left < container.normalizedLeft", + ); + } + if (rect.left > container.right) { + console.warn( + "adjust rect (resize): rect.left > container.normalizedRight", + ); + } + if (rect.top < container.top) { + console.warn( + "adjust rect (resize): rect.top < container.normalizedTop", + ); + } + if (rect.top > container.bottom) { + console.warn( + "adjust rect (resize): rect.top > container.normalizedBottom", + ); + } + } + } + + if (mode === "move") { + rect.left = + rect.width >= 0 + ? clamp(rect.left, container.left, container.right - rect.width) + : clamp(rect.left, container.left - rect.width, container.right); + rect.top = + rect.height >= 0 + ? clamp(rect.top, container.top, container.bottom - rect.height) + : clamp(rect.top, container.top - rect.height, container.bottom); + } else if (mode === "resize") { + const noFlip = options?.resizeNoFlip; + rect.right = clamp( + rect.right, + noFlip ? 0 : container.left, + container.right, + ); + rect.bottom = clamp( + rect.bottom, + noFlip ? 0 : container.top, + container.bottom, + ); + } else { + rect.left = clamp(rect.left, container.left, container.right); + rect.top = clamp(rect.top, container.top, container.bottom); + rect.right = clamp(rect.right, container.left, container.right); + rect.bottom = clamp(rect.bottom, container.top, container.bottom); + } + + // Now correct ratio + const currentRatio = rect.normalizedRatio; + let targetRatio = options?.targetRatio; + if (targetRatio != null) targetRatio = Math.abs(targetRatio); + if (targetRatio != null && currentRatio !== targetRatio) { + const { ratioCorrectBasedOn } = options ?? {}; + + const newWidth = + (Math.sign(rect.width) * rect.normalizedHeight) / targetRatio; + const newHeight = + Math.sign(rect.height) * rect.normalizedWidth * targetRatio; + + const newBottom = rect.top + newHeight; + const newRight = rect.left + newWidth; + + if (ratioCorrectBasedOn === "width") { + if (newBottom >= container.top && newBottom <= container.bottom) { + rect.height = newHeight; + } else { + rect.bottom = clamp(newBottom, container.top, container.bottom); + rect.width = + (Math.sign(rect.width) * rect.normalizedHeight) / targetRatio; + } + } else if (ratioCorrectBasedOn === "height") { + if (newRight >= container.left && newRight <= container.right) { + rect.width = newWidth; + } else { + rect.right = clamp(newRight, container.left, container.right); + rect.height = + Math.sign(rect.height) * rect.normalizedWidth * targetRatio; + } + } else if (ratioCorrectBasedOn === "smaller") { + if (currentRatio > targetRatio) { + // too tall + rect.width = + (Math.sign(rect.width) * rect.normalizedHeight) / targetRatio; + } else { + rect.height = + Math.sign(rect.height) * rect.normalizedWidth * targetRatio; + } + } else { + if (currentRatio < targetRatio) { + // too wide + rect.width = + (Math.sign(rect.width) * rect.normalizedHeight) / targetRatio; + } else { + rect.height = + Math.sign(rect.height) * rect.normalizedWidth * targetRatio; + } + } + } + + return rect; +} -- cgit v1.2.3 From d65dcebc3ed64c96c70f0ee7f228b4dfe79b28a1 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 29 Aug 2023 20:41:34 +0800 Subject: ... --- FrontEnd/src/components/common.ts | 2 + FrontEnd/src/components/dialog/Dialog.css | 4 +- FrontEnd/src/components/dialog/Dialog.tsx | 39 ++--- FrontEnd/src/components/theme-color.css | 173 --------------------- FrontEnd/src/components/theme.css | 100 ++++++------ FrontEnd/src/index.css | 3 - .../src/pages/setting/ChangeNicknameDialog.tsx | 2 +- FrontEnd/src/pages/setting/index.tsx | 4 +- FrontEnd/src/utilities/index.ts | 10 ++ 9 files changed, 88 insertions(+), 249 deletions(-) delete mode 100644 FrontEnd/src/components/theme-color.css create mode 100644 FrontEnd/src/utilities/index.ts (limited to 'FrontEnd/src/utilities') diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts index 171ebc48..835a8b4a 100644 --- a/FrontEnd/src/components/common.ts +++ b/FrontEnd/src/components/common.ts @@ -1,3 +1,5 @@ +import "./index.css"; + export type { Text, I18nText } from "~src/common"; export { UiLogicError, c, convertI18nText, useC } from "~src/common"; diff --git a/FrontEnd/src/components/dialog/Dialog.css b/FrontEnd/src/components/dialog/Dialog.css index e4c61440..f25309ae 100644 --- a/FrontEnd/src/components/dialog/Dialog.css +++ b/FrontEnd/src/components/dialog/Dialog.css @@ -17,7 +17,7 @@ right: 0; top: 0; bottom: 0; - background-color: var(--cru-surface-dim-color); + background-color: var(--cru-dialog-overlay-color); opacity: 0.8; } @@ -27,7 +27,7 @@ margin: 2em auto; - border: var(--cru-key-container-color) 1px solid; + border: var(--cru-dialog-container-background-color) 1px solid; border-radius: 5px; padding: 1.5em; background-color: var(--cru-surface-color); diff --git a/FrontEnd/src/components/dialog/Dialog.tsx b/FrontEnd/src/components/dialog/Dialog.tsx index b1d66704..bdba9198 100644 --- a/FrontEnd/src/components/dialog/Dialog.tsx +++ b/FrontEnd/src/components/dialog/Dialog.tsx @@ -1,6 +1,5 @@ import { ReactNode, useRef } from "react"; import ReactDOM from "react-dom"; -import { CSSTransition } from "react-transition-group"; import classNames from "classnames"; import { ThemeColor } from "../common"; @@ -16,42 +15,34 @@ if (optionalPortalElement == null) { const portalElement = optionalPortalElement; interface DialogProps { - color?: ThemeColor; children?: ReactNode; disableCloseOnClickOnOverlay?: boolean; } export default function Dialog({ - color, children, disableCloseOnClickOnOverlay, }: DialogProps) { - color = color ?? "primary"; - const closeDialog = useCloseDialog(); - const nodeRef = useRef(null); + const lastPointerDownIdRef = useRef(null); return ReactDOM.createPortal( - +
-
-
{children}
-
- , + className="cru-dialog-background" + onPointerDown={(e) => { + lastPointerDownIdRef.current = e.pointerId; + }} + onPointerUp={(e) => { + if (lastPointerDownIdRef.current === e.pointerId) { + if (!disableCloseOnClickOnOverlay) closeDialog(); + } + lastPointerDownIdRef.current = null; + }} + /> +
{children}
+
, portalElement, ); } diff --git a/FrontEnd/src/components/theme-color.css b/FrontEnd/src/components/theme-color.css deleted file mode 100644 index 24a7e267..00000000 --- a/FrontEnd/src/components/theme-color.css +++ /dev/null @@ -1,173 +0,0 @@ -/* Generated by theme-generator.ts */ - -:root { - --cru-primary-color: hsl(210 100% 40%); - --cru-primary-1-color: hsl(210 100% 37%); - --cru-primary-2-color: hsl(210 100% 34%); - --cru-primary-on-color: hsl(210 100% 100%); - --cru-primary-container-color: hsl(210 100% 90%); - --cru-primary-container-1-color: hsl(210 100% 80%); - --cru-primary-container-2-color: hsl(210 100% 70%); - --cru-primary-on-container-color: hsl(210 100% 10%); - --cru-secondary-color: hsl(40 100% 40%); - --cru-secondary-1-color: hsl(40 100% 37%); - --cru-secondary-2-color: hsl(40 100% 34%); - --cru-secondary-on-color: hsl(40 100% 100%); - --cru-secondary-container-color: hsl(40 100% 90%); - --cru-secondary-container-1-color: hsl(40 100% 80%); - --cru-secondary-container-2-color: hsl(40 100% 70%); - --cru-secondary-on-container-color: hsl(40 100% 10%); - --cru-tertiary-color: hsl(160 100% 40%); - --cru-tertiary-1-color: hsl(160 100% 37%); - --cru-tertiary-2-color: hsl(160 100% 34%); - --cru-tertiary-on-color: hsl(160 100% 100%); - --cru-tertiary-container-color: hsl(160 100% 90%); - --cru-tertiary-container-1-color: hsl(160 100% 80%); - --cru-tertiary-container-2-color: hsl(160 100% 70%); - --cru-tertiary-on-container-color: hsl(160 100% 10%); - --cru-danger-color: hsl(0 100% 40%); - --cru-danger-1-color: hsl(0 100% 37%); - --cru-danger-2-color: hsl(0 100% 34%); - --cru-danger-on-color: hsl(0 100% 100%); - --cru-danger-container-color: hsl(0 100% 90%); - --cru-danger-container-1-color: hsl(0 100% 80%); - --cru-danger-container-2-color: hsl(0 100% 70%); - --cru-danger-on-container-color: hsl(0 100% 10%); - --cru-success-color: hsl(120 60% 40%); - --cru-success-1-color: hsl(120 60% 37%); - --cru-success-2-color: hsl(120 60% 34%); - --cru-success-on-color: hsl(120 60% 100%); - --cru-success-container-color: hsl(120 60% 90%); - --cru-success-container-1-color: hsl(120 60% 80%); - --cru-success-container-2-color: hsl(120 60% 70%); - --cru-success-on-container-color: hsl(120 60% 10%); - --cru-surface-dim-color: hsl(0 0% 87%); - --cru-surface-color: hsl(0 0% 98%); - --cru-surface-1-color: hsl(0 0% 90%); - --cru-surface-2-color: hsl(0 0% 82%); - --cru-surface-bright-color: hsl(0 0% 98%); - --cru-surface-container-lowest-color: hsl(0 0% 100%); - --cru-surface-container-low-color: hsl(0 0% 96%); - --cru-surface-container-color: hsl(0 0% 94%); - --cru-surface-container-high-color: hsl(0 0% 92%); - --cru-surface-container-highest-color: hsl(0 0% 90%); - --cru-surface-on-color: hsl(0 0% 10%); - --cru-surface-on-variant-color: hsl(0 0% 30%); - --cru-surface-outline-color: hsl(0 0% 50%); - --cru-surface-outline-variant-color: hsl(0 0% 80%); -} - -@media (prefers-color-scheme: dark) { - :root { - --cru-primary-color: hsl(210 100% 80%); - --cru-primary-1-color: hsl(210 100% 75%); - --cru-primary-2-color: hsl(210 100% 68%); - --cru-primary-on-color: hsl(210 100% 20%); - --cru-primary-container-color: hsl(210 100% 30%); - --cru-primary-container-1-color: hsl(210 100% 25%); - --cru-primary-container-2-color: hsl(210 100% 20%); - --cru-primary-on-container-color: hsl(210 100% 90%); - --cru-secondary-color: hsl(40 100% 80%); - --cru-secondary-1-color: hsl(40 100% 75%); - --cru-secondary-2-color: hsl(40 100% 68%); - --cru-secondary-on-color: hsl(40 100% 20%); - --cru-secondary-container-color: hsl(40 100% 30%); - --cru-secondary-container-1-color: hsl(40 100% 25%); - --cru-secondary-container-2-color: hsl(40 100% 20%); - --cru-secondary-on-container-color: hsl(40 100% 90%); - --cru-tertiary-color: hsl(160 100% 80%); - --cru-tertiary-1-color: hsl(160 100% 75%); - --cru-tertiary-2-color: hsl(160 100% 68%); - --cru-tertiary-on-color: hsl(160 100% 20%); - --cru-tertiary-container-color: hsl(160 100% 30%); - --cru-tertiary-container-1-color: hsl(160 100% 25%); - --cru-tertiary-container-2-color: hsl(160 100% 20%); - --cru-tertiary-on-container-color: hsl(160 100% 90%); - --cru-danger-color: hsl(0 100% 80%); - --cru-danger-1-color: hsl(0 100% 75%); - --cru-danger-2-color: hsl(0 100% 68%); - --cru-danger-on-color: hsl(0 100% 20%); - --cru-danger-container-color: hsl(0 100% 30%); - --cru-danger-container-1-color: hsl(0 100% 25%); - --cru-danger-container-2-color: hsl(0 100% 20%); - --cru-danger-on-container-color: hsl(0 100% 90%); - --cru-success-color: hsl(120 60% 80%); - --cru-success-1-color: hsl(120 60% 75%); - --cru-success-2-color: hsl(120 60% 68%); - --cru-success-on-color: hsl(120 60% 20%); - --cru-success-container-color: hsl(120 60% 30%); - --cru-success-container-1-color: hsl(120 60% 25%); - --cru-success-container-2-color: hsl(120 60% 20%); - --cru-success-on-container-color: hsl(120 60% 90%); - --cru-surface-dim-color: hsl(0 0% 6%); - --cru-surface-color: hsl(0 0% 6%); - --cru-surface-1-color: hsl(0 0% 25%); - --cru-surface-2-color: hsl(0 0% 40%); - --cru-surface-bright-color: hsl(0 0% 24%); - --cru-surface-container-lowest-color: hsl(0 0% 4%); - --cru-surface-container-low-color: hsl(0 0% 10%); - --cru-surface-container-color: hsl(0 0% 12%); - --cru-surface-container-high-color: hsl(0 0% 17%); - --cru-surface-container-highest-color: hsl(0 0% 22%); - --cru-surface-on-color: hsl(0 0% 90%); - --cru-surface-on-variant-color: hsl(0 0% 80%); - --cru-surface-outline-color: hsl(0 0% 60%); - --cru-surface-outline-variant-color: hsl(0 0% 30%); - } -} - -.cru-primary { - --cru-key-color: var(--cru-primary-color); - --cru-key-1-color: var(--cru-primary-1-color); - --cru-key-2-color: var(--cru-primary-2-color); - --cru-key-on-color: var(--cru-primary-on-color); - --cru-key-container-color: var(--cru-primary-container-color); - --cru-key-container-1-color: var(--cru-primary-container-1-color); - --cru-key-container-2-color: var(--cru-primary-container-2-color); - --cru-key-on-container-color: var(--cru-primary-on-container-color); -} - -.cru-secondary { - --cru-key-color: var(--cru-secondary-color); - --cru-key-1-color: var(--cru-secondary-1-color); - --cru-key-2-color: var(--cru-secondary-2-color); - --cru-key-on-color: var(--cru-secondary-on-color); - --cru-key-container-color: var(--cru-secondary-container-color); - --cru-key-container-1-color: var(--cru-secondary-container-1-color); - --cru-key-container-2-color: var(--cru-secondary-container-2-color); - --cru-key-on-container-color: var(--cru-secondary-on-container-color); -} - -.cru-tertiary { - --cru-key-color: var(--cru-tertiary-color); - --cru-key-1-color: var(--cru-tertiary-1-color); - --cru-key-2-color: var(--cru-tertiary-2-color); - --cru-key-on-color: var(--cru-tertiary-on-color); - --cru-key-container-color: var(--cru-tertiary-container-color); - --cru-key-container-1-color: var(--cru-tertiary-container-1-color); - --cru-key-container-2-color: var(--cru-tertiary-container-2-color); - --cru-key-on-container-color: var(--cru-tertiary-on-container-color); -} - -.cru-danger { - --cru-key-color: var(--cru-danger-color); - --cru-key-1-color: var(--cru-danger-1-color); - --cru-key-2-color: var(--cru-danger-2-color); - --cru-key-on-color: var(--cru-danger-on-color); - --cru-key-container-color: var(--cru-danger-container-color); - --cru-key-container-1-color: var(--cru-danger-container-1-color); - --cru-key-container-2-color: var(--cru-danger-container-2-color); - --cru-key-on-container-color: var(--cru-danger-on-container-color); -} - -.cru-success { - --cru-key-color: var(--cru-success-color); - --cru-key-1-color: var(--cru-success-1-color); - --cru-key-2-color: var(--cru-success-2-color); - --cru-key-on-color: var(--cru-success-on-color); - --cru-key-container-color: var(--cru-success-container-color); - --cru-key-container-1-color: var(--cru-success-container-1-color); - --cru-key-container-2-color: var(--cru-success-container-2-color); - --cru-key-on-container-color: var(--cru-success-on-container-color); -} - diff --git a/FrontEnd/src/components/theme.css b/FrontEnd/src/components/theme.css index 6ceb369f..d7e30d1a 100644 --- a/FrontEnd/src/components/theme.css +++ b/FrontEnd/src/components/theme.css @@ -1,5 +1,3 @@ -@import "./theme-color.css"; - :root { --cru-default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; --cru-page-padding: 1em 2em; @@ -10,26 +8,26 @@ /* theme colors */ :root { - --cru-primary-color: hsl(210, 100%, 50%); - --cru-secondary-color: hsl(30, 100%, 50%); - --cru-create-color: hsl(120, 100%, 25%); - --cru-danger-color: hsl(0, 100%, 50%); + --cru-primary-color: hsl(210 100% 50%); + --cru-secondary-color: hsl(30 100% 50%); + --cru-create-color: hsl(120 100% 25%); + --cru-danger-color: hsl(0 100% 50%); } /* common colors */ :root { - --cru-background-color: hsl(0, 0%, 100%); - --cru-container-background-color: hsl(0, 0%, 97%); - --cru-text-primary-color: hsl(0, 0%, 0%); - --cru-text-secondary-color: hsl(0, 0%, 38%); + --cru-background-color: hsl(0 0% 100%); + --cru-container-background-color: hsl(0 0% 97%); + --cru-text-primary-color: hsl(0 0% 0%); + --cru-text-secondary-color: hsl(0 0% 38%); } @media (prefers-color-scheme: dark) { :root { - --cru-background-color: hsl(0, 0%, 0%); - --cru-container-background-color: hsl(0, 0%, 2%); - --cru-text-primary-color: hsl(0, 0%, 100%); - --cru-text-secondary-color: hsl(0, 0%, 85%); + --cru-background-color: hsl(0 0% 0%); + --cru-container-background-color: hsl(0 0% 2%); + --cru-text-primary-color: hsl(0 0% 100%); + --cru-text-secondary-color: hsl(0 0% 85%); } } @@ -37,37 +35,51 @@ --cru-body-background-color: var(--cru-background-color); } +/* dialog color */ + +:root { + --cru-dialog-overlay-color: hsl(0 0% 100%); + --cru-dialog-container-background-color: hsl(0 0% 100%); +} + +@media (prefers-color-scheme: dark) { + :root { + --cru-dialog-overlay-color: hsl(0 0% 0%); + --cru-dialog-container-background-color: hsl(0 0% 0%); + } +} + /* clickable color */ :root { --cru-clickable-primary-normal-color: var(--cru-primary-color); - --cru-clickable-primary-hover-color: hsl(210, 100%, 60%); - --cru-clickable-primary-focus-color: hsl(210, 100%, 60%); - --cru-clickable-primary-active-color: hsl(210, 100%, 70%); + --cru-clickable-primary-hover-color: hsl(210 100% 60%); + --cru-clickable-primary-focus-color: hsl(210 100% 60%); + --cru-clickable-primary-active-color: hsl(210 100% 70%); --cru-clickable-secondary-normal-color: var(--cru-secondary-color); - --cru-clickable-secondary-hover-color: hsl(30, 100%, 60%); - --cru-clickable-secondary-focus-color: hsl(30, 100%, 60%); - --cru-clickable-secondary-active-color: hsl(30, 100%, 70%); + --cru-clickable-secondary-hover-color: hsl(30 100% 60%); + --cru-clickable-secondary-focus-color: hsl(30 100% 60%); + --cru-clickable-secondary-active-color: hsl(30 100% 70%); --cru-clickable-create-normal-color: var(--cru-create-color); - --cru-clickable-create-hover-color: hsl(120, 100%, 35%); - --cru-clickable-create-focus-color: hsl(120, 100%, 35%); - --cru-clickable-create-active-color: hsl(120, 100%, 35%); + --cru-clickable-create-hover-color: hsl(120 100% 35%); + --cru-clickable-create-focus-color: hsl(120 100% 35%); + --cru-clickable-create-active-color: hsl(120 100% 35%); --cru-clickable-danger-normal-color: var(--cru-danger-color); - --cru-clickable-danger-hover-color: hsl(0, 100%, 60%); - --cru-clickable-danger-focus-color: hsl(0, 100%, 60%); - --cru-clickable-danger-active-color: hsl(0, 100%, 70%); - --cru-clickable-grayscale-normal-color: hsl(0, 0%, 100%); - --cru-clickable-grayscale-hover-color: hsl(0, 0%, 92%); - --cru-clickable-grayscale-focus-color: hsl(0, 0%, 92%); - --cru-clickable-grayscale-active-color: hsl(0, 0%, 88%); - --cru-clickable-disabled-color: hsl(0, 0%, 50%); + --cru-clickable-danger-hover-color: hsl(0 100% 60%); + --cru-clickable-danger-focus-color: hsl(0 100% 60%); + --cru-clickable-danger-active-color: hsl(0 100% 70%); + --cru-clickable-grayscale-normal-color: hsl(0 0% 100%); + --cru-clickable-grayscale-hover-color: hsl(0 0% 92%); + --cru-clickable-grayscale-focus-color: hsl(0 0% 92%); + --cru-clickable-grayscale-active-color: hsl(0 0% 88%); + --cru-clickable-disabled-color: hsl(0 0% 50%); } @media (prefers-color-scheme: dark) { :root { - --cru-clickable-grayscale-normal-color: hsl(0, 0%, 0%); - --cru-clickable-grayscale-hover-color: hsl(0, 0%, 10%); - --cru-clickable-grayscale-focus-color: hsl(0, 0%, 10%); - --cru-clickable-grayscale-active-color: hsl(0, 0%, 20%); + --cru-clickable-grayscale-normal-color: hsl(0 0% 0%); + --cru-clickable-grayscale-hover-color: hsl(0 0% 10%); + --cru-clickable-grayscale-focus-color: hsl(0 0% 10%); + --cru-clickable-grayscale-active-color: hsl(0 0% 20%); } } @@ -110,19 +122,19 @@ :root { /* push button colors */ --cru-push-button-text-color: #ffffff; - --cru-push-button-disabled-text-color: hsl(0, 0%, 80%); + --cru-push-button-disabled-text-color: hsl(0 0% 80%); } /* Card colors */ :root { - --cru-card-background-primary-color: hsl(210, 100%, 50%); - --cru-card-border-primary-color: hsl(210, 100%, 50%); - --cru-card-background-secondary-color: hsl(30, 100%, 50%); - --cru-card-border-secondary-color: hsl(30, 100%, 50%); - --cru-card-background-create-color: hsl(120, 100%, 25%); - --cru-card-border-create-color: hsl(120, 100%, 25%); - --cru-card-background-danger-color: hsl(0, 100%, 50%); - --cru-card-border-danger-color: hsl(0, 100%, 50%); + --cru-card-background-primary-color: hsl(210 100% 50%); + --cru-card-border-primary-color: hsl(210 100% 50%); + --cru-card-background-secondary-color: hsl(30 100% 50%); + --cru-card-border-secondary-color: hsl(30 100% 50%); + --cru-card-background-create-color: hsl(120 100% 25%); + --cru-card-border-create-color: hsl(120 100% 25%); + --cru-card-background-danger-color: hsl(0 100% 50%); + --cru-card-border-danger-color: hsl(0 100% 50%); } .cru-card-primary { diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css index ee92520b..f779297b 100644 --- a/FrontEnd/src/index.css +++ b/FrontEnd/src/index.css @@ -1,8 +1,5 @@ @import "npm:bootstrap-icons/font/bootstrap-icons.css"; -@import "./views/common/index.css"; - - small { line-height: 1.2; } diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx index bd1eaa51..912f554f 100644 --- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx @@ -1,7 +1,7 @@ import { getHttpUserClient } from "~src/http/user"; import { useUserLoggedIn } from "~src/services/user"; -import OperationDialog from "~src/components/dialog/OperationDialog"; +import { OperationDialog } from "~src/components/dialog"; export default function ChangeNicknameDialog() { const user = useUserLoggedIn(); diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 4d2c28c7..d2333134 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -245,9 +245,9 @@ export default function SettingPage() { const navigate = useNavigate(); const { controller, createDialogSwitch } = useDialog({ - "change-password": , + "change-nickname": , "change-avatar": , - "change-nickname": , + "change-password": , logout: ( { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, milliseconds); + }); +} -- cgit v1.2.3 From c7e781ffc4b347aa05ce0547a106a01e2fd792f3 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 30 Aug 2023 00:49:42 +0800 Subject: ... --- FrontEnd/src/components/Skeleton.css | 10 ++++++++-- FrontEnd/src/components/Skeleton.tsx | 32 +++++++++++--------------------- FrontEnd/src/utilities/index.ts | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 23 deletions(-) (limited to 'FrontEnd/src/utilities') diff --git a/FrontEnd/src/components/Skeleton.css b/FrontEnd/src/components/Skeleton.css index a571eead..0f78d3b5 100644 --- a/FrontEnd/src/components/Skeleton.css +++ b/FrontEnd/src/components/Skeleton.css @@ -4,11 +4,17 @@ .cru-skeleton-line { height: 1em; - background-color: hsl(0, 0%, 90%); + background-color: hsl(0 0% 90%); margin: 0.7em 0; border-radius: 0.2em; } -.cru-skeleton-line.last { +@media (prefers-color-scheme: dark) { + .cru-skeleton-line { + background-color: hsl(0 0% 20%); + } +} + +.cru-skeleton-line:last-child { width: 50%; } diff --git a/FrontEnd/src/components/Skeleton.tsx b/FrontEnd/src/components/Skeleton.tsx index 3b149db9..03f80df5 100644 --- a/FrontEnd/src/components/Skeleton.tsx +++ b/FrontEnd/src/components/Skeleton.tsx @@ -1,32 +1,22 @@ -import * as React from "react"; -import classnames from "classnames"; -import range from "lodash/range"; +import { ComponentPropsWithoutRef } from "react"; +import classNames from "classnames"; + +import { range } from "~src/utilities"; import "./Skeleton.css"; -export interface SkeletonProps { +interface SkeletonProps extends ComponentPropsWithoutRef<"div"> { lineNumber?: number; - className?: string; - style?: React.CSSProperties; } -const Skeleton: React.FC = (props) => { - const { lineNumber: lineNumberProps, className, style } = props; - const lineNumber = lineNumberProps ?? 3; +export default function Skeleton(props: SkeletonProps) { + const { lineNumber, className, ...otherProps } = props; return ( -
- {range(lineNumber).map((i) => ( -
+
+ {range(lineNumber ?? 3).map((i) => ( +
))}
); -}; - -export default Skeleton; +} diff --git a/FrontEnd/src/utilities/index.ts b/FrontEnd/src/utilities/index.ts index 085f8ae3..7659a8aa 100644 --- a/FrontEnd/src/utilities/index.ts +++ b/FrontEnd/src/utilities/index.ts @@ -8,3 +8,20 @@ export function delay(milliseconds: number): Promise { }, milliseconds); }); } + +export function range(stop: number): number[]; +export function range(start: number, stop: number, step?: number): number[]; +export function range(start: number, stop?: number, step?: number): number[] { + if (stop == undefined) { + stop = start; + start = 0; + } + if (step == undefined) { + step = 1; + } + const result: number[] = []; + for (let i = start; i < stop; i += step) { + result.push(i); + } + return result; +} -- cgit v1.2.3 From 1a543da5a018d1a99b51bfe5d7d90edff0502a11 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 31 Aug 2023 18:33:29 +0800 Subject: Fix a fatal bug in geometry. --- FrontEnd/src/components/BlobImage.tsx | 10 ++++++++-- FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 10 ++++------ FrontEnd/src/utilities/geometry.ts | 15 ++++++++------- 3 files changed, 20 insertions(+), 15 deletions(-) (limited to 'FrontEnd/src/utilities') diff --git a/FrontEnd/src/components/BlobImage.tsx b/FrontEnd/src/components/BlobImage.tsx index cccf2f74..047a13b4 100644 --- a/FrontEnd/src/components/BlobImage.tsx +++ b/FrontEnd/src/components/BlobImage.tsx @@ -1,7 +1,13 @@ -import { ComponentPropsWithoutRef, useState, useEffect, useMemo } from "react"; +import { + useState, + useEffect, + useMemo, + Ref, + ComponentPropsWithoutRef, +} from "react"; type BlobImageProps = Omit, "src"> & { - imgRef?: React.Ref; + imgRef?: Ref; src?: Blob | string | null; keyBySrc?: boolean; }; diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index 96ae971b..0df10411 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -109,7 +109,6 @@ export default function ChangeAvatarDialog() { const cancelButton = { key: "cancel", - action: "secondary", text: "operationDialog.cancel", onClick: close, } as const; @@ -117,7 +116,6 @@ export default function ChangeAvatarDialog() { const createPreviousButton = (onClick: () => void) => ({ key: "previous", - action: "secondary", text: "operationDialog.previousStep", onClick, }) as const; @@ -130,7 +128,7 @@ export default function ChangeAvatarDialog() { cancelButton, { key: "next", - action: "primary", + action: "major", text: "operationDialog.nextStep", onClick: () => setState("crop"), disabled: file == null, @@ -141,7 +139,7 @@ export default function ChangeAvatarDialog() { createPreviousButton(onCropPrevious), { key: "next", - action: "primary", + action: "major", text: "operationDialog.nextStep", onClick: onCropNext, disabled: !canCrop, @@ -153,7 +151,7 @@ export default function ChangeAvatarDialog() { createPreviousButton(onPreviewPrevious), { key: "upload", - action: "primary", + action: "major", text: "settings.dialogChangeAvatar.upload", onClick: upload, }, @@ -171,7 +169,7 @@ export default function ChangeAvatarDialog() { cancelButton, { key: "retry", - action: "primary", + action: "major", text: "operationDialog.retry", onClick: upload, }, diff --git a/FrontEnd/src/utilities/geometry.ts b/FrontEnd/src/utilities/geometry.ts index 12dd4bbb..60a8d3d4 100644 --- a/FrontEnd/src/utilities/geometry.ts +++ b/FrontEnd/src/utilities/geometry.ts @@ -49,7 +49,7 @@ export class Rect { } set right(value: number) { - this.width = this.left + value; + this.width = value - this.left; } get bottom(): number { @@ -57,7 +57,7 @@ export class Rect { } set bottom(value: number) { - this.height = this.top + value; + this.height = value - this.top; } get ratio(): number { @@ -218,15 +218,16 @@ export function adjustRectToContainer( : clamp(rect.top, container.top - rect.height, container.bottom); } else if (mode === "resize") { const noFlip = options?.resizeNoFlip; - rect.right = clamp( + const newRight = clamp( rect.right, - noFlip ? 0 : container.left, - container.right, + rect.width > 0 && noFlip ? rect.left : container.left, + rect.width < 0 && noFlip ? rect.left : container.right, ); + rect.right = newRight; rect.bottom = clamp( rect.bottom, - noFlip ? 0 : container.top, - container.bottom, + rect.height > 0 && noFlip ? rect.top : container.top, + rect.height < 0 && noFlip ? rect.top : container.bottom, ); } else { rect.left = clamp(rect.left, container.left, container.right); -- cgit v1.2.3 From 754597d49cd2d3f6295e5fe3ed68c6210bf4e8a5 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 14 Sep 2023 18:58:44 +0800 Subject: Fix mobile post create. --- FrontEnd/src/components/common.ts | 3 + FrontEnd/src/components/hooks/index.ts | 1 + FrontEnd/src/components/hooks/useWindowLeave.ts | 22 ++ FrontEnd/src/pages/timeline/edit/ImagePostEdit.css | 2 +- FrontEnd/src/pages/timeline/edit/ImagePostEdit.tsx | 6 +- .../src/pages/timeline/edit/MarkdownPostEdit.tsx | 369 ++++++++++----------- .../src/pages/timeline/edit/PlainTextPostEdit.css | 7 +- .../src/pages/timeline/edit/PlainTextPostEdit.tsx | 11 +- .../pages/timeline/edit/TimelinePostCreateView.tsx | 223 ++++++------- FrontEnd/src/services/TimelinePostBuilder.ts | 128 ------- FrontEnd/src/utilities/array.ts | 41 +++ 11 files changed, 366 insertions(+), 447 deletions(-) create mode 100644 FrontEnd/src/components/hooks/useWindowLeave.ts delete mode 100644 FrontEnd/src/services/TimelinePostBuilder.ts create mode 100644 FrontEnd/src/utilities/array.ts (limited to 'FrontEnd/src/utilities') diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts index b9b55f9b..a6c3e705 100644 --- a/FrontEnd/src/components/common.ts +++ b/FrontEnd/src/components/common.ts @@ -17,3 +17,6 @@ export type ClickableColor = ThemeColor | "grayscale" | "light" | "minor"; export { breakpoints } from "./breakpoints"; export * as geometry from "~src/utilities/geometry"; + +export * as array from "~src/utilities/array" + diff --git a/FrontEnd/src/components/hooks/index.ts b/FrontEnd/src/components/hooks/index.ts index 3c9859bc..771b0e2a 100644 --- a/FrontEnd/src/components/hooks/index.ts +++ b/FrontEnd/src/components/hooks/index.ts @@ -1,3 +1,4 @@ export { useMobile } from "./responsive"; export { default as useClickOutside } from "./useClickOutside"; export { default as useScrollToBottom } from "./useScrollToBottom"; +export { default as useWindowLeave } from "./useWindowLeave"; diff --git a/FrontEnd/src/components/hooks/useWindowLeave.ts b/FrontEnd/src/components/hooks/useWindowLeave.ts new file mode 100644 index 00000000..08777e30 --- /dev/null +++ b/FrontEnd/src/components/hooks/useWindowLeave.ts @@ -0,0 +1,22 @@ +import { useEffect } from "react"; + +import { useC, Text } from "../common"; + +export default function useWindowLeave( + allow: boolean, + message: Text = "timeline.confirmLeave", +) { + const c = useC(); + + useEffect(() => { + if (!allow) { + window.onbeforeunload = () => { + return c(message); + }; + + return () => { + window.onbeforeunload = null; + }; + } + }, [allow, message]); +} diff --git a/FrontEnd/src/pages/timeline/edit/ImagePostEdit.css b/FrontEnd/src/pages/timeline/edit/ImagePostEdit.css index 3d5e895c..df7a6af6 100644 --- a/FrontEnd/src/pages/timeline/edit/ImagePostEdit.css +++ b/FrontEnd/src/pages/timeline/edit/ImagePostEdit.css @@ -1,4 +1,4 @@ -.timeline-post-create-image { +.timeline-edit-image { max-width: 100px; max-height: 100px; } diff --git a/FrontEnd/src/pages/timeline/edit/ImagePostEdit.tsx b/FrontEnd/src/pages/timeline/edit/ImagePostEdit.tsx index d25d04b4..4676e45a 100644 --- a/FrontEnd/src/pages/timeline/edit/ImagePostEdit.tsx +++ b/FrontEnd/src/pages/timeline/edit/ImagePostEdit.tsx @@ -3,7 +3,7 @@ import classNames from "classnames"; import BlobImage from "~/src/components/BlobImage"; interface TimelinePostEditImageProps { - file: File; + file: File | null; onChange: (file: File | null) => void; disabled: boolean; className?: string; @@ -14,7 +14,7 @@ export default function ImagePostEdit(props: TimelinePostEditImageProps) { return (
- {file && } + {file && }
); } diff --git a/FrontEnd/src/pages/timeline/edit/MarkdownPostEdit.tsx b/FrontEnd/src/pages/timeline/edit/MarkdownPostEdit.tsx index d10d3f2d..0dfaf33e 100644 --- a/FrontEnd/src/pages/timeline/edit/MarkdownPostEdit.tsx +++ b/FrontEnd/src/pages/timeline/edit/MarkdownPostEdit.tsx @@ -1,216 +1,201 @@ -import * as React from "react"; +import { useEffect, useState } from "react"; import classnames from "classnames"; -import { useTranslation } from "react-i18next"; +import { marked } from "marked"; -import { - getHttpTimelineClient, - HttpTimelinePostInfo, -} from "~src/http/timeline"; +import { HttpTimelinePostPostRequestData } from "~src/http/timeline"; -import TimelinePostBuilder from "~src/services/TimelinePostBuilder"; +import base64 from "~src/utilities/base64"; -import FlatButton from "~src/components/button/FlatButton"; +import { array } from "~src/components/common"; import { TabPages } from "~src/components/tab"; -import ConfirmDialog from "~src/components/dialog/ConfirmDialog"; -import Spinner from "~src/components/Spinner"; -import IconButton from "~src/components/button/IconButton"; -import { DialogProvider, useDialog } from "~src/components/dialog"; +import { IconButton } from "~src/components/button"; +import BlobImage from "~src/components/BlobImage"; import "./MarkdownPostEdit.css"; -export interface MarkdownPostEditProps { - owner: string; - timeline: string; - onPosted: (post: HttpTimelinePostInfo) => void; - onPostError: () => void; - onClose: () => void; - className?: string; +class MarkedRenderer extends marked.Renderer { + constructor(public images: string[]) { + super(); + } + + // Custom image parser for indexed image link. + image(href: string | null, title: string | null, text: string): string { + if (href != null) { + const i = parseInt(href); + if (!isNaN(i) && i > 0 && i <= this.images.length) { + href = this.images[i - 1]; + } + } + + return this.image(href, title, text); + } } -const MarkdownPostEdit: React.FC = ({ - owner: ownerUsername, - timeline: timelineName, - onPosted, - onClose, - onPostError, - className, -}) => { - const { t } = useTranslation(); - - const [canLeave, setCanLeave] = React.useState(true); - - const [process, setProcess] = React.useState(false); - - const { controller, switchDialog } = useDialog({ - "leave-confirm": ( - - ), - }); - - const [text, _setText] = React.useState(""); - const [images, _setImages] = React.useState<{ file: File; url: string }[]>( - [], - ); - const [previewHtml, _setPreviewHtml] = React.useState(""); - - const _builder = React.useRef(null); - - const getBuilder = (): TimelinePostBuilder => { - if (_builder.current == null) { - const builder = new TimelinePostBuilder(() => { - setCanLeave(builder.isEmpty); - _setText(builder.text); - _setImages(builder.images); - _setPreviewHtml(builder.renderHtml()); - }); - _builder.current = builder; - } - return _builder.current; +function generateMarkedOptions(imageUrls: string[]): marked.MarkedOptions { + return { + mangle: false, + headerIds: false, + renderer: new MarkedRenderer(imageUrls), }; +} - const canSend = text.length > 0; +function renderHtml(text: string, imageUrls: string[]): string { + return marked.parse(text, generateMarkedOptions(imageUrls)); +} - React.useEffect(() => { - return () => { - getBuilder().dispose(); - }; - }, []); +async function build( + text: string, + images: File[], +): Promise { + return [ + { + contentType: "text/markdown", + data: await base64(text), + }, + ...(await Promise.all( + images.map(async (image) => { + const data = await base64(image); + return { contentType: image.type, data }; + }), + )), + ]; +} - React.useEffect(() => { - window.onbeforeunload = (): unknown => { - if (!canLeave) { - return t("timeline.confirmLeave"); - } - }; +export function useMarkdownEdit(disabled: boolean): { + hasContent: boolean; + clear: () => void; + build: () => Promise; + markdownEditProps: Omit; +} { + const [text, setText] = useState(""); + const [images, setImages] = useState([]); + + return { + hasContent: text !== "" || images.length !== 0, + clear: () => { + setText(""); + setImages([]); + }, + build: () => { + return build(text, images); + }, + markdownEditProps: { + disabled, + text, + images, + onTextChange: setText, + onImageAppend: (image) => setImages(array.copy_push(images, image)), + onImageMove: (o, n) => setImages(array.copy_move(images, o, n)), + onImageDelete: (i) => setImages(array.copy_delete(images, i)), + }, + }; +} + +function MarkdownPreview({ text, images }: { text: string; images: File[] }) { + const [html, setHtml] = useState(""); + + useEffect(() => { + const imageUrls = images.map((image) => URL.createObjectURL(image)); + + setHtml(renderHtml(text, imageUrls)); return () => { - window.onbeforeunload = null; + imageUrls.forEach((url) => URL.revokeObjectURL(url)); }; - }, [canLeave, t]); - - const send = async (): Promise => { - setProcess(true); - try { - const dataList = await getBuilder().build(); - const post = await getHttpTimelineClient().postPost( - ownerUsername, - timelineName, - { - dataList, - }, - ); - onPosted(post); - onClose(); - } catch (e) { - setProcess(false); - onPostError(); - } - }; + }, [text, images]); return ( - <> - - ) : ( -
- { - if (canLeave) { - onClose(); - } else { - switchDialog("leave-confirm"); +
+ ); +} + +interface MarkdownPostEditProps { + disabled: boolean; + text: string; + images: File[]; + onTextChange: (text: string) => void; + onImageAppend: (image: File) => void; + onImageMove: (oldIndex: number, newIndex: number) => void; + onImageDelete: (index: number) => void; + className?: string; +} + +export function MarkdownPostEdit({ + disabled, + text, + images, + onTextChange, + onImageAppend, + // onImageMove, + onImageDelete, + className, +}: MarkdownPostEditProps) { + return ( + { + onTextChange(event.currentTarget.value); + }} + /> + ), + }, + { + name: "images", + text: "image", + page: ( +
+ {images.map((image, index) => ( +
+ + { + onImageDelete(index); + }} + /> +
+ ))} + ) => { + const { files } = event.currentTarget; + if (files != null && files.length !== 0) { + onImageAppend(files[0]); } }} + disabled={disabled} /> - {canSend && ( - void send()} /> - )}
- ) - } - pages={[ - { - name: "text", - text: "edit", - page: ( -