diff --git a/src/atoms/index.ts b/src/atoms/index.ts index aad6ccd..1096c10 100644 --- a/src/atoms/index.ts +++ b/src/atoms/index.ts @@ -2,7 +2,7 @@ import { atom } from "jotai" import type { ColumnID, SourceID } from "@shared/types" import { sources } from "@shared/sources" import { primitiveMetadataAtom } from "./primitiveMetadataAtom" -import type { Update } from "./types" +import type { ToastItem, Update } from "./types" export { primitiveMetadataAtom, preprocessMetadata } from "./primitiveMetadataAtom" @@ -56,3 +56,5 @@ export const goToTopAtom = atom({ ok: false, fn: undefined as (() => void) | undefined, }) + +export const toastAtom = atom([]) diff --git a/src/atoms/types.ts b/src/atoms/types.ts index 5dfd523..b0abfe8 100644 --- a/src/atoms/types.ts +++ b/src/atoms/types.ts @@ -1 +1,15 @@ +import type { MaybePromise } from "@shared/type.util" + export type Update = T | ((prev: T) => T) + +export interface ToastItem { + id: number + type?: "success" | "error" | "warning" | "info" + msg: string + duration?: number + action?: { + label: string + onClick: () => MaybePromise + } + onDismiss?: () => MaybePromise +} diff --git a/src/components/common/toast.tsx b/src/components/common/toast.tsx index 244bd81..b99d3cd 100644 --- a/src/components/common/toast.tsx +++ b/src/components/common/toast.tsx @@ -1,35 +1,125 @@ import clsx from "clsx" -import { Toaster } from "sonner" +import { AnimatePresence, motion } from "framer-motion" +import { useAtomValue, useSetAtom } from "jotai" +import { useCallback, useMemo, useRef } from "react" +import { useHoverDirty, useMount, useUpdateEffect, useWindowSize } from "react-use" +import { toastAtom } from "~/atoms" +import type { ToastItem } from "~/atoms/types" +import { Timer } from "~/utils" +const WIDTH = 320 export function Toast() { + const { width } = useWindowSize() + const center = useMemo(() => { + const t = (width - WIDTH) / 2 + return t > width * 0.9 ? width * 0.9 : t + }, [width]) + const toastItems = useAtomValue(toastAtom) + return ( - + + {toastItems.length && ( + + { + toastItems.map(k => ) + } + + )} + + ) +} + +const colors = { + success: "green", + error: "red", + warning: "orange", + info: "blue", +} + +function Item({ info }: { info: ToastItem }) { + const color = colors[info.type ?? "info"] + const setToastItems = useSetAtom(toastAtom) + const hidden = useCallback((dismiss = true) => { + setToastItems(prev => prev.filter(k => k.id !== info.id)) + if (dismiss) { + info.onDismiss?.() + } + }, [info, setToastItems]) + const timer = useRef() + + useMount(() => { + timer.current = new Timer(() => { + hidden() + }, info.duration ?? 5000) + return () => timer.current?.clear() + }) + + const ref = useRef(null) + const isHoverd = useHoverDirty(ref) + useUpdateEffect(() => { + if (isHoverd) { + timer.current?.pause() + } else { + timer.current?.resume() + } + }, [isHoverd]) + + return ( + +
+ { + isHoverd + ? + )} +
+ +
) } diff --git a/src/hooks/usePWA.ts b/src/hooks/usePWA.ts index 4e9d04d..440ee79 100644 --- a/src/hooks/usePWA.ts +++ b/src/hooks/usePWA.ts @@ -1,16 +1,17 @@ import { useEffect } from "react" -import { toast } from "sonner" import { useRegisterSW } from "virtual:pwa-register/react" +import { useToast } from "./useToast" export function usePWA() { const { needRefresh: [needRefresh, setNeedRefresh], updateServiceWorker, } = useRegisterSW() + const toaster = useToast() useEffect(() => { if (needRefresh) { - toast("网站有更新,点击更新", { + toaster("网站有更新,点击更新", { action: { label: "更新", onClick: () => updateServiceWorker(true), @@ -20,5 +21,5 @@ export function usePWA() { }, }) } - }, [needRefresh, updateServiceWorker, setNeedRefresh]) + }, [needRefresh, updateServiceWorker, setNeedRefresh, toaster]) } diff --git a/src/hooks/useSync.ts b/src/hooks/useSync.ts index cf784a2..ca59eed 100644 --- a/src/hooks/useSync.ts +++ b/src/hooks/useSync.ts @@ -2,8 +2,8 @@ import type { PrimitiveMetadata } from "@shared/types" import { useAtom } from "jotai" import { ofetch } from "ofetch" import { useDebounce, useMount } from "react-use" -import { toast } from "sonner" import { useLogin } from "./useLogin" +import { useToast } from "./useToast" import { preprocessMetadata, primitiveMetadataAtom } from "~/atoms" import { safeParseString } from "~/utils" @@ -49,6 +49,7 @@ export async function downloadMetadata(): Promise export function useSync() { const [primitiveMetadata, setPrimitiveMetadata] = useAtom(primitiveMetadataAtom) const { logout, login } = useLogin() + const toaster = useToast() useDebounce(async () => { if (primitiveMetadata.action === "manual") { @@ -64,7 +65,8 @@ export function useSync() { } } catch (e: any) { if (e.statusCode === 401) { - toast.error("身份校验失败,请重新登录", { + toaster("身份校验失败,请重新登录", { + type: "error", action: { label: "登录", onClick: login, diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts new file mode 100644 index 0000000..195f9c2 --- /dev/null +++ b/src/hooks/useToast.ts @@ -0,0 +1,18 @@ +import { useSetAtom } from "jotai" +import { useCallback } from "react" +import { toastAtom } from "~/atoms" +import type { ToastItem } from "~/atoms/types" + +export function useToast() { + const setToastItems = useSetAtom(toastAtom) + return useCallback((msg: string, props?: Omit) => { + setToastItems(prev => [ + { + msg, + id: Date.now(), + ...props, + }, + ...prev, + ]) + }, [setToastItems]) +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 1554b52..9d21efb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +import type { MaybePromise } from "@shared/type.util" + export function safeParseString(str: any) { try { return JSON.parse(str) @@ -5,3 +7,31 @@ export function safeParseString(str: any) { return "" } } + +export class Timer { + private timerId?: any + private start!: number + private remaining: number + private callback: () => MaybePromise + + constructor(callback: () => MaybePromise, delay: number) { + this.callback = callback + this.remaining = delay + this.resume() + } + + pause() { + clearTimeout(this.timerId) + this.remaining -= Date.now() - this.start + } + + resume() { + this.start = Date.now() + clearTimeout(this.timerId) + this.timerId = setTimeout(this.callback, this.remaining) + } + + clear() { + clearTimeout(this.timerId) + } +} diff --git a/uno.config.ts b/uno.config.ts index c539fff..06e8606 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -36,7 +36,7 @@ export default defineConfig({ "btn": "op50 hover:op85", }, safelist: [ - ...[...new Set(Object.values(sources).map(k => k.color))].map(k => + ...["orange", ...new Set(Object.values(sources).map(k => k.color))].map(k => `bg-${k} color-${k} border-${k} sprinkle-${k} shadow-${k} bg-${k}-500 color-${k}-500 dark:bg-${k} dark:color-${k}`.trim().split(/\s+/)).flat(),