@prose-reader/react-reader 1.117.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/package.json +36 -0
- package/src/common/useFullscreen.ts +44 -0
- package/src/components/ui/avatar.tsx +74 -0
- package/src/components/ui/checkbox.tsx +25 -0
- package/src/components/ui/close-button.tsx +17 -0
- package/src/components/ui/color-mode.tsx +75 -0
- package/src/components/ui/dialog.tsx +62 -0
- package/src/components/ui/drawer.tsx +52 -0
- package/src/components/ui/field.tsx +33 -0
- package/src/components/ui/input-group.tsx +53 -0
- package/src/components/ui/popover.tsx +59 -0
- package/src/components/ui/progress.tsx +34 -0
- package/src/components/ui/provider.tsx +12 -0
- package/src/components/ui/radio.tsx +24 -0
- package/src/components/ui/slider.tsx +82 -0
- package/src/components/ui/toggle-tip.tsx +70 -0
- package/src/components/ui/tooltip.tsx +46 -0
- package/src/context/ReactReaderProvider.tsx +14 -0
- package/src/context/context.ts +6 -0
- package/src/context/useReader.ts +9 -0
- package/src/index.ts +2 -0
- package/src/navigation/QuickMenu/BottomBar.tsx +65 -0
- package/src/navigation/QuickMenu/PaginationInfoSection.tsx +62 -0
- package/src/navigation/QuickMenu/QuickBar.tsx +40 -0
- package/src/navigation/QuickMenu/QuickMenu.tsx +22 -0
- package/src/navigation/QuickMenu/Scrubber.tsx +138 -0
- package/src/navigation/QuickMenu/TimeIndicator.tsx +29 -0
- package/src/navigation/QuickMenu/TopBar.tsx +72 -0
- package/src/navigation/useNavigationContext.ts +46 -0
- package/src/pagination/usePagination.ts +29 -0
- package/src/settings/useSettings.ts +9 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +22 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
|
|
4
|
+
export interface SliderProps extends ChakraSlider.RootProps {
|
|
5
|
+
marks?: Array<number | { value: number; label: React.ReactNode }>
|
|
6
|
+
label?: React.ReactNode
|
|
7
|
+
showValue?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
11
|
+
function Slider(props, ref) {
|
|
12
|
+
const { marks: marksProp, label, showValue, ...rest } = props
|
|
13
|
+
const value = props.defaultValue ?? props.value
|
|
14
|
+
|
|
15
|
+
const marks = marksProp?.map((mark) => {
|
|
16
|
+
if (typeof mark === "number") return { value: mark, label: undefined }
|
|
17
|
+
return mark
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const hasMarkLabel = !!marks?.some((mark) => mark.label)
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
|
|
24
|
+
{label && !showValue && (
|
|
25
|
+
<ChakraSlider.Label>{label}</ChakraSlider.Label>
|
|
26
|
+
)}
|
|
27
|
+
{label && showValue && (
|
|
28
|
+
<HStack justify="space-between">
|
|
29
|
+
<ChakraSlider.Label>{label}</ChakraSlider.Label>
|
|
30
|
+
<ChakraSlider.ValueText />
|
|
31
|
+
</HStack>
|
|
32
|
+
)}
|
|
33
|
+
<ChakraSlider.Control data-has-mark-label={hasMarkLabel || undefined}>
|
|
34
|
+
<ChakraSlider.Track>
|
|
35
|
+
<ChakraSlider.Range />
|
|
36
|
+
</ChakraSlider.Track>
|
|
37
|
+
<SliderThumbs value={value} />
|
|
38
|
+
<SliderMarks marks={marks} />
|
|
39
|
+
</ChakraSlider.Control>
|
|
40
|
+
</ChakraSlider.Root>
|
|
41
|
+
)
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
function SliderThumbs(props: { value?: number[] }) {
|
|
46
|
+
const { value } = props
|
|
47
|
+
return (
|
|
48
|
+
<For each={value}>
|
|
49
|
+
{(_, index) => (
|
|
50
|
+
<ChakraSlider.Thumb key={index} index={index}>
|
|
51
|
+
<ChakraSlider.HiddenInput />
|
|
52
|
+
</ChakraSlider.Thumb>
|
|
53
|
+
)}
|
|
54
|
+
</For>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SliderMarksProps {
|
|
59
|
+
marks?: Array<number | { value: number; label: React.ReactNode }>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(
|
|
63
|
+
function SliderMarks(props, ref) {
|
|
64
|
+
const { marks } = props
|
|
65
|
+
if (!marks?.length) return null
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<ChakraSlider.MarkerGroup ref={ref}>
|
|
69
|
+
{marks.map((mark, index) => {
|
|
70
|
+
const value = typeof mark === "number" ? mark : mark.value
|
|
71
|
+
const label = typeof mark === "number" ? undefined : mark.label
|
|
72
|
+
return (
|
|
73
|
+
<ChakraSlider.Marker key={index} value={value}>
|
|
74
|
+
<ChakraSlider.MarkerIndicator />
|
|
75
|
+
{label}
|
|
76
|
+
</ChakraSlider.Marker>
|
|
77
|
+
)
|
|
78
|
+
})}
|
|
79
|
+
</ChakraSlider.MarkerGroup>
|
|
80
|
+
)
|
|
81
|
+
},
|
|
82
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Popover as ChakraPopover, IconButton, Portal } from "@chakra-ui/react"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { HiOutlineInformationCircle } from "react-icons/hi"
|
|
4
|
+
|
|
5
|
+
export interface ToggleTipProps extends ChakraPopover.RootProps {
|
|
6
|
+
showArrow?: boolean
|
|
7
|
+
portalled?: boolean
|
|
8
|
+
portalRef?: React.RefObject<HTMLElement>
|
|
9
|
+
content?: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ToggleTip = React.forwardRef<HTMLDivElement, ToggleTipProps>(
|
|
13
|
+
function ToggleTip(props, ref) {
|
|
14
|
+
const {
|
|
15
|
+
showArrow,
|
|
16
|
+
children,
|
|
17
|
+
portalled = true,
|
|
18
|
+
content,
|
|
19
|
+
portalRef,
|
|
20
|
+
...rest
|
|
21
|
+
} = props
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ChakraPopover.Root
|
|
25
|
+
{...rest}
|
|
26
|
+
positioning={{ ...rest.positioning, gutter: 4 }}
|
|
27
|
+
>
|
|
28
|
+
<ChakraPopover.Trigger asChild>{children}</ChakraPopover.Trigger>
|
|
29
|
+
<Portal disabled={!portalled} container={portalRef}>
|
|
30
|
+
<ChakraPopover.Positioner>
|
|
31
|
+
<ChakraPopover.Content
|
|
32
|
+
width="auto"
|
|
33
|
+
px="2"
|
|
34
|
+
py="1"
|
|
35
|
+
textStyle="xs"
|
|
36
|
+
rounded="sm"
|
|
37
|
+
ref={ref}
|
|
38
|
+
>
|
|
39
|
+
{showArrow && (
|
|
40
|
+
<ChakraPopover.Arrow>
|
|
41
|
+
<ChakraPopover.ArrowTip />
|
|
42
|
+
</ChakraPopover.Arrow>
|
|
43
|
+
)}
|
|
44
|
+
{content}
|
|
45
|
+
</ChakraPopover.Content>
|
|
46
|
+
</ChakraPopover.Positioner>
|
|
47
|
+
</Portal>
|
|
48
|
+
</ChakraPopover.Root>
|
|
49
|
+
)
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
export const InfoTip = React.forwardRef<
|
|
54
|
+
HTMLDivElement,
|
|
55
|
+
Partial<ToggleTipProps>
|
|
56
|
+
>(function InfoTip(props, ref) {
|
|
57
|
+
const { children, ...rest } = props
|
|
58
|
+
return (
|
|
59
|
+
<ToggleTip content={children} {...rest} ref={ref}>
|
|
60
|
+
<IconButton
|
|
61
|
+
variant="ghost"
|
|
62
|
+
aria-label="info"
|
|
63
|
+
size="2xs"
|
|
64
|
+
colorPalette="gray"
|
|
65
|
+
>
|
|
66
|
+
<HiOutlineInformationCircle />
|
|
67
|
+
</IconButton>
|
|
68
|
+
</ToggleTip>
|
|
69
|
+
)
|
|
70
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
|
|
4
|
+
export interface TooltipProps extends ChakraTooltip.RootProps {
|
|
5
|
+
showArrow?: boolean
|
|
6
|
+
portalled?: boolean
|
|
7
|
+
portalRef?: React.RefObject<HTMLElement>
|
|
8
|
+
content: React.ReactNode
|
|
9
|
+
contentProps?: ChakraTooltip.ContentProps
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
|
14
|
+
function Tooltip(props, ref) {
|
|
15
|
+
const {
|
|
16
|
+
showArrow,
|
|
17
|
+
children,
|
|
18
|
+
disabled,
|
|
19
|
+
portalled = true,
|
|
20
|
+
content,
|
|
21
|
+
contentProps,
|
|
22
|
+
portalRef,
|
|
23
|
+
...rest
|
|
24
|
+
} = props
|
|
25
|
+
|
|
26
|
+
if (disabled) return children
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<ChakraTooltip.Root {...rest}>
|
|
30
|
+
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
|
31
|
+
<Portal disabled={!portalled} container={portalRef}>
|
|
32
|
+
<ChakraTooltip.Positioner>
|
|
33
|
+
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
|
34
|
+
{showArrow && (
|
|
35
|
+
<ChakraTooltip.Arrow>
|
|
36
|
+
<ChakraTooltip.ArrowTip />
|
|
37
|
+
</ChakraTooltip.Arrow>
|
|
38
|
+
)}
|
|
39
|
+
{content}
|
|
40
|
+
</ChakraTooltip.Content>
|
|
41
|
+
</ChakraTooltip.Positioner>
|
|
42
|
+
</Portal>
|
|
43
|
+
</ChakraTooltip.Root>
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Reader } from "@prose-reader/core"
|
|
2
|
+
import { memo } from "react"
|
|
3
|
+
import { ReaderContext } from "./context"
|
|
4
|
+
|
|
5
|
+
export const ReactReaderProvider = memo(
|
|
6
|
+
({
|
|
7
|
+
children,
|
|
8
|
+
reader,
|
|
9
|
+
}: { children?: React.ReactNode; reader: Reader | undefined }) => {
|
|
10
|
+
return (
|
|
11
|
+
<ReaderContext.Provider value={reader}>{children}</ReaderContext.Provider>
|
|
12
|
+
)
|
|
13
|
+
},
|
|
14
|
+
)
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Box, IconButton, Stack } from "@chakra-ui/react"
|
|
2
|
+
import {
|
|
3
|
+
RxDoubleArrowDown,
|
|
4
|
+
RxDoubleArrowLeft,
|
|
5
|
+
RxDoubleArrowRight,
|
|
6
|
+
RxDoubleArrowUp,
|
|
7
|
+
} from "react-icons/rx"
|
|
8
|
+
import { useObserve } from "reactjrx"
|
|
9
|
+
import { useReader } from "../../context/useReader"
|
|
10
|
+
import { PaginationInfoSection } from "./PaginationInfoSection"
|
|
11
|
+
import { QuickBar } from "./QuickBar"
|
|
12
|
+
import { Scrubber } from "./Scrubber"
|
|
13
|
+
import { TimeIndicator } from "./TimeIndicator"
|
|
14
|
+
|
|
15
|
+
export const BottomBar = ({ open }: { open: boolean }) => {
|
|
16
|
+
const reader = useReader()
|
|
17
|
+
const navigation = useObserve(() => reader?.navigation.state$, [reader])
|
|
18
|
+
const settings = useObserve(() => reader?.settings.values$, [reader])
|
|
19
|
+
const isVerticalDirection = settings?.computedPageTurnDirection === "vertical"
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<QuickBar present={open} position="bottom" height={130}>
|
|
23
|
+
<IconButton
|
|
24
|
+
aria-label="left"
|
|
25
|
+
size="lg"
|
|
26
|
+
variant="ghost"
|
|
27
|
+
flexShrink={0}
|
|
28
|
+
onClick={() => reader?.navigation.goToLeftOrTopSpineItem()}
|
|
29
|
+
disabled={
|
|
30
|
+
!navigation?.canGoLeftSpineItem && !navigation?.canGoTopSpineItem
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
{isVerticalDirection ? <RxDoubleArrowUp /> : <RxDoubleArrowLeft />}
|
|
34
|
+
</IconButton>
|
|
35
|
+
<Stack
|
|
36
|
+
flex={1}
|
|
37
|
+
maxW={400}
|
|
38
|
+
gap={2}
|
|
39
|
+
alignItems="center"
|
|
40
|
+
overflow="auto"
|
|
41
|
+
px={4}
|
|
42
|
+
>
|
|
43
|
+
<PaginationInfoSection />
|
|
44
|
+
<Box height={5} maxW={300} width="100%" overflow="visible">
|
|
45
|
+
<Scrubber />
|
|
46
|
+
</Box>
|
|
47
|
+
</Stack>
|
|
48
|
+
<IconButton
|
|
49
|
+
aria-label="right"
|
|
50
|
+
size="lg"
|
|
51
|
+
flexShrink={0}
|
|
52
|
+
variant="ghost"
|
|
53
|
+
disabled={
|
|
54
|
+
!navigation?.canGoRightSpineItem && !navigation?.canGoBottomSpineItem
|
|
55
|
+
}
|
|
56
|
+
onClick={() => {
|
|
57
|
+
reader?.navigation.goToRightOrBottomSpineItem()
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{isVerticalDirection ? <RxDoubleArrowDown /> : <RxDoubleArrowRight />}
|
|
61
|
+
</IconButton>
|
|
62
|
+
<TimeIndicator position="absolute" bottom={0} left={0} p={2} />
|
|
63
|
+
</QuickBar>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { HStack, Stack, Text } from "@chakra-ui/react"
|
|
2
|
+
import {
|
|
3
|
+
ProgressBar,
|
|
4
|
+
ProgressRoot,
|
|
5
|
+
ProgressValueText,
|
|
6
|
+
} from "../../components/ui/progress"
|
|
7
|
+
import { usePagination } from "../../pagination/usePagination"
|
|
8
|
+
import { useNavigationContext } from "../useNavigationContext"
|
|
9
|
+
|
|
10
|
+
export const PaginationInfoSection = () => {
|
|
11
|
+
const pagination = usePagination()
|
|
12
|
+
const {
|
|
13
|
+
hasOnlyOnePage,
|
|
14
|
+
leftPageIndex,
|
|
15
|
+
rightPageIndex,
|
|
16
|
+
totalApproximatePages,
|
|
17
|
+
beginAndEndAreDifferent,
|
|
18
|
+
} = useNavigationContext()
|
|
19
|
+
const progress = Math.round((pagination?.percentageEstimateOfBook ?? 0) * 100)
|
|
20
|
+
|
|
21
|
+
const buildTitleChain = (
|
|
22
|
+
chapterInfo: NonNullable<typeof pagination>["beginChapterInfo"],
|
|
23
|
+
): string => {
|
|
24
|
+
if (chapterInfo?.subChapter) {
|
|
25
|
+
return `${chapterInfo.title} / ${buildTitleChain(chapterInfo.subChapter)}`
|
|
26
|
+
}
|
|
27
|
+
return chapterInfo?.title || ""
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const chapterTitle = buildTitleChain(pagination?.beginChapterInfo)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Stack alignItems="center" gap={1} maxW="100%" overflow="auto">
|
|
34
|
+
<ProgressRoot value={progress} size="xs" width={150}>
|
|
35
|
+
<HStack justifyContent="space-between">
|
|
36
|
+
<ProgressBar width={110} />
|
|
37
|
+
<ProgressValueText>{`${progress}%`}</ProgressValueText>
|
|
38
|
+
</HStack>
|
|
39
|
+
</ProgressRoot>
|
|
40
|
+
<Text truncate maxWidth="100%" fontSize="sm" mt={1}>
|
|
41
|
+
{chapterTitle ? `Chapter: ${chapterTitle}` : `\u00A0`}
|
|
42
|
+
</Text>
|
|
43
|
+
{!hasOnlyOnePage && (
|
|
44
|
+
<HStack>
|
|
45
|
+
<Text fontSize="xs">
|
|
46
|
+
{beginAndEndAreDifferent
|
|
47
|
+
? `${leftPageIndex + 1} - ${rightPageIndex + 1} of ${totalApproximatePages}`
|
|
48
|
+
: `${leftPageIndex + 1} of ${totalApproximatePages}`}
|
|
49
|
+
</Text>
|
|
50
|
+
{!!pagination?.hasChapters && (
|
|
51
|
+
<>
|
|
52
|
+
<Text>-</Text>
|
|
53
|
+
<Text fontSize="xs">
|
|
54
|
+
({(pagination?.beginAbsolutePageIndex ?? 0) + 1})
|
|
55
|
+
</Text>
|
|
56
|
+
</>
|
|
57
|
+
)}
|
|
58
|
+
</HStack>
|
|
59
|
+
)}
|
|
60
|
+
</Stack>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Presence, type PresenceProps } from "@chakra-ui/react"
|
|
2
|
+
import { memo } from "react"
|
|
3
|
+
|
|
4
|
+
export const QuickBar = memo(
|
|
5
|
+
({
|
|
6
|
+
children,
|
|
7
|
+
position,
|
|
8
|
+
...rest
|
|
9
|
+
}: { position: "top" | "bottom" } & PresenceProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<Presence
|
|
12
|
+
display="flex"
|
|
13
|
+
flexDirection="row"
|
|
14
|
+
width="100%"
|
|
15
|
+
position="absolute"
|
|
16
|
+
{...(position === "bottom" ? { bottom: 0 } : { top: 0 })}
|
|
17
|
+
animationName={
|
|
18
|
+
position === "bottom"
|
|
19
|
+
? {
|
|
20
|
+
_open: "slide-from-bottom, fade-in",
|
|
21
|
+
_closed: "slide-to-bottom, fade-out",
|
|
22
|
+
}
|
|
23
|
+
: {
|
|
24
|
+
_open: "slide-from-top, fade-in",
|
|
25
|
+
_closed: "slide-to-top, fade-out",
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
animationDuration="moderate"
|
|
29
|
+
bgColor="bg.panel"
|
|
30
|
+
alignItems="center"
|
|
31
|
+
justifyContent="center"
|
|
32
|
+
shadow="md"
|
|
33
|
+
px={4}
|
|
34
|
+
{...rest}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</Presence>
|
|
38
|
+
)
|
|
39
|
+
},
|
|
40
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { memo } from "react"
|
|
2
|
+
import { BottomBar } from "./BottomBar"
|
|
3
|
+
import { TopBar } from "./TopBar"
|
|
4
|
+
|
|
5
|
+
export const QuickMenu = memo(
|
|
6
|
+
({
|
|
7
|
+
open,
|
|
8
|
+
onBackClick,
|
|
9
|
+
onMoreClick,
|
|
10
|
+
}: { open: boolean; onBackClick: () => void; onMoreClick: () => void }) => {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<TopBar
|
|
14
|
+
open={open}
|
|
15
|
+
onBackClick={onBackClick}
|
|
16
|
+
onMoreClick={onMoreClick}
|
|
17
|
+
/>
|
|
18
|
+
<BottomBar open={open} />
|
|
19
|
+
</>
|
|
20
|
+
)
|
|
21
|
+
},
|
|
22
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import RcSlider from "rc-slider"
|
|
2
|
+
import { type ComponentProps, useCallback, useEffect } from "react"
|
|
3
|
+
import { useObserve, useSignal, useSubscribe } from "reactjrx"
|
|
4
|
+
import { useReader } from "../../context/useReader"
|
|
5
|
+
import { usePagination } from "../../pagination/usePagination"
|
|
6
|
+
import "rc-slider/assets/index.css"
|
|
7
|
+
import { useNavigationContext } from "../useNavigationContext"
|
|
8
|
+
|
|
9
|
+
const useSliderValues = () => {
|
|
10
|
+
const pagination = usePagination()
|
|
11
|
+
const isUsingSpread = pagination?.isUsingSpread
|
|
12
|
+
const { beginPageIndex: currentRealPage, totalApproximatePages = 0 } =
|
|
13
|
+
useNavigationContext()
|
|
14
|
+
const currentPage = isUsingSpread
|
|
15
|
+
? Math.floor((currentRealPage || 0) / 2)
|
|
16
|
+
: currentRealPage
|
|
17
|
+
const [value, valueSignal] = useSignal({
|
|
18
|
+
default: currentPage || 0,
|
|
19
|
+
})
|
|
20
|
+
const min = 0
|
|
21
|
+
const max = Math.max(
|
|
22
|
+
0,
|
|
23
|
+
isUsingSpread
|
|
24
|
+
? Math.floor((totalApproximatePages - 1) / 2)
|
|
25
|
+
: totalApproximatePages - 1,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
valueSignal.setValue(currentPage || 0)
|
|
30
|
+
}, [currentPage, valueSignal])
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
value,
|
|
34
|
+
valueSignal,
|
|
35
|
+
min,
|
|
36
|
+
max,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Scrubber = () => {
|
|
41
|
+
const reader = useReader()
|
|
42
|
+
const pagination = usePagination()
|
|
43
|
+
const { manifest } = useObserve(() => reader?.context.state$, []) ?? {}
|
|
44
|
+
const reverse = manifest?.readingDirection === "rtl"
|
|
45
|
+
const isUsingSpread = pagination?.isUsingSpread
|
|
46
|
+
const { totalApproximatePages = 0, isBeginWithinChapter } =
|
|
47
|
+
useNavigationContext()
|
|
48
|
+
const step = 1
|
|
49
|
+
const isScrubberWithinChapter = isBeginWithinChapter
|
|
50
|
+
const { value, valueSignal, min, max } = useSliderValues()
|
|
51
|
+
|
|
52
|
+
const onChange: NonNullable<ComponentProps<typeof RcSlider>["onChange"]> =
|
|
53
|
+
useCallback(
|
|
54
|
+
(values) => {
|
|
55
|
+
const [value = 0] = Array.isArray(values) ? values : [values]
|
|
56
|
+
|
|
57
|
+
valueSignal.setValue(value)
|
|
58
|
+
|
|
59
|
+
const pageIndex = isUsingSpread
|
|
60
|
+
? Math.floor(value) * 2
|
|
61
|
+
: Math.floor(value)
|
|
62
|
+
|
|
63
|
+
if (!isScrubberWithinChapter) {
|
|
64
|
+
reader?.navigation.goToAbsolutePageIndex({
|
|
65
|
+
absolutePageIndex: pageIndex,
|
|
66
|
+
animation: false,
|
|
67
|
+
})
|
|
68
|
+
} else {
|
|
69
|
+
reader?.navigation.goToPageOfSpineItem({
|
|
70
|
+
pageIndex,
|
|
71
|
+
spineItemId: reader.pagination.getState().beginSpineItemIndex ?? 0,
|
|
72
|
+
animation: false,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[reader, isUsingSpread, valueSignal, isScrubberWithinChapter],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @note
|
|
81
|
+
* Scrubber can navigate fast and without lock we may end up with
|
|
82
|
+
* slowness due to the reader
|
|
83
|
+
* paginating and loading items in between.
|
|
84
|
+
* This is good practice (but not required) to throttle it.
|
|
85
|
+
*/
|
|
86
|
+
useSubscribe(
|
|
87
|
+
() =>
|
|
88
|
+
reader?.navigation.throttleLock({
|
|
89
|
+
duration: 100,
|
|
90
|
+
trigger: valueSignal.subject,
|
|
91
|
+
}),
|
|
92
|
+
[reader, valueSignal],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// const marks =
|
|
96
|
+
// max > 1
|
|
97
|
+
// ? Array.from({ length: max + 1 }, (_, i) => i).reduce(
|
|
98
|
+
// (acc: number[], val) => [...acc, val],
|
|
99
|
+
// [],
|
|
100
|
+
// )
|
|
101
|
+
// : []
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
totalApproximatePages === 1 ||
|
|
105
|
+
(isUsingSpread && totalApproximatePages === 2)
|
|
106
|
+
) {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// @tmp not available yet in chakra
|
|
111
|
+
// if (reverse) return null
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<RcSlider
|
|
115
|
+
value={[value]}
|
|
116
|
+
max={max}
|
|
117
|
+
min={min}
|
|
118
|
+
reverse={reverse}
|
|
119
|
+
step={step}
|
|
120
|
+
onChange={onChange}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
// return (
|
|
124
|
+
// <Slider
|
|
125
|
+
// value={[value]}
|
|
126
|
+
// max={max}
|
|
127
|
+
// min={min}
|
|
128
|
+
// marks={marks}
|
|
129
|
+
// onChange={e => {
|
|
130
|
+
// debugger
|
|
131
|
+
// }}
|
|
132
|
+
// onValueChange={onChange}
|
|
133
|
+
// // reverse={reverse}
|
|
134
|
+
// orientation="horizontal"
|
|
135
|
+
// step={step}
|
|
136
|
+
// />
|
|
137
|
+
// )
|
|
138
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Text, type TextProps } from "@chakra-ui/react"
|
|
2
|
+
import { useEffect, useState } from "react"
|
|
3
|
+
|
|
4
|
+
export const useTime = () => {
|
|
5
|
+
const [time, setTime] = useState(new Date())
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const interval = setInterval(() => {
|
|
9
|
+
setTime(new Date())
|
|
10
|
+
}, 1000 * 60)
|
|
11
|
+
|
|
12
|
+
return () => clearInterval(interval)
|
|
13
|
+
}, [])
|
|
14
|
+
|
|
15
|
+
return time
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const TimeIndicator = (props: TextProps) => {
|
|
19
|
+
const time = useTime()
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Text fontSize="xs" {...props}>
|
|
23
|
+
{time.toLocaleTimeString(navigator.language, {
|
|
24
|
+
hour: "2-digit",
|
|
25
|
+
minute: "2-digit",
|
|
26
|
+
})}
|
|
27
|
+
</Text>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { HStack, IconButton, Stack, Text } from "@chakra-ui/react"
|
|
2
|
+
import { IoIosArrowBack, IoMdMore } from "react-icons/io"
|
|
3
|
+
import { MdFullscreen, MdFullscreenExit } from "react-icons/md"
|
|
4
|
+
import { useObserve } from "reactjrx"
|
|
5
|
+
import { useFullscreen } from "../../common/useFullscreen"
|
|
6
|
+
import { useReader } from "../../context/useReader"
|
|
7
|
+
import { QuickBar } from "./QuickBar"
|
|
8
|
+
|
|
9
|
+
export const TopBar = ({
|
|
10
|
+
open,
|
|
11
|
+
onBackClick,
|
|
12
|
+
onMoreClick,
|
|
13
|
+
}: {
|
|
14
|
+
open: boolean
|
|
15
|
+
onBackClick: () => void
|
|
16
|
+
onMoreClick: () => void
|
|
17
|
+
}) => {
|
|
18
|
+
const reader = useReader()
|
|
19
|
+
const manifest = useObserve(() => reader?.context.manifest$, [reader])
|
|
20
|
+
const { isFullscreen, onToggleFullscreenClick } = useFullscreen()
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<QuickBar
|
|
24
|
+
present={open}
|
|
25
|
+
position="top"
|
|
26
|
+
height="80px"
|
|
27
|
+
justifyContent="space-between"
|
|
28
|
+
>
|
|
29
|
+
<IconButton
|
|
30
|
+
aria-label="left"
|
|
31
|
+
size="lg"
|
|
32
|
+
variant="ghost"
|
|
33
|
+
flexShrink={0}
|
|
34
|
+
onClick={onBackClick}
|
|
35
|
+
>
|
|
36
|
+
<IoIosArrowBack />
|
|
37
|
+
</IconButton>
|
|
38
|
+
<Stack
|
|
39
|
+
flex={1}
|
|
40
|
+
maxW={600}
|
|
41
|
+
gap={1}
|
|
42
|
+
alignItems="center"
|
|
43
|
+
overflow="auto"
|
|
44
|
+
px={4}
|
|
45
|
+
>
|
|
46
|
+
<Text truncate maxWidth="100%">
|
|
47
|
+
{manifest?.title}
|
|
48
|
+
</Text>
|
|
49
|
+
</Stack>
|
|
50
|
+
<HStack>
|
|
51
|
+
<IconButton
|
|
52
|
+
aria-label="right"
|
|
53
|
+
size="lg"
|
|
54
|
+
flexShrink={0}
|
|
55
|
+
variant="ghost"
|
|
56
|
+
onClick={onMoreClick}
|
|
57
|
+
>
|
|
58
|
+
<IoMdMore />
|
|
59
|
+
</IconButton>
|
|
60
|
+
<IconButton
|
|
61
|
+
aria-label="right"
|
|
62
|
+
size="lg"
|
|
63
|
+
flexShrink={0}
|
|
64
|
+
variant="ghost"
|
|
65
|
+
onClick={onToggleFullscreenClick}
|
|
66
|
+
>
|
|
67
|
+
{isFullscreen ? <MdFullscreenExit /> : <MdFullscreen />}
|
|
68
|
+
</IconButton>
|
|
69
|
+
</HStack>
|
|
70
|
+
</QuickBar>
|
|
71
|
+
)
|
|
72
|
+
}
|