diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index 39602a48..448f53a7 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -8,7 +8,7 @@ import { Languages } from '@utils/constants/languages'; import { load } from 'cheerio'; import dayjs from 'dayjs'; import { NovelStatus, Plugin, PluginItem } from './types'; -import { FilterInputs } from './types/filterTypes'; +import { FilterTypes } from './types/filterTypes'; import { parseMadaraDate } from './helpers/parseDate'; import { isUrlAbsolute } from '@utils/isAbsoluteUrl'; import { fetchApi, fetchFile } from '@utils/fetch/fetch'; @@ -21,7 +21,7 @@ const packages: Record = { '@libs/fetch': { fetchApi, fetchFile }, '@libs/parseMadaraDate': { parseMadaraDate }, '@libs/isAbsoluteUrl': { isUrlAbsolute }, - '@libs/filterInputs': { FilterInputs }, + '@libs/filterInputs': { FilterTypes }, '@libs/showToast': { showToast }, '@libs/defaultCover': { defaultCover }, }; @@ -69,7 +69,7 @@ let plugins: Record = {}; // get existing plugin in device const setupPlugin = async (path: string) => { const rawCode = await RNFS.readFile(path, 'utf8'); - const plugin = await initPlugin(rawCode, path); + const plugin = initPlugin(rawCode, path); if (!plugin) { showToast(`Invalid script in: ${path}`); } @@ -78,7 +78,9 @@ const setupPlugin = async (path: string) => { const installPlugin = async (url: string): Promise => { try { - return await fetch(url) + return await fetch(url, { + headers: { 'pragma': 'no-cache', 'cache-control': 'no-cache' }, + }) .then(res => res.text()) .then(async rawCode => { const plugin = initPlugin(rawCode); @@ -143,9 +145,9 @@ const collectPlugins = async () => { const fetchPlugins = async () => { // plugins host - const githubUsername = 'LNReader'; - const githubRepository = 'lnreader-sources'; - const githubBranch = 'plugins'; + const githubUsername = 'skillgg'; + const githubRepository = 'lnplugins'; + const githubBranch = 'filters_redesign'; const availablePlugins: Record> = await fetch( `https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/${githubBranch}/.dist/${githubUsername}/plugins.min.json`, diff --git a/src/plugins/types/filterTypes.ts b/src/plugins/types/filterTypes.ts index 48bc648a..42e13d5c 100644 --- a/src/plugins/types/filterTypes.ts +++ b/src/plugins/types/filterTypes.ts @@ -1,19 +1,121 @@ -interface FilterValue { - label: string; +export interface FilterOption { + readonly label: string; + readonly value: string; +} +export enum FilterTypes { + TextInput = 'Text', + Picker = 'Picker', + CheckboxGroup = 'Checkbox', + Switch = 'Switch', + ExcludableCheckboxGroup = 'XCheckbox', +} + +type SwitchFilter = { + type: FilterTypes.Switch; + /** Default value */ + value: boolean; +}; + +type TextFilter = { + type: FilterTypes.TextInput; + /** Default value */ value: string; -} +}; -export enum FilterInputs { - TextInput, - Picker, - Checkbox, -} +type CheckboxFilter = { + type: FilterTypes.CheckboxGroup; + options: readonly FilterOption[]; + /** Default value */ + value: string[]; +}; +type PickerFilter = { + type: FilterTypes.Picker; + options: readonly FilterOption[]; + /** Default value */ + value: string; +}; -export interface SourceFilter { - key: string; - label: string; - values: FilterValue[]; - inputType: FilterInputs; -} +type ExcludableCheckboxFilter = { + type: FilterTypes.ExcludableCheckboxGroup; + options: readonly FilterOption[]; + value: { include?: string[]; exclude?: string[] }; +}; -export type SelectedFilter = Record; +export type Filters = Record< + string, + | { label: string } & ( + | PickerFilter + | CheckboxFilter + | TextFilter + | SwitchFilter + | ExcludableCheckboxFilter + ) +>; + +export type FilterToValues< + T extends Record | undefined, +> = T extends undefined + ? undefined + : { + [K in keyof T]: Omit< + { type: NonNullable[K]['type'] } & Filters[string], + 'label' | 'options' + >; + }; + +export type ValueOfFilter = + T extends FilterTypes.CheckboxGroup + ? CheckboxFilter['value'] + : T extends FilterTypes.Picker + ? PickerFilter['value'] + : T extends FilterTypes.Switch + ? SwitchFilter['value'] + : T extends FilterTypes.TextInput + ? TextFilter['value'] + : T extends FilterTypes.ExcludableCheckboxGroup + ? ExcludableCheckboxFilter['value'] + : never; + +export const isPickerValue = ( + q: FilterToValues[string], +): q is FilterToValues<{ + [key: string]: { label: string } & PickerFilter; +}>[string] => { + return q.type === FilterTypes.Picker && typeof q.value === 'string'; +}; + +export const isCheckboxValue = ( + q: FilterToValues[string], +): q is FilterToValues<{ + [key: string]: { label: string } & CheckboxFilter; +}>[string] => { + return q.type === FilterTypes.CheckboxGroup && Array.isArray(q.value); +}; + +export const isSwitchValue = ( + q: FilterToValues[string], +): q is FilterToValues<{ + [key: string]: { label: string } & SwitchFilter; +}>[string] => { + return q.type === FilterTypes.Switch && typeof q.value === 'boolean'; +}; + +export const isTextValue = ( + q: FilterToValues[string], +): q is FilterToValues<{ + [key: string]: { label: string } & TextFilter; +}>[string] => { + return q.type === FilterTypes.TextInput && typeof q.value === 'string'; +}; + +export const isXCheckboxValue = ( + q: FilterToValues[string], +): q is FilterToValues<{ + [key: string]: { label: string } & ExcludableCheckboxFilter; +}>[string] => { + return ( + q.type === FilterTypes.ExcludableCheckboxGroup && + typeof q.value === 'object' && + !Array.isArray(q.value) + ); +}; diff --git a/src/plugins/types/index.ts b/src/plugins/types/index.ts index ca841be5..c0edc173 100644 --- a/src/plugins/types/index.ts +++ b/src/plugins/types/index.ts @@ -1,4 +1,4 @@ -import { SelectedFilter, SourceFilter } from './filterTypes'; +import { FilterToValues, Filters } from './filterTypes'; import { Languages } from '@utils/constants/languages'; export interface NovelItem { @@ -35,9 +35,9 @@ export interface SourceNovel { chapters?: ChapterItem[]; } -export interface SourceOptions { +export interface PopularNovelsOptions { showLatestNovels?: boolean; - filters?: SelectedFilter; + filters?: FilterToValues; } // this is for display in available plugins @@ -53,13 +53,13 @@ export interface PluginItem { export interface Plugin extends PluginItem { path: string; // path in device - filters?: SourceFilter[]; + filters?: Filters; userAgent: string; cookieString: string; rawCode: string; popularNovels: ( pageNo: number, - options?: SourceOptions, + options?: PopularNovelsOptions, ) => Promise; parseNovelAndChapters: ( novelUrl: string, diff --git a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx index 4a6a926a..26acbc86 100644 --- a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx +++ b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx @@ -156,7 +156,7 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { /> )} - {!showLatestNovels && filterValues?.length ? ( + {!showLatestNovels && filterValues ? ( <> { /> diff --git a/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx b/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx index b63aad63..d6515939 100644 --- a/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx +++ b/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx @@ -6,39 +6,44 @@ import { BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet'; import { useTheme } from '@hooks/useTheme'; import { - FilterInputs, - SelectedFilter, - SourceFilter, + FilterTypes, + FilterToValues, + Filters, } from '@plugins/types/filterTypes'; import { FlatList } from 'react-native-gesture-handler'; import { Button, IconButtonV2 } from '@components/index'; import { Checkbox } from '@components/Checkbox/Checkbox'; -import { defaultTo } from 'lodash-es'; import { Picker } from '@react-native-picker/picker'; import useBoolean from '@hooks/useBoolean'; import { overlay } from 'react-native-paper'; +import { getValueFor } from './filterUtils'; const insertOrRemoveIntoArray = (array: string[], val: string): string[] => array.indexOf(val) > -1 ? array.filter(ele => ele !== val) : [...array, val]; +type SelectedFilters = FilterToValues; + interface FilterItemProps { - filter: SourceFilter; - selectedFilters: SelectedFilter | undefined; - setSelectedFilters: React.Dispatch< - React.SetStateAction - >; + filter: Filters[string]; + filterKey: keyof Filters; + selectedFilters: SelectedFilters; + setSelectedFilters: React.Dispatch>; } const FilterItem: React.FC = ({ filter, + filterKey, selectedFilters, setSelectedFilters, }) => { const theme = useTheme(); const { value: isVisible, toggle: toggleCard } = useBoolean(); - - if (filter.inputType === FilterInputs.Picker) { + if (filter.type === FilterTypes.Picker) { + const value = getValueFor<(typeof filter)['type']>( + filter, + selectedFilters[filterKey], + ); return ( @@ -47,17 +52,17 @@ const FilterItem: React.FC = ({ setSelectedFilters(prevFilters => ({ ...prevFilters, - [filter.key]: itemValue, + [filterKey]: { value: itemValue, type: FilterTypes.Picker }, })) } placeholder={filter.label} > - {filter.values.map(val => { + {filter.options.map(val => { return ( = ({ ); } - if (filter.inputType === FilterInputs.Checkbox) { - const isChecked = (val: string) => - (selectedFilters?.[filter.key] as string[])?.includes(val); - + if (filter.type === FilterTypes.CheckboxGroup) { + const value = getValueFor<(typeof filter)['type']>( + filter, + selectedFilters[filterKey], + ); return ( = ({ /> {isVisible - ? filter.values.map(val => { + ? filter.options.map(val => { return ( setSelectedFilters(prevFilters => ({ ...prevFilters, - [filter.key]: insertOrRemoveIntoArray( - defaultTo(prevFilters?.[filter.key] as string[], []), - val.value, - ), + [filterKey]: { + type: FilterTypes.CheckboxGroup, + value: insertOrRemoveIntoArray(value, val.value), + }, })) } /> @@ -115,26 +122,119 @@ const FilterItem: React.FC = ({ ); } + if (filter.type === FilterTypes.ExcludableCheckboxGroup) { + const value = getValueFor<(typeof filter)['type']>( + filter, + selectedFilters[filterKey], + ); + return ( + + + + {filter.label} + + + + {isVisible + ? filter.options.map(val => { + return ( + { + if (value.exclude?.includes(val.value)) { + setSelectedFilters(prev => { + return { + ...prev, + [filterKey]: { + type: FilterTypes.ExcludableCheckboxGroup, + value: { + include: [...(value.include || [])], + exclude: [ + ...(value.exclude?.filter( + f => f !== val.value, + ) || []), + ], + }, + }, + }; + }); + } else if (value.include?.includes(val.value)) { + setSelectedFilters(prev => { + return { + ...prev, + [filterKey]: { + type: FilterTypes.ExcludableCheckboxGroup, + value: { + include: [ + ...(value.include?.filter( + f => f !== val.value, + ) || []), + ], + exclude: [...(value.exclude || []), val.value], + }, + }, + }; + }); + } else { + setSelectedFilters(prev => { + return { + ...prev, + [filterKey]: { + type: FilterTypes.ExcludableCheckboxGroup, + value: { + include: [...(value.include || []), val.value], + exclude: value.exclude, + }, + }, + }; + }); + } + }} + /> + ); + }) + : null} + + ); + } + return <>; }; interface BottomSheetProps { filterSheetRef: React.RefObject | null; - filtersValues: SourceFilter[] | undefined; - setFilters: (filters?: SelectedFilter) => void; - clearFilters: () => void; + filters: Filters; + setFilters: (filters?: SelectedFilters) => void; + clearFilters: (filters: Filters) => void; } const FilterBottomSheet: React.FC = ({ + filters, filterSheetRef, - filtersValues, clearFilters, setFilters, }) => { const theme = useTheme(); - const [selectedFilters, setSelectedFilters] = useState< - SelectedFilter | undefined - >(); + + const [selectedFilters, setSelectedFilters] = + useState(filters); return ( @@ -150,8 +250,8 @@ const FilterBottomSheet: React.FC = ({