diff --git a/package-lock.json b/package-lock.json index 2dbdcd45f..757808222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "expo-speech": "~11.3.0", "expo-sqlite": "~11.3.3", "expo-web-browser": "~12.3.2", + "htmlparser2": "^9.1.0", "i18n-js": "^3.8.0", "lodash-es": "^4.17.21", "protobufjs": "^7.2.6", @@ -6756,6 +6757,25 @@ "htmlparser2": "^8.0.0" } }, + "node_modules/@types/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -8273,6 +8293,24 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cheerio/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -11015,9 +11053,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -11028,8 +11066,8 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, "node_modules/http-errors": { @@ -16780,6 +16818,24 @@ "node": ">=0.10.0" } }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/sanitize-html/node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", diff --git a/package.json b/package.json index 7d4dbaff4..3372babc7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "expo-speech": "~11.3.0", "expo-sqlite": "~11.3.3", "expo-web-browser": "~12.3.2", + "htmlparser2": "^9.1.0", "i18n-js": "^3.8.0", "lodash-es": "^4.17.21", "protobufjs": "^7.2.6", diff --git a/src/hooks/persisted/usePlugins.ts b/src/hooks/persisted/usePlugins.ts index 0455db005..b0caf920f 100644 --- a/src/hooks/persisted/usePlugins.ts +++ b/src/hooks/persisted/usePlugins.ts @@ -1,5 +1,5 @@ import { locale } from 'expo-localization'; -import { Language, languagesMapping } from '@utils/constants/languages'; +import { languagesMapping } from '@utils/constants/languages'; import { orderBy } from 'lodash-es'; import { useMMKVObject } from 'react-native-mmkv'; import { PluginItem } from '@plugins/types'; @@ -23,17 +23,13 @@ export const FILTERED_INSTALLED_PLUGINS = 'FILTERED_INSTALLED_PLUGINS'; const defaultLang = languagesMapping[locale.split('-')[0]] || 'English'; -export type PluginsMap = Record; - export default function usePlugins() { const [lastUsedPlugin, setLastUsedPlugin] = useMMKVObject(LAST_USED_PLUGIN); const [languagesFilter = [defaultLang], setLanguagesFilter] = - useMMKVObject(LANGUAGES_FILTER); - const [ - filteredAvailablePlugins = {} as PluginsMap, - setFilteredAvailablePlugins, - ] = useMMKVObject(FILTERED_AVAILABLE_PLUGINS); + useMMKVObject(LANGUAGES_FILTER); + const [filteredAvailablePlugins = [], setFilteredAvailablePlugins] = + useMMKVObject(FILTERED_AVAILABLE_PLUGINS); const [filteredInstalledPlugins = [], setFilteredInstalledPlugins] = useMMKVObject(FILTERED_INSTALLED_PLUGINS); /** @@ -41,54 +37,51 @@ export default function usePlugins() { * We cant use the languagesFilter directly because it is updated only after component's lifecycle end. * And toggleLanguagFilter triggers filterPlugins before lifecycle end. */ - const filterPlugins = useCallback((filter: Language[]) => { + const filterPlugins = useCallback((filter: string[]) => { const installedPlugins = getMMKVObject(INSTALLED_PLUGINS) || []; const availablePlugins = - getMMKVObject(AVAILABLE_PLUGINS) || ({} as PluginsMap); + getMMKVObject(AVAILABLE_PLUGINS) || []; setFilteredInstalledPlugins( installedPlugins.filter(plg => filter.includes(plg.lang)), ); setFilteredAvailablePlugins( - filter.reduce((pre, cur) => { - pre[cur] = orderBy(availablePlugins[cur], 'name'); - return pre; - }, {} as PluginsMap), + orderBy( + availablePlugins.filter(plg => filter.includes(plg.lang)), + 'name', + ), ); }, []); const refreshPlugins = useCallback(() => { const installedPlugins = getMMKVObject(INSTALLED_PLUGINS) || []; - return fetchPlugins().then(fetchedPluginsMap => { - for (const key in fetchedPluginsMap) { - const lang = key as Language; - fetchedPluginsMap[lang] = fetchedPluginsMap[lang].filter(plg => { - const finded = installedPlugins.find(v => v.id === plg.id); - if (finded) { - if (newer(plg.version, finded.version)) { - finded.hasUpdate = true; - finded.iconUrl = plg.iconUrl; - finded.url = plg.url; - if (finded.id === lastUsedPlugin?.id) { - setLastUsedPlugin(finded); - } + return fetchPlugins().then(fetchedPlugins => { + fetchedPlugins.filter(plg => { + const finded = installedPlugins.find(v => v.id === plg.id); + if (finded) { + if (newer(plg.version, finded.version)) { + finded.hasUpdate = true; + finded.iconUrl = plg.iconUrl; + finded.url = plg.url; + if (finded.id === lastUsedPlugin?.id) { + setLastUsedPlugin(finded); } - return false; } - return true; - }); - setMMKVObject(INSTALLED_PLUGINS, installedPlugins); - setMMKVObject(AVAILABLE_PLUGINS, fetchedPluginsMap); - filterPlugins(languagesFilter); - } + return false; + } + return true; + }); + setMMKVObject(INSTALLED_PLUGINS, installedPlugins); + setMMKVObject(AVAILABLE_PLUGINS, fetchedPlugins); + filterPlugins(languagesFilter); }); }, [languagesFilter]); - const toggleLanguageFilter = (lang: Language) => { + const toggleLanguageFilter = (lang: string) => { const newFilter = languagesFilter.includes(lang) ? languagesFilter.filter(l => l !== lang) - : [...languagesFilter, lang]; + : [lang, ...languagesFilter]; setLanguagesFilter(newFilter); filterPlugins(newFilter); }; @@ -106,10 +99,7 @@ export default function usePlugins() { const installedPlugins = getMMKVObject(INSTALLED_PLUGINS) || []; const availablePlugins = - getMMKVObject(AVAILABLE_PLUGINS) || ({} as PluginsMap); - availablePlugins[plugin.lang] = availablePlugins[plugin.lang]?.filter( - plg => plg.id !== plugin.id, - ); + getMMKVObject(AVAILABLE_PLUGINS) || []; const actualPlugin: PluginItem = { ...plugin, @@ -119,7 +109,10 @@ export default function usePlugins() { if (!installedPlugins.some(plg => plg.id === plugin.id)) { setMMKVObject(INSTALLED_PLUGINS, [...installedPlugins, actualPlugin]); } - setMMKVObject(AVAILABLE_PLUGINS, availablePlugins); + setMMKVObject( + AVAILABLE_PLUGINS, + availablePlugins.filter(plg => plg.id !== plugin.id), + ); filterPlugins(languagesFilter); } else { throw new Error( @@ -136,14 +129,11 @@ export default function usePlugins() { const installedPlugins = getMMKVObject(INSTALLED_PLUGINS) || []; const availablePlugins = - getMMKVObject(AVAILABLE_PLUGINS) || ({} as PluginsMap); + getMMKVObject(AVAILABLE_PLUGINS) || []; // safe - if (!availablePlugins[plugin.lang]?.some(_plg => _plg.id === plugin.id)) { - availablePlugins[plugin.lang] = [ - ...(availablePlugins[plugin.lang] || []), - plugin, - ]; + if (!availablePlugins.some(_plg => _plg.id === plugin.id)) { + availablePlugins.push(plugin); setMMKVObject(AVAILABLE_PLUGINS, availablePlugins); } setMMKVObject( diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index 6542943eb..a0f9e905d 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -1,7 +1,6 @@ import RNFS from 'react-native-fs'; import { PluginDownloadFolder } from '@utils/constants/download'; import { newer } from '@utils/compareVersion'; -import { Language } from '@utils/constants/languages'; // packages for plugins import { load } from 'cheerio'; @@ -13,11 +12,13 @@ import { isUrlAbsolute } from './helpers/isAbsoluteUrl'; import { fetchApi, fetchFile, fetchProto, fetchText } from './helpers/fetch'; import { defaultCover } from './helpers/constants'; import { encode, decode } from 'urlencode'; +import { Parser } from 'htmlparser2'; import TextFile from '@native/TextFile'; const pluginsFilePath = PluginDownloadFolder + '/plugins.json'; const packages: Record = { + 'htmlparser2': { Parser }, 'cheerio': { load }, 'dayjs': dayjs, 'qs': qs, @@ -125,13 +126,13 @@ const updatePlugin = async (plugin: PluginItem) => { return installPlugin(plugin.url); }; -const fetchPlugins = (): Promise>> => { +const fetchPlugins = (): Promise => { // plugins host const githubUsername = 'LNReader'; const githubRepository = 'lnreader-sources'; - + const pluginsTag = 'v2.1.0'; return fetch( - `https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/dist/.dist/plugins.min.json`, + `https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/plugins/${pluginsTag}/.dist/plugins.min.json`, ).then(res => res.json()); }; diff --git a/src/plugins/types/index.ts b/src/plugins/types/index.ts index 3de17377d..624151b06 100644 --- a/src/plugins/types/index.ts +++ b/src/plugins/types/index.ts @@ -1,5 +1,4 @@ import { FilterToValues, Filters } from './filterTypes'; -import { Language } from '@utils/constants/languages'; export interface NovelItem { name: string; @@ -50,7 +49,7 @@ export interface PluginItem { id: string; name: string; site: string; - lang: Language; + lang: string; version: string; url: string; // the url of raw code iconUrl: string; diff --git a/src/screens/browse/BrowseSettings.tsx b/src/screens/browse/BrowseSettings.tsx index 9dc496c46..3e14b1dd6 100644 --- a/src/screens/browse/BrowseSettings.tsx +++ b/src/screens/browse/BrowseSettings.tsx @@ -4,7 +4,7 @@ import { Appbar, List, SwitchItem } from '@components'; import { useBrowseSettings, usePlugins, useTheme } from '@hooks/persisted'; import { getString } from '@strings/translations'; -import { availableLanguages, languages } from '@utils/constants/languages'; +import { languages } from '@utils/constants/languages'; import { BrowseSettingsScreenProp } from '@navigators/types'; const BrowseSettings = ({ navigation }: BrowseSettingsScreenProp) => { @@ -61,10 +61,10 @@ const BrowseSettings = ({ navigation }: BrowseSettingsScreenProp) => { } keyExtractor={item => item} - data={availableLanguages} + data={languages} renderItem={({ item }) => ( toggleLanguageFilter(item)} theme={theme} diff --git a/src/screens/browse/components/BrowseTabs.tsx b/src/screens/browse/components/BrowseTabs.tsx index d52852593..7c1c93325 100644 --- a/src/screens/browse/components/BrowseTabs.tsx +++ b/src/screens/browse/components/BrowseTabs.tsx @@ -19,7 +19,6 @@ import { import { coverPlaceholderColor } from '@theme/colors'; import { ThemeColors } from '@theme/types'; import { Swipeable } from 'react-native-gesture-handler'; -import { languages } from '@utils/constants/languages'; import { getString } from '@strings/translations'; import { BrowseScreenProps } from '@navigators/types'; import { Button, IconButtonV2 } from '@components'; @@ -30,6 +29,7 @@ import Animated, { useSharedValue, withTiming, } from 'react-native-reanimated'; +import { groupBy } from 'lodash-es'; interface AvailableTabProps { searchText: string; @@ -168,7 +168,7 @@ export const InstalledTab = memo( numberOfLines={1} style={[{ color: theme.onSurfaceVariant }, styles.addition]} > - {`${languages[item.lang]} - ${item.version}`} + {`${item.lang} - ${item.version}`} @@ -297,7 +297,7 @@ const AvailablePluginCard = ({ textStyles, ]} > - {`${languages[plugin.lang]} - ${plugin.version}`} + {`${plugin.lang} - ${plugin.version}`} @@ -337,22 +337,20 @@ export const AvailableTab = memo(({ searchText, theme }: AvailableTabProps) => { const sections = useMemo(() => { const list = []; - for (const language of languagesFilter) { - if (filteredAvailablePlugins[language]) { - let plugins; - if (searchText) { - plugins = filteredAvailablePlugins[language]?.filter(plg => + const group = groupBy( + searchText + ? filteredAvailablePlugins.filter(plg => plg.name.toLocaleLowerCase().includes(searchText.toLowerCase()), - ); - } else { - plugins = filteredAvailablePlugins[language]; - } - if (plugins && plugins.length) { - list.push({ - header: language, - data: plugins, - }); - } + ) + : filteredAvailablePlugins, + 'lang', + ); + for (const language of languagesFilter) { + if (group[language]?.length) { + list.push({ + header: language, + data: group[language], + }); } } return list; diff --git a/src/utils/constants/languages.ts b/src/utils/constants/languages.ts index 36fcdd013..024b87783 100644 --- a/src/utils/constants/languages.ts +++ b/src/utils/constants/languages.ts @@ -2,43 +2,22 @@ // https://en.wikipedia.org/wiki/IETF_language_tag // https://en.wikipedia.org/wiki/List_of_language_names -export const languages = { - Arabic: 'العربية', - Chinese: '中文, 汉语, 漢語', - English: 'English', - French: 'Français', - Indonesian: 'Bahasa Indonesia', - Japanese: '日本語', - Korean: '조선말, 한국어', - Polish: 'Polski', - Portuguese: 'Português', - Russian: 'Русский', - Spanish: 'Español', - Thai: 'ไทย', - Turkish: 'Türkçe', - Ukrainian: 'Українська', - Vietnamese: 'Tiếng Việt', -} as const; - -export type Language = keyof typeof languages; -export type NativeLanguage = (typeof languages)[Language]; - -export const languagesMapping: Record = { - 'ab': 'Arabic', - 'zh': 'Chinese', +export const languagesMapping: Record = { + 'ab': 'العربية', + 'zh': '中文, 汉语, 漢語', 'en': 'English', - 'fr': 'French', - 'id': 'Indonesian', - 'ja': 'Japanese', - 'ko': 'Korean', - 'pl': 'Polish', - 'pt': 'Portuguese', - 'ru': 'Russian', - 'es': 'Spanish', - 'th': 'Thai', - 'tr': 'Turkish', - 'uk': 'Ukrainian', - 'vi': 'Vietnamese', + 'fr': 'Français', + 'id': 'Bahasa Indonesia', + 'ja': '日本語', + 'ko': '조선말, 한국어', + 'pl': 'Polski', + 'pt': 'Português', + 'ru': 'Русский', + 'es': 'Español', + 'th': 'ไทย', + 'tr': 'Türkçe', + 'uk': 'Українська', + 'vi': 'Tiếng Việt', }; -export const availableLanguages = Object.keys(languages) as Language[]; +export const languages = Object.values(languagesMapping);