feat: new design for mark read in date item

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-08-21 16:24:27 +08:00
parent 1ab3770f02
commit e5b0583d38
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
11 changed files with 217 additions and 90 deletions

View File

@ -137,7 +137,7 @@ export const RelativeDay = ({ date }: { date: Date }) => {
return (
<Tooltip>
<TooltipTrigger>{dateString}</TooltipTrigger>
<TooltipTrigger onFocusCapture={stopPropagation}>{dateString}</TooltipTrigger>
<TooltipPortal>
<TooltipContent>{dayjs(date).format("llll")}</TooltipContent>
</TooltipPortal>

View File

@ -0,0 +1,19 @@
.popover {
border: 0.5px solid rgb(216, 216, 216);
box-shadow: rgba(0, 0, 0, 0.067) 0px 3px 8px, rgba(0, 0, 0, 0.067) 0px 2px 5px,
rgba(0, 0, 0, 0.067) 0px 1px 1px;
border-radius: 6px;
@apply bg-background z-50 outline-none;
}
[data-theme="dark"] .popover {
box-shadow: rgba(0, 0, 0, 0.184) 0px 3px 8px, rgba(0, 0, 0, 0.184) 0px 2px 5px,
rgba(0, 0, 0, 0.184) 0px 1px 1px;
background-color: rgb(27, 28, 31);
border: 0.5px solid rgb(56, 59, 65);
}
.popover__padding--base {
padding: 8px 16px;
}

View File

@ -2,6 +2,8 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@renderer/lib/utils"
import * as React from "react"
import styles from "./index.module.css"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
@ -18,7 +20,9 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
styles.popover,
styles["popover__padding--base"],
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}

View File

@ -1,4 +1,5 @@
import { AnimatePresence, m } from "framer-motion"
import { useEffect, useState } from "react"
export const IconScaleTransition = ({
icon1,
@ -9,24 +10,35 @@ export const IconScaleTransition = ({
icon1: string
icon2: string
}) => (
<AnimatePresence mode="popLayout">
{status === "init" ? (
<m.i
className={icon1}
key="1"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
/>
) : (
<m.i
className={icon2}
key="2"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
/>
)}
</AnimatePresence>
)
}) => {
const [isMount, isMounted] = useState(false)
useEffect(() => {
isMounted(true)
return () => {
isMounted(false)
}
}, [])
const initial = isMount ? { scale: 0 } : true
return (
<AnimatePresence mode="popLayout">
{status === "init" ? (
<m.i
className={icon1}
key="1"
initial={initial}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
/>
) : (
<m.i
className={icon2}
key="2"
initial={initial}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
/>
)}
</AnimatePresence>
)
}

View File

@ -17,12 +17,17 @@ export type NativeMenuItem =
| { type: "separator", disabled?: boolean }
function sortShortcutsString(shortcut: string) {
// always order by Shift, Ctrl, Alt, Meta
const arr = shortcut.split("+")
const order = ["Shift", "Ctrl", "Alt", "Meta"]
arr.sort((a, b) => order.indexOf(a) - order.indexOf(b))
return arr.join("+")
const arr = shortcut.split("+")
const sortedModifiers = arr
.filter((key) => order.includes(key))
.sort((a, b) => order.indexOf(a) - order.indexOf(b))
const otherKeys = arr.filter((key) => !order.includes(key))
return [...sortedModifiers, ...otherKeys].join("+")
}
export const showNativeMenu = async (
items: Array<Nullable<NativeMenuItem | false>>,

View File

@ -1,20 +1,30 @@
import { ActionButton } from "@renderer/components/ui/button"
import { RelativeDay } from "@renderer/components/ui/datetime"
import { useScrollViewElement } from "@renderer/components/ui/scroll-area/hooks"
import { IconScaleTransition } from "@renderer/components/ux/transition/icon"
import { FeedViewType } from "@renderer/lib/enum"
import { cn } from "@renderer/lib/utils"
import { throttle } from "lodash-es"
import { memo, useLayoutEffect, useMemo, useRef, useState } from "react"
import {
memo,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import { useMarkAll } from "../hooks/useMarkAll"
import { MarkAllReadButton } from "./mark-all-button"
const useParseDate = (date: string) => useMemo(() => {
const dateObj = new Date(date)
return {
dateObj,
startOfDay: new Date(dateObj.setHours(0, 0, 0, 0)).getTime(),
endOfDay: new Date(dateObj.setHours(23, 59, 59, 999)).getTime(),
}
}, [date])
const useParseDate = (date: string) =>
useMemo(() => {
const dateObj = new Date(date)
return {
dateObj,
startOfDay: new Date(dateObj.setHours(0, 0, 0, 0)).getTime(),
endOfDay: new Date(dateObj.setHours(23, 59, 59, 999)).getTime(),
}
}, [date])
const useSticky = () => {
const $scroller = useScrollViewElement()
@ -64,18 +74,70 @@ const UniversalDateItem = ({
const { startOfDay, endOfDay, dateObj } = useParseDate(date)
const { isSticky, itemRef } = useSticky()
const RelativeElement = <RelativeDay date={dateObj} />
const handleMarkAllAsRead = useMarkAll({
startTime: startOfDay,
endTime: endOfDay,
})
const [confirmMark, setConfirmMark] = useState(false)
const timerRef = useRef<any>()
return (
<div className={cn(className, isSticky && "border-b")} ref={itemRef}>
<MarkAllReadButton
which={RelativeElement}
className="size-7 text-base"
filter={{
startTime: startOfDay,
endTime: endOfDay,
<div
className={cn(className, isSticky && "border-b")}
ref={itemRef}
onMouseEnter={() => {
clearTimeout(timerRef.current)
}}
onMouseLeave={() => {
timerRef.current = setTimeout(() => {
setConfirmMark(false)
}, 1000)
}}
>
<ActionButton
tooltip={(
<span>
Mark
<span> </span>
{RelativeElement}
<span> </span>
as read
</span>
)}
onClick={() => {
if (confirmMark) {
clearTimeout(timerRef.current)
handleMarkAllAsRead()
setConfirmMark(false)
} else {
setConfirmMark(true)
}
}}
/>
{RelativeElement}
className="size-7 text-base"
>
<IconScaleTransition
icon1="i-mgc-check-filled text-green-600"
icon2="i-mgc-check-circle-cute-re"
status={!confirmMark ? "done" : "init"}
/>
</ActionButton>
{confirmMark ? (
<div className="animate-mask-in">
Mark
<span> </span>
{RelativeElement}
{" "}
as read?
</div>
) : (
RelativeElement
)}
</div>
)
}

View File

@ -1,8 +1,5 @@
import { PopoverPortal } from "@radix-ui/react-popover"
import {
ActionButton,
Button,
} from "@renderer/components/ui/button"
import { ActionButton, Button } from "@renderer/components/ui/button"
import {
Popover,
PopoverClose,
@ -10,21 +7,16 @@ import {
PopoverTrigger,
} from "@renderer/components/ui/popover"
import { shortcuts } from "@renderer/constants/shortcuts"
import { useRouteParms } from "@renderer/hooks/biz/useRouteParams"
import { cn } from "@renderer/lib/utils"
import {
subscriptionActions,
useFolderFeedsByFeedId,
} from "@renderer/store/subscription"
import { AnimatePresence, m } from "framer-motion"
import type { FC, ReactNode } from "react"
import { forwardRef, useCallback, useState } from "react"
import { forwardRef, useState } from "react"
import type { MarkAllFilter } from "../hooks/useMarkAll"
import { useMarkAll } from "../hooks/useMarkAll"
interface MarkAllButtonProps {
filter?: {
startTime: number
endTime: number
}
filter?: MarkAllFilter
className?: string
which?: ReactNode
@ -46,8 +38,9 @@ export const MarkAllReadButton = forwardRef<
tooltip={(
<span>
Mark
<span> </span>
{which}
{" "}
<span> </span>
as read
</span>
)}
@ -58,19 +51,23 @@ export const MarkAllReadButton = forwardRef<
</ActionButton>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent className="flex w-fit flex-col items-center justify-center gap-3 text-[0.94rem] font-medium">
<div>
<PopoverContent className="flex w-fit flex-col items-center justify-center gap-3 font-medium [&_button]:text-xs">
<div className="text-sm">
Mark
<span> </span>
{which}
{" "}
<span> </span>
as read?
</div>
<div className="space-x-4">
<PopoverClose>
<Button variant="outline">Cancel</Button>
<Button className="px-1" variant="outline">
Cancel
</Button>
</PopoverClose>
<Button
className="px-1"
onClick={() => {
handleMarkAllAsRead()
setMarkPopoverOpen(false)
@ -148,28 +145,3 @@ export const FlatMarkAllReadButton: FC<MarkAllButtonProps> = (props) => {
</Button>
)
}
const useMarkAll = (filter: MarkAllButtonProps["filter"]) => {
const routerParams = useRouteParms()
const { feedId, view } = routerParams
const folderIds = useFolderFeedsByFeedId({
feedId,
view,
})
return useCallback(async () => {
if (!routerParams) return
if (typeof routerParams.feedId === "number" || routerParams.isAllFeeds) {
subscriptionActions.markReadByView(view, filter)
} else if (folderIds) {
subscriptionActions.markReadByFeedIds(folderIds, view, filter)
} else if (routerParams.feedId) {
subscriptionActions.markReadByFeedIds(
routerParams.feedId?.split(","),
view,
filter,
)
}
}, [routerParams, folderIds, view, filter])
}

View File

@ -0,0 +1,33 @@
import { useRouteParms } from "@renderer/hooks/biz/useRouteParams"
import { subscriptionActions } from "@renderer/store/subscription"
import { useFolderFeedsByFeedId } from "@renderer/store/subscription/hooks"
import { useCallback } from "react"
export interface MarkAllFilter {
startTime: number
endTime: number
}
export const useMarkAll = (filter?: MarkAllFilter) => {
const routerParams = useRouteParms()
const { feedId, view } = routerParams
const folderIds = useFolderFeedsByFeedId({
feedId,
view,
})
return useCallback(async () => {
if (!routerParams) return
if (typeof routerParams.feedId === "number" || routerParams.isAllFeeds) {
subscriptionActions.markReadByView(view, filter)
} else if (folderIds) {
subscriptionActions.markReadByFeedIds(folderIds, view, filter)
} else if (routerParams.feedId) {
subscriptionActions.markReadByFeedIds(
routerParams.feedId?.split(","),
view,
filter,
)
}
}, [routerParams, folderIds, view, filter])
}

View File

@ -191,13 +191,14 @@ export function FeedColumn({ children }: PropsWithChildren) {
>
{normalStyle && (
<div
className="relative flex items-center gap-1 text-xl font-bold"
className="relative flex items-center gap-1 font-default text-lg font-semibold"
onClick={(e) => {
e.stopPropagation()
navigateBackHome()
}}
>
<Logo className="mr-1 size-6" />
{APP_NAME}
</div>
)}

View File

@ -280,3 +280,21 @@
box-shadow: -12px 0px 17px 2px rgba(0, 0, 0, 0.653);
}
}
@layer components {
.animate-mask-in {
animation: mask-in 0.5s ease-in-out forwards;
}
@keyframes mask-in {
0% {
mask: linear-gradient(90deg, #000 25%, #000000e6 50%, #00000000) 150% 0 /
400% no-repeat;
opacity: 0.2;
}
100% {
mask: linear-gradient(90deg, #000 25%, #000000e6 50%, #00000000) 0 / 400%
no-repeat;
opacity: 1;
}
}
}

View File

@ -30,6 +30,7 @@ export default resolveConfig({
extend: {
fontFamily: {
theme: "var(--fo-font-family)",
default: "SN pro, sans-serif, system-ui",
},
colors: {
border: "hsl(var(--border) / <alpha-value>)",