feat: new design for mark read in date item
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
1ab3770f02
commit
e5b0583d38
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>)",
|
||||
|
|
|
|||
Loading…
Reference in New Issue