Skip to content

Commit 0130c3b

Browse files
authored
feat(epub-view): epub viewer with persisted location (#1164)
Co-authored-by: Andrejs <Leitans>
1 parent 6bd4bf1 commit 0130c3b

File tree

8 files changed

+215
-17
lines changed

8 files changed

+215
-17
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@
234234
"react-localization": "^1.0.15",
235235
"react-markdown": "^7.1.1",
236236
"react-number-format": "^5.1.2",
237+
"react-reader": "^2.0.10",
237238
"react-redux": "^8.0.5",
238239
"react-router-dom": "^6.9.0",
239240
"react-textarea-autosize": "^8.2.0",

src/components/ContentItem/contentItem.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// TODO: refactor needed
2-
import React, { useEffect, useState } from 'react';
2+
import { useEffect, useState } from 'react';
33
import { Link } from 'react-router-dom';
4-
import { $TsFixMe } from 'src/types/tsfix';
4+
import { LinksType } from 'src/containers/Search/types';
55
import useQueueIpfsContent from 'src/hooks/useQueueIpfsContent';
66
import type {
7-
IpfsContentType,
87
IPFSContentDetails,
8+
IpfsContentType,
99
} from 'src/services/ipfs/types';
10-
import { LinksType } from 'src/containers/Search/types';
10+
import { $TsFixMe } from 'src/types/tsfix';
1111

1212
import { parseArrayLikeToDetails } from 'src/services/ipfs/utils/content';
1313

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, {
2+
CSSProperties,
3+
ComponentProps,
4+
useCallback,
5+
useMemo,
6+
} from 'react';
7+
import { ReactReader } from 'react-reader';
8+
import useEPubLocation from 'src/hooks/useEPubLocation';
9+
10+
interface IProps {
11+
url: string;
12+
search?: boolean;
13+
style?: CSSProperties;
14+
}
15+
16+
type EpubInitOptions = ComponentProps<typeof ReactReader>['epubInitOptions'];
17+
const epubInitOptions: EpubInitOptions = { openAs: 'epub' };
18+
19+
function EPubView({ url, search, style }: IProps) {
20+
const [location, setLocation] = useEPubLocation(url);
21+
const currentLocation = location && !search ? location : 0;
22+
23+
const currentStyle = useMemo(
24+
() => ({ height: search ? '300px' : '60vh', ...style }),
25+
[style, search]
26+
);
27+
28+
const onLocationChange = useCallback(
29+
(loc: string) => {
30+
if (!search) {
31+
setLocation(loc);
32+
}
33+
},
34+
[search, setLocation]
35+
);
36+
37+
return (
38+
<div style={currentStyle}>
39+
<ReactReader
40+
url={url}
41+
location={currentLocation}
42+
locationChanged={onLocationChange}
43+
epubInitOptions={epubInitOptions}
44+
/>
45+
</div>
46+
);
47+
}
48+
49+
export default React.memo(EPubView);

src/components/contentIpfs/contentIpfs.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { IPFSContentDetails, IPFSContentMaybe } from 'src/services/ipfs/types';
21
import { CYBER_GATEWAY } from 'src/constants/config';
2+
import { CYBER_GATEWAY_URL } from 'src/services/ipfs/config';
3+
import { IPFSContentDetails, IPFSContentMaybe } from 'src/services/ipfs/types';
4+
import EPubView from '../EPubView/EPubView';
5+
import Pdf from '../PDF';
6+
import TextMarkdown from '../TextMarkdown';
37
import VideoPlayerGatewayOnly from '../VideoPlayer/VideoPlayerGatewayOnly';
8+
import Audio from './component/Audio/Audio';
49
import GatewayContent from './component/gateway';
5-
import TextMarkdown from '../TextMarkdown';
6-
import LinkHttp from './component/link';
7-
import Pdf from '../PDF';
810
import Img from './component/img';
9-
import Audio from './component/Audio/Audio';
11+
import LinkHttp from './component/link';
1012

1113
function OtherItem({
1214
content,
@@ -77,6 +79,12 @@ function ContentIpfs({ details, content, cid, search }: ContentTabProps) {
7779
{contentType === 'link' && (
7880
<LinkHttp url={details.content!} preview={search} />
7981
)}
82+
{contentType === 'epub' && (
83+
<EPubView
84+
url={`${CYBER_GATEWAY_URL}/ipfs/${cid}`}
85+
search={search}
86+
/>
87+
)}
8088
{contentType === 'other' && (
8189
<OtherItem search={search} cid={cid} content={details.content} />
8290
)}
@@ -85,4 +93,5 @@ function ContentIpfs({ details, content, cid, search }: ContentTabProps) {
8593
</div>
8694
);
8795
}
96+
8897
export default ContentIpfs;

src/hooks/useEPubLocation.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* eslint-disable import/prefer-default-export */
2+
import { useCallback } from 'react';
3+
import type { EpubView } from 'react-reader';
4+
5+
const epubKey = 'cyb:epub';
6+
7+
const getEPubMap = (): Record<string, EpubView['location']> => {
8+
try {
9+
const epubMapString = localStorage.getItem(epubKey) || '';
10+
const epubMap = JSON.parse(epubMapString);
11+
12+
return epubMap;
13+
} catch (error) {
14+
console.error('Failed to parse epub locations map:', error);
15+
16+
return {};
17+
}
18+
};
19+
20+
const getLocation = (url: string) => {
21+
const epubMap = getEPubMap();
22+
23+
return epubMap[url] ?? null;
24+
};
25+
26+
const getSetEPubLocation =
27+
(url: string) => (location: EpubView['location']) => {
28+
const epubMap = getEPubMap();
29+
30+
try {
31+
epubMap[url] = location;
32+
localStorage.setItem(epubKey, JSON.stringify(epubMap));
33+
} catch (error) {
34+
console.error('Failed to save EPub location:', error);
35+
}
36+
};
37+
38+
const useEPubLocation = (
39+
url: string
40+
): [EpubView['location'], (url: string) => void] => {
41+
const currentLocation = getLocation(url);
42+
const setEPubLocation = useCallback(getSetEPubLocation(url), [url]);
43+
44+
return [currentLocation, setEPubLocation];
45+
};
46+
47+
export default useEPubLocation;

src/services/ipfs/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type IpfsContentType =
9191
| 'video'
9292
| 'audio'
9393
| 'html'
94+
| 'epub'
9495
| 'other';
9596

9697
export type IPFSContentDetails =

src/services/ipfs/utils/content.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,18 @@ export const detectContentType = (
3131
if (mime.includes('audio')) {
3232
return 'audio';
3333
}
34+
35+
if (mime.includes('epub')) {
36+
return 'epub';
37+
}
3438
}
39+
3540
return 'other';
3641
};
3742

3843
const basic = /\s?<!doctype html>|(<html\b[^>]*>|<body\b[^>]*>|<x-[^>]+>)+/i;
3944

40-
function isHtml(string) {
45+
function isHtml(string: string) {
4146
const newString = string.trim().slice(0, 1000);
4247
return basic.test(newString);
4348
}
@@ -55,30 +60,25 @@ export const chunksToBlob = (
5560
// eslint-disable-next-line import/no-unused-modules, import/prefer-default-export
5661
export const parseArrayLikeToDetails = async (
5762
content: IPFSContentMaybe,
58-
// rawDataResponse: Uint8ArrayLike | undefined,
59-
// mime: string | undefined,
6063
cid: string,
6164
onProgress?: onProgressCallback
6265
): Promise<IPFSContentDetails> => {
6366
try {
64-
// console.log('------parseArrayLikeToDetails', cid, content);
6567
const mime = content?.meta?.mime;
6668
const response: IPFSContentDetails = {
6769
link: `/ipfs/${cid}`,
6870
gateway: false,
6971
cid,
7072
};
7173
const initialType = detectContentType(mime);
72-
if (['video', 'audio'].indexOf(initialType) > -1) {
74+
if (['video', 'audio', 'epub'].indexOf(initialType) > -1) {
7375
return { ...response, type: initialType, gateway: true };
7476
}
7577

7678
const rawData = content?.result
7779
? await getResponseResult(content.result, onProgress)
7880
: undefined;
7981

80-
// console.log(rawData);
81-
8282
if (!mime) {
8383
response.text = `Can't detect MIME for ${cid.toString()}`;
8484
response.gateway = true; // ???

0 commit comments

Comments
 (0)