Skip to content

Commit 30d5395

Browse files
committed
feat: read indicator
Signed-off-by: Innei <[email protected]>
1 parent ce48b33 commit 30d5395

File tree

4 files changed

+83
-11
lines changed

4 files changed

+83
-11
lines changed

src/app/notes/[id]/page.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ClientOnly } from '~/components/common/ClientOnly'
1414
import { PageDataHolder } from '~/components/common/PageHolder'
1515
import { MdiClockOutline } from '~/components/icons/clock'
1616
import { useSetHeaderMetaInfo } from '~/components/layout/header/internal/hooks'
17+
import { Divider } from '~/components/ui/divider'
1718
import { FloatPopover } from '~/components/ui/float-popover'
1819
import { Loading } from '~/components/ui/loading'
1920
import { Markdown } from '~/components/ui/markdown'
@@ -33,6 +34,7 @@ import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'
3334
import { parseDate } from '~/utils/datetime'
3435
import { springScrollToTop } from '~/utils/scroller'
3536

37+
import { ReadIndicator } from '../../../components/common/ReadIndicator'
3638
import { NoteActionAside } from '../../../components/widgets/note/NoteActionAside'
3739
import { NoteHideIfSecret } from '../../../components/widgets/note/NoteHideIfSecret'
3840
import { NoteMetaBar } from '../../../components/widgets/note/NoteMetaBar'
@@ -114,6 +116,7 @@ const NotePage = memo(({ note }: { note: NoteModel }) => {
114116
<TocAside
115117
className="sticky top-[120px] ml-4 mt-[120px]"
116118
treeClassName="max-h-[calc(100vh-6rem-4.5rem-300px)] h-[calc(100vh-6rem-4.5rem-300px)] min-h-[120px] relative"
119+
accessory={NoteReadIndicator}
117120
>
118121
<NoteActionAside className="translate-y-full" />
119122
</TocAside>
@@ -130,6 +133,14 @@ const NotePage = memo(({ note }: { note: NoteModel }) => {
130133
</Suspense>
131134
)
132135
})
136+
const NoteReadIndicator = () => {
137+
return (
138+
<li>
139+
<Divider />
140+
<ReadIndicator className="text-sm" />
141+
</li>
142+
)
143+
}
133144

134145
const NoteTitle = () => {
135146
const note = useNoteData()
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
2+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
3+
'use client'
4+
5+
import type { ElementType } from 'react'
6+
7+
import {
8+
useArticleElementPositsion,
9+
useArticleElementSize,
10+
} from '~/providers/article/article-element-provider'
11+
import { usePageScrollLocationSelector } from '~/providers/root/page-scroll-info-provider'
12+
import { clsxm } from '~/utils/helper'
13+
14+
export const ReadIndicator: Component<{
15+
as?: ElementType
16+
}> = ({ className, as }) => {
17+
const { y } = useArticleElementPositsion()
18+
const { h } = useArticleElementSize()
19+
const readPercent = usePageScrollLocationSelector((scrollTop) => {
20+
return Math.floor(Math.min(Math.max(0, ((scrollTop - y) / h) * 100), 100))
21+
})
22+
const As = as || 'span'
23+
return (
24+
<As className={clsxm('text-gray-800 dark:text-neutral-300', className)}>
25+
{readPercent}%
26+
</As>
27+
)
28+
}

src/components/widgets/toc/Toc.tsx

+28-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
1+
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'
22
import { motion } from 'framer-motion'
33
import { atom, useAtom } from 'jotai'
4+
import type { FC } from 'react'
45
import type { ITocItem } from './TocItem'
56

67
import { RightToLeftTransitionView } from '~/components/ui/transition/RightToLeftTransitionView'
@@ -15,10 +16,14 @@ export type TocAsideProps = {
1516
treeClassName?: string
1617
}
1718

18-
export const TocAside: Component<TocAsideProps> = ({
19+
interface TocSharedProps {
20+
accessory?: React.ReactNode | React.FC
21+
}
22+
export const TocAside: Component<TocAsideProps & TocSharedProps> = ({
1923
className,
2024
children,
2125
treeClassName,
26+
accessory,
2227
}) => {
2328
const containerRef = useRef<HTMLUListElement>(null)
2429
const $article = useArticleElement()
@@ -91,19 +96,30 @@ export const TocAside: Component<TocAsideProps> = ({
9196
rootDepth={rootDepth}
9297
containerRef={containerRef}
9398
className={clsxm('absolute max-h-[75vh]', treeClassName)}
99+
accessory={accessory}
94100
/>
95101
{children}
96102
</aside>
97103
)
98104
}
99105

100-
const TocTree: Component<{
101-
toc: ITocItem[]
102-
activeId: string | null
103-
setActiveId: (id: string | null) => void
104-
rootDepth: number
105-
containerRef: React.MutableRefObject<HTMLUListElement | null>
106-
}> = ({ toc, activeId, setActiveId, rootDepth, containerRef, className }) => {
106+
const TocTree: Component<
107+
{
108+
toc: ITocItem[]
109+
activeId: string | null
110+
setActiveId: (id: string | null) => void
111+
rootDepth: number
112+
containerRef: React.MutableRefObject<HTMLUListElement | null>
113+
} & TocSharedProps
114+
> = ({
115+
toc,
116+
activeId,
117+
setActiveId,
118+
rootDepth,
119+
containerRef,
120+
className,
121+
accessory,
122+
}) => {
107123
const handleScrollTo = useCallback(
108124
(i: number, $el: HTMLElement | null, anchorId: string) => {
109125
if ($el) {
@@ -134,6 +150,9 @@ const TocTree: Component<{
134150
/>
135151
)
136152
})}
153+
{React.isValidElement(accessory)
154+
? accessory
155+
: React.createElement(accessory as FC)}
137156
</ul>
138157
)
139158
}

src/providers/article/article-element-provider.tsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ const [
2020
w: 0,
2121
})
2222

23+
const [
24+
ArticleElementPositsionProviderInternal,
25+
useArticleElementPositsion,
26+
useSetArticleElementPositsion,
27+
] = createContextState({
28+
x: 0,
29+
y: 0,
30+
})
31+
2332
const [
2433
IsEOArticleElementProviderInternal,
2534
useIsEOArticleElement,
@@ -29,6 +38,7 @@ const [
2938
const Providers = [
3039
<ArticleElementProviderInternal key="ArticleElementProviderInternal" />,
3140
<ArticleElementSizeProviderInternal key="ArticleElementSizeProviderInternal" />,
41+
<ArticleElementPositsionProviderInternal key="ArticleElementPositsionProviderInternal" />,
3242
<IsEOArticleElementProviderInternal key="IsEOArticleElementProviderInternal" />,
3343
]
3444
const ArticleElementProvider: Component = ({ children, className }) => {
@@ -41,16 +51,19 @@ const ArticleElementProvider: Component = ({ children, className }) => {
4151
}
4252
const ArticleElementResizeObserver = () => {
4353
const setSize = useSetArticleElementSize()
54+
const setPos = useSetArticleElementPositsion()
4455
const $article = useArticleElement()
4556
useIsomorphicLayoutEffect(() => {
4657
if (!$article) return
47-
const { height, width } = $article.getBoundingClientRect()
58+
const { height, width, x, y } = $article.getBoundingClientRect()
4859
setSize({ h: height, w: width })
60+
setPos({ x, y })
4961

5062
const observer = new ResizeObserver((entries) => {
5163
const entry = entries[0]
52-
const { height, width } = entry.contentRect
64+
const { height, width, x, y } = entry.contentRect
5365
setSize({ h: height, w: width })
66+
setPos({ x, y })
5467
})
5568
observer.observe($article)
5669
return () => {
@@ -107,4 +120,5 @@ export {
107120
useArticleElement,
108121
useIsEOArticleElement,
109122
useArticleElementSize,
123+
useArticleElementPositsion,
110124
}

0 commit comments

Comments
 (0)