diff --git a/package-lock.json b/package-lock.json index 9bafc5ad..9fdeb24b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,8 @@ "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^9.0.0", "react-native-webview": "11.26.0", - "sanitize-html": "^2.7.0" + "sanitize-html": "^2.7.0", + "urlencode": "^2.0.0" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -16603,6 +16604,25 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, + "node_modules/urlencode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/urlencode/-/urlencode-2.0.0.tgz", + "integrity": "sha512-K4+koEq4II9FqKKdLyMwfVFiWvTLJsdsIihXCprumjlOwpviO44E4hAhLYBLb6CEVTZh9hXXMTQHIT+Hwv5BPw==", + "dependencies": { + "iconv-lite": "~0.6.3" + } + }, + "node_modules/urlencode/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/use-latest-callback": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.1.9.tgz", diff --git a/package.json b/package.json index ba72c324..b0e21c41 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,8 @@ "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^9.0.0", "react-native-webview": "11.26.0", - "sanitize-html": "^2.7.0" + "sanitize-html": "^2.7.0", + "urlencode": "^2.0.0" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/src/plugins/helpers/fetch.ts b/src/plugins/helpers/fetch.ts new file mode 100644 index 00000000..f87b6fc9 --- /dev/null +++ b/src/plugins/helpers/fetch.ts @@ -0,0 +1,107 @@ +import { getUserAgent } from '@hooks/persisted/useUserAgent'; + +type FetchInit = { + headers?: Record | Headers; + method?: string; + body?: FormData | string; + [x: string]: string | Record | undefined | FormData | Headers; +}; + +const makeInit = (init?: FetchInit) => { + const defaultHeaders = { + 'User-Agent': getUserAgent(), + }; + if (init?.headers) { + if (init.headers instanceof Headers) { + if (!init.headers.get('User-Agent') && defaultHeaders['User-Agent']) { + init.headers.set('User-Agent', defaultHeaders['User-Agent']); + } + } else { + init.headers = { + ...defaultHeaders, + ...init.headers, + }; + } + } else { + init = { + ...init, + headers: defaultHeaders, + }; + } + return init; +}; + +export const fetchApi = async ( + url: string, + init?: FetchInit, +): Promise => { + init = makeInit(init); + return await fetch(url, init); +}; + +const FILE_READER_PREFIX_LENGTH = 'data:application/octet-stream;base64,' + .length; + +/** + * + * @param url + * @param init + * @returns base64 of file + */ +export const fetchFile = async ( + url: string, + init?: FetchInit, +): Promise => { + init = makeInit(init); + try { + const res = await fetch(url, init); + if (!res.ok) { + throw new Error(); + } + const blob = await res.blob(); + return await new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onloadend = () => { + resolve(fr.result.slice(FILE_READER_PREFIX_LENGTH) as string); + }; + fr.onerror = () => reject(); + fr.onabort = () => reject(); + fr.readAsDataURL(blob); + }); + } catch (e) { + return ''; + } +}; + +/** + * + * @param url + * @param init + * @param encoding link: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/encoding + * @returns plain text + */ +export const fetchText = async ( + url: string, + init?: FetchInit, + encoding?: string, +): Promise => { + init = makeInit(init); + try { + const res = await fetch(url, init); + if (!res.ok) { + throw new Error(); + } + const blob = await res.blob(); + return await new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onloadend = () => { + resolve(fr.result as string); + }; + fr.onerror = () => reject(); + fr.onabort = () => reject(); + fr.readAsText(blob, encoding); + }); + } catch (e) { + return ''; + } +}; diff --git a/src/utils/isAbsoluteUrl.ts b/src/plugins/helpers/isAbsoluteUrl.ts similarity index 100% rename from src/utils/isAbsoluteUrl.ts rename to src/plugins/helpers/isAbsoluteUrl.ts diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index 76496f72..0d5a4362 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -10,15 +10,17 @@ import dayjs from 'dayjs'; import { NovelStatus, Plugin, PluginItem } from './types'; import { FilterTypes } from './types/filterTypes'; import { parseMadaraDate } from './helpers/parseDate'; -import { isUrlAbsolute } from '@utils/isAbsoluteUrl'; -import { fetchApi, fetchFile } from '@utils/fetch/fetch'; +import { isUrlAbsolute } from './helpers/isAbsoluteUrl'; +import { fetchApi, fetchFile, fetchText } from './helpers/fetch'; import { defaultCover } from './helpers/constants'; +import { encode, decode } from 'urlencode'; const packages: Record = { 'cheerio': { load }, 'dayjs': dayjs, + 'urlencode': { encode, decode }, '@libs/novelStatus': { NovelStatus }, - '@libs/fetch': { fetchApi, fetchFile }, + '@libs/fetch': { fetchApi, fetchFile, fetchText }, '@libs/parseMadaraDate': { parseMadaraDate }, '@libs/isAbsoluteUrl': { isUrlAbsolute }, '@libs/filterInputs': { FilterTypes }, diff --git a/src/screens/reader/components/ReaderFooter.tsx b/src/screens/reader/components/ReaderFooter.tsx index eea134f0..82df5631 100644 --- a/src/screens/reader/components/ReaderFooter.tsx +++ b/src/screens/reader/components/ReaderFooter.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Pressable, StyleSheet, View } from 'react-native'; import { IconButton } from 'react-native-paper'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { isUrlAbsolute } from '../../../utils/isAbsoluteUrl'; import * as WebBrowser from 'expo-web-browser'; import color from 'color'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; @@ -66,28 +65,24 @@ const ChapterFooter = ({ iconColor={theme.onSurface} /> - {isUrlAbsolute(chapterUrl) ? ( - WebBrowser.openBrowserAsync(chapterUrl)} - > - - - ) : null} - <> - scrollTo(0)} - > - - - + WebBrowser.openBrowserAsync(chapterUrl)} + > + + + scrollTo(0)} + > + + | Headers; - [x: string]: - | string - | Record - | undefined - | FormData - | Headers; - }, -): Promise => { - const defaultHeaders = { - 'User-Agent': getUserAgent(), - }; - if (init?.headers) { - if (init.headers instanceof Headers) { - if (!init.headers.get('User-Agent') && defaultHeaders['User-Agent']) { - init.headers.set('User-Agent', defaultHeaders['User-Agent']); - } - } else { - init.headers = { - ...defaultHeaders, - ...init.headers, - }; - } - } else { - init = { - ...init, - headers: defaultHeaders, - }; - } - return await fetch(url, init); -}; - -const FILE_READER_PREFIX_LENGTH = 'data:application/octet-stream;base64,' - .length; - -export const fetchFile = async (url: string, init: any): Promise => { - if (!init) { - init = {}; - } - try { - const res = await fetch(url, init); - if (!res.ok) { - throw new Error(); - } - const blob = await res.blob(); - return await new Promise((resolve, reject) => { - const fr = new FileReader(); - fr.onloadend = () => { - resolve(fr.result.slice(FILE_READER_PREFIX_LENGTH) as string); - }; - fr.onerror = () => reject(); - fr.onabort = () => reject(); - fr.readAsDataURL(blob); - }); - } catch (e) { - return ''; - } -}; - export const fetchTimeout = async ( url: string, init?: any,