Move to Zustand and add Filters to web_test (#1077)

* Moved to zustand and started refactored filters

- Move from redux to zustand
- Refactor Filters typings and add some helper types
- Add Filters button and Picker/Switch filter options
- Fixed Syosetu typings

* Add rule consistent-type-definitions

- Add consistent-type-definitions eslint rule to force consistent use of types instead of interfaces
- Run eslint to auto-change every interface to types

https://dev.to/atheodosiou/types-vs-interfaces-in-typescript-making-the-right-choice-48a4
https://www.youtube.com/watch?v=zM9UPcIyyhQ

* Add other filters and refactor

- Move Dialog to FiltersDialog
- CheckboxFilter, TextFilter, XCheckboxFilter added
- Added TripleCheckbox and TripleSwitch as tri-state checkbox/switch items using Mui stylings

* TriStateCheckbox/Switch refactor

- Change name of TripleSwitch and TripleCheckbox to TriState
- Change slightly TriStateSwitch and StyledSwitch styles
- Add option to change into TriStateSwitches via `localStorage` value `useSwitches`

* Refactor code and fix issues

* fix: prettier

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
This commit is contained in:
Marcin Majewski
2024-08-18 17:31:41 +02:00
committed by GitHub
parent eb5522b066
commit b2caf26da3
73 changed files with 1914 additions and 916 deletions
+1
View File
@@ -13,3 +13,4 @@ src/plugins/*/*\[*\]*.ts
dist dist
total.svg total.svg
src/plugins/index.ts src/plugins/index.ts
.history
+3 -1
View File
@@ -138,13 +138,14 @@ export default tseslint.config(
'@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'no-case-declarations': 'warn', 'no-case-declarations': 'warn',
'no-undef': 'error', 'no-undef': 'error',
}, },
languageOptions: { languageOptions: {
ecmaVersion: 5, ecmaVersion: 5,
sourceType: 'module', sourceType: 'module',
globals: Object.assign(...globalsHermes), globals: Object.assign({}, ...globalsHermes),
}, },
}, },
{ {
@@ -154,6 +155,7 @@ export default tseslint.config(
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-namespace': 'off', '@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-ts-comment': 'off',
'no-undef': 'error', 'no-undef': 'error',
+66 -1
View File
@@ -26,11 +26,13 @@
"react-virtuoso": "^4.7.11", "react-virtuoso": "^4.7.11",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"sanitize-html": "^2.12.1", "sanitize-html": "^2.12.1",
"urlencode": "^2.0.0" "urlencode": "^2.0.0",
"zustand": "^4.5.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.5.0", "@eslint/js": "^9.5.0",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/eslint-config-prettier": "^6.11.3",
"@types/qs": "^6.9.15", "@types/qs": "^6.9.15",
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
@@ -1868,6 +1870,12 @@
"@types/eslint": "*" "@types/eslint": "*"
} }
}, },
"node_modules/@types/eslint-config-prettier": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/@types/eslint-config-prettier/-/eslint-config-prettier-6.11.3.tgz",
"integrity": "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==",
"dev": true
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@@ -5661,6 +5669,41 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zustand": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.4.tgz",
"integrity": "sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/zustand/node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
} }
}, },
"dependencies": { "dependencies": {
@@ -6763,6 +6806,12 @@
"@types/eslint": "*" "@types/eslint": "*"
} }
}, },
"@types/eslint-config-prettier": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/@types/eslint-config-prettier/-/eslint-config-prettier-6.11.3.tgz",
"integrity": "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==",
"dev": true
},
"@types/estree": { "@types/estree": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@@ -9390,6 +9439,22 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true "dev": true
},
"zustand": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.4.tgz",
"integrity": "sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg==",
"requires": {
"use-sync-external-store": "1.2.0"
},
"dependencies": {
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {}
}
}
} }
} }
} }
+3 -1
View File
@@ -39,11 +39,13 @@
"react-virtuoso": "^4.7.11", "react-virtuoso": "^4.7.11",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"sanitize-html": "^2.12.1", "sanitize-html": "^2.12.1",
"urlencode": "^2.0.0" "urlencode": "^2.0.0",
"zustand": "^4.5.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.5.0", "@eslint/js": "^9.5.0",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/eslint-config-prettier": "^6.11.3",
"@types/qs": "^6.9.15", "@types/qs": "^6.9.15",
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
+2 -1
View File
@@ -26,6 +26,7 @@ const proxyRequest = (req, res) => {
proxy.on('proxyRes', function (proxyRes, req, res) { proxy.on('proxyRes', function (proxyRes, req, res) {
var statusCode = proxyRes.statusCode; var statusCode = proxyRes.statusCode;
// Redirect
if ( if (
statusCode === 301 || statusCode === 301 ||
statusCode === 302 || statusCode === 302 ||
@@ -82,7 +83,7 @@ const cookiesHandler = (req, res) => {
http http
.createServer(function (req, res) { .createServer(function (req, res) {
const path = req.url.replace(/^\//, ''); const path = req.url.charAt(0) === '/' ? req.url.slice(1) : req.url;
if (req.headers['access-control-request-method']) { if (req.headers['access-control-request-method']) {
res.setHeader( res.setHeader(
'access-control-allow-methods', 'access-control-allow-methods',
@@ -55,16 +55,16 @@ async function getFilters(sources: HotNovelPubMetadata) {
return filters; return filters;
} }
interface response { type response = {
status: number; status: number;
message: string; message: string;
data?: DataEntity[]; data?: DataEntity[];
} };
interface DataEntity { type DataEntity = {
id: number; id: number;
name: string; name: string;
slug: string; slug: string;
} };
async function start() { async function start() {
const result = []; const result = [];
+38 -38
View File
@@ -3,17 +3,17 @@ import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin'; import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
export interface HotNovelPubMetadata { export type HotNovelPubMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
filters?: Filters; filters?: Filters;
options?: HotNovelPubOptions; options?: HotNovelPubOptions;
} };
interface HotNovelPubOptions { type HotNovelPubOptions = {
lang?: string; lang?: string;
} };
class HotNovelPubPlugin implements Plugin.PluginBase { class HotNovelPubPlugin implements Plugin.PluginBase {
id: string; id: string;
@@ -161,16 +161,16 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
resolveUrl = (path: string) => this.site + '/' + path; resolveUrl = (path: string) => this.site + '/' + path;
} }
interface responseNovels { type responseNovels = {
status: number; status: number;
message: string; message: string;
data: Data; data: Data;
} };
interface Data { type Data = {
category?: Category; category?: Category;
books: Books; books: Books;
} };
interface Category { type Category = {
id: number; id: number;
name: string; name: string;
description: string; description: string;
@@ -178,13 +178,13 @@ interface Category {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
countryId: number; countryId: number;
} };
interface Books { type Books = {
total: number; total: number;
pages_count: number; pages_count: number;
data?: DataEntity[]; data?: DataEntity[];
} };
interface DataEntity { type DataEntity = {
id: number; id: number;
name: string; name: string;
view: number; view: number;
@@ -193,24 +193,24 @@ interface DataEntity {
slug: string; slug: string;
categories?: CategoriesEntity[] | null; categories?: CategoriesEntity[] | null;
source?: null; source?: null;
} };
interface CategoriesEntity { type CategoriesEntity = {
id: number; id: number;
name: string; name: string;
slug?: string; slug?: string;
} };
interface responseNovel { type responseNovel = {
status: number; status: number;
message: string; message: string;
data: Data1; data: Data1;
} };
interface Data1 { type Data1 = {
book: Book1; book: Book1;
tags: Tags; tags: Tags;
chapters?: ChaptersEntity[]; chapters?: ChaptersEntity[];
} };
interface Book1 { type Book1 = {
id: number; id: number;
name: string; name: string;
description: string; description: string;
@@ -229,47 +229,47 @@ interface Book1 {
source?: null; source?: null;
idSource?: null; idSource?: null;
prize?: null; prize?: null;
} };
interface Authorize { type Authorize = {
id: number; id: number;
name: string; name: string;
description: string; description: string;
avatar: string; avatar: string;
slug: string; slug: string;
} };
interface Tags { type Tags = {
tags_name?: string[] | null; tags_name?: string[] | null;
tags?: CategoriesEntity[] | null; tags?: CategoriesEntity[] | null;
} };
interface ChaptersEntity { type ChaptersEntity = {
id: number; id: number;
title: string; title: string;
slug: string; slug: string;
index: number; index: number;
} };
interface ChapterType { type ChapterType = {
type: string; type: string;
data?: string[]; data?: string[];
} };
interface responseSearch { type responseSearch = {
status: number; status: number;
message: string; message: string;
data: Data2; data: Data2;
} };
interface Data2 { type Data2 = {
books?: BooksEntity[] | null; books?: BooksEntity[] | null;
authorizes?: AuthorizesEntityOrCategoriesEntity[] | null; authorizes?: AuthorizesEntityOrCategoriesEntity[] | null;
categories?: AuthorizesEntityOrCategoriesEntity[] | null; categories?: AuthorizesEntityOrCategoriesEntity[] | null;
} };
interface BooksEntity { type BooksEntity = {
id: number; id: number;
name: string; name: string;
slug: string; slug: string;
} };
interface AuthorizesEntityOrCategoriesEntity { type AuthorizesEntityOrCategoriesEntity = {
id: number; id: number;
name: string; name: string;
slug: string; slug: string;
} };
+2 -2
View File
@@ -5,12 +5,12 @@ import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio'; import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export interface IfreedomMetadata { export type IfreedomMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
filters?: Filters; filters?: Filters;
} };
class IfreedomPlugin implements Plugin.PluginBase { class IfreedomPlugin implements Plugin.PluginBase {
id: string; id: string;
+4 -4
View File
@@ -4,18 +4,18 @@ import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs'; import { Filters, FilterTypes } from '@libs/filterInputs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface LightNovelWorldOptions { type LightNovelWorldOptions = {
lang?: string; lang?: string;
versionIncrements?: number; versionIncrements?: number;
} };
export interface LightNovelWorldMetadata { export type LightNovelWorldMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
options?: LightNovelWorldOptions; options?: LightNovelWorldOptions;
filters?: any; filters?: any;
} };
class LightNovelWorld implements Plugin.PagePlugin { class LightNovelWorld implements Plugin.PagePlugin {
id: string; id: string;
+4 -4
View File
@@ -6,21 +6,21 @@ import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from '@libs/defaultCover'; import { defaultCover } from '@libs/defaultCover';
import { Filters } from '@libs/filterInputs'; import { Filters } from '@libs/filterInputs';
interface LightNovelWPOptions { type LightNovelWPOptions = {
reverseChapters?: boolean; reverseChapters?: boolean;
lang?: string; lang?: string;
versionIncrements?: number; versionIncrements?: number;
seriesPath?: string; seriesPath?: string;
customJs?: string; customJs?: string;
} };
export interface LightNovelWPMetadata { export type LightNovelWPMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
options?: LightNovelWPOptions; options?: LightNovelWPOptions;
filters?: any; filters?: any;
} };
class LightNovelWPPlugin implements Plugin.PluginBase { class LightNovelWPPlugin implements Plugin.PluginBase {
id: string; id: string;
+4 -4
View File
@@ -9,20 +9,20 @@ import dayjs from 'dayjs';
const includesAny = (str: string, keywords: string[]) => const includesAny = (str: string, keywords: string[]) =>
new RegExp(keywords.join('|')).test(str); new RegExp(keywords.join('|')).test(str);
interface MadaraOptions { type MadaraOptions = {
useNewChapterEndpoint?: boolean; useNewChapterEndpoint?: boolean;
lang?: string; lang?: string;
orderBy?: string; orderBy?: string;
versionIncrements?: number; versionIncrements?: number;
} };
export interface MadaraMetadata { export type MadaraMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
options?: MadaraOptions; options?: MadaraOptions;
filters?: any; filters?: any;
} };
class MadaraPlugin implements Plugin.PluginBase { class MadaraPlugin implements Plugin.PluginBase {
id: string; id: string;
+6 -6
View File
@@ -5,17 +5,17 @@ import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from '@libs/defaultCover'; import { defaultCover } from '@libs/defaultCover';
import { Filters } from '@libs/filterInputs'; import { Filters } from '@libs/filterInputs';
interface MTLNovelOptions { type MTLNovelOptions = {
lang?: string; lang?: string;
} };
export interface MTLNovelMetadata { export type MTLNovelMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
options?: MTLNovelOptions; options?: MTLNovelOptions;
filters?: any; filters?: any;
} };
class MTLNovelPlugin implements Plugin.PluginBase { class MTLNovelPlugin implements Plugin.PluginBase {
id: string; id: string;
@@ -197,11 +197,11 @@ class MTLNovelPlugin implements Plugin.PluginBase {
const result = await res.json(); const result = await res.json();
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
interface SearchEntry { type SearchEntry = {
title: string; title: string;
thumbnail: string; thumbnail: string;
permalink: string; permalink: string;
} };
result.items[0].results.map((item: SearchEntry) => { result.items[0].results.map((item: SearchEntry) => {
const novelName = item.title.replace(/<\/?strong>/g, ''); const novelName = item.title.replace(/<\/?strong>/g, '');
const novelCover = item.thumbnail; const novelCover = item.thumbnail;
+4 -4
View File
@@ -3,18 +3,18 @@ import { fetchApi } from '@libs/fetch';
import { Plugin } from '@typings/plugin'; import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
interface ReadNovelFullOptions { type ReadNovelFullOptions = {
lang?: string; lang?: string;
versionIncrements?: number; versionIncrements?: number;
} };
export interface ReadNovelFullMetadata { export type ReadNovelFullMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
options?: ReadNovelFullOptions; options?: ReadNovelFullOptions;
filters?: any; filters?: any;
} };
class ReadNovelFullPlugin implements Plugin.PluginBase { class ReadNovelFullPlugin implements Plugin.PluginBase {
id: string; id: string;
+4 -4
View File
@@ -6,17 +6,17 @@ import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import qs from 'qs'; import qs from 'qs';
interface ReadwnOptions { type ReadwnOptions = {
versionIncrements?: number; versionIncrements?: number;
} };
export interface ReadwnMetadata { export type ReadwnMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
filters?: any; filters?: any;
options?: ReadwnOptions; options?: ReadwnOptions;
} };
class ReadwnPlugin implements Plugin.PluginBase { class ReadwnPlugin implements Plugin.PluginBase {
id: string; id: string;
+4 -4
View File
@@ -5,12 +5,12 @@ import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio'; import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export interface RulateMetadata { export type RulateMetadata = {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
filters?: Filters; filters?: Filters;
} };
class RulatePlugin implements Plugin.PluginBase { class RulatePlugin implements Plugin.PluginBase {
id: string; id: string;
@@ -241,10 +241,10 @@ class RulatePlugin implements Plugin.PluginBase {
}; };
} }
interface response { type response = {
id: number; id: number;
title_one: string; title_one: string;
title_two: string; title_two: string;
url: string; url: string;
img: string; img: string;
} };
+19 -23
View File
@@ -8,8 +8,6 @@ import {
ThemeProvider, ThemeProvider,
} from '@mui/material'; } from '@mui/material';
import Appbar from '@components/Appbar'; import Appbar from '@components/Appbar';
import { Provider } from 'react-redux';
import store from '@redux/store';
import AppProvider from './provider'; import AppProvider from './provider';
import PopularNovels from '@components/accordions/PopularNovels'; import PopularNovels from '@components/accordions/PopularNovels';
import SearchNovels from '@components/accordions/SearchNovels'; import SearchNovels from '@components/accordions/SearchNovels';
@@ -30,27 +28,25 @@ function App() {
}, },
}); });
return ( return (
<Provider store={store}> <StyledEngineProvider injectFirst>
<StyledEngineProvider injectFirst> <ThemeProvider theme={theme}>
<ThemeProvider theme={theme}> <CssBaseline />
<CssBaseline /> <Appbar />
<Appbar /> <Container
<Container sx={{
sx={{ mt: 6,
mt: 6, width: { sm: 500, md: 1024, lg: '90vw' },
width: { sm: 500, md: 1024, lg: '90vw' }, }}
}} >
> <HeadersSection />
<HeadersSection /> <PopularNovels />
<PopularNovels /> <SearchNovels />
<SearchNovels /> <ParseNovel />
<ParseNovel /> <ParseChapter />
<ParseChapter /> </Container>
</Container> <AppProvider />
<AppProvider /> </ThemeProvider>
</ThemeProvider> </StyledEngineProvider>
</StyledEngineProvider>
</Provider>
); );
} }
+26 -26
View File
@@ -18,42 +18,29 @@ import { Search, Translate } from '@mui/icons-material';
import { searchPlugins } from '@provider/plugins'; import { searchPlugins } from '@provider/plugins';
import { Plugin } from '@typings/plugin'; import { Plugin } from '@typings/plugin';
import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { FixedSizeList, ListChildComponentProps } from 'react-window';
import { useDispatch } from 'react-redux'; import { useAppStore } from '@store';
import { selectPlugin } from '@redux/pluginSlice';
import usePlugin from '@hooks/usePlugin';
const resovleIcon = (iconPath: string) => { const resovleIcon = (iconPath: string) => {
return '/static/' + iconPath; return '/static/' + iconPath;
}; };
export default function () { function renderPlugin(setListVisible: (v: boolean) => void) {
const [searchedPlugins, setSearchPlugins] = useState<Plugin.PluginBase[]>([]); return function (props: ListChildComponentProps<Plugin.PluginBase[]>) {
const [keyword, setKeyword] = useState('');
const [listVisible, setListVisible] = useState(false);
const plugin = usePlugin();
const dispatch = useDispatch();
useEffect(() => {
const plugins = searchPlugins(keyword);
setSearchPlugins(plugins);
}, [keyword]);
function renderPlugin(props: ListChildComponentProps<Plugin.PluginBase[]>) {
const { index, style, data } = props; const { index, style, data } = props;
const plugin = data[index]; const plugin = data[index];
const selectPlugin = useAppStore(state => state.selectPlugin);
return ( return (
<ListItem style={style} key={index} component="div" disablePadding> <ListItem style={style} key={index} component="div" disablePadding>
<ListItemButton <ListItemButton
onClick={() => { onClick={() => {
dispatch( selectPlugin({
selectPlugin({ id: plugin.id,
id: plugin.id, name: plugin.name,
name: plugin.name, version: plugin.version,
version: plugin.version, icon: plugin.icon,
icon: plugin.icon, site: plugin.site,
site: plugin.site, });
}),
);
setListVisible(false); setListVisible(false);
}} }}
> >
@@ -64,7 +51,20 @@ export default function () {
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
); );
} };
}
export default function Appbar() {
const [searchedPlugins, setSearchPlugins] = useState<Plugin.PluginBase[]>([]);
const [keyword, setKeyword] = useState('');
const [listVisible, setListVisible] = useState(false);
const plugin = useAppStore(state => state.plugin);
useEffect(() => {
const plugins = searchPlugins(keyword);
setSearchPlugins(plugins);
}, [keyword]);
return ( return (
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
@@ -111,7 +111,7 @@ export default function () {
backgroundColor: 'white', backgroundColor: 'white',
}} }}
> >
{renderPlugin} {renderPlugin(setListVisible)}
</FixedSizeList> </FixedSizeList>
) : null} ) : null}
</FormControl> </FormControl>
+2 -2
View File
@@ -3,10 +3,10 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
import React from 'react'; import React from 'react';
import CopiableItem from './CopiableItem'; import CopiableItem from './CopiableItem';
interface Validation { type Validation = {
type?: 'error' | 'warning'; type?: 'error' | 'warning';
message?: string; message?: string;
} };
const validatePath = (path: string): Validation => { const validatePath = (path: string): Validation => {
if (path.includes('http')) if (path.includes('http'))
+14
View File
@@ -0,0 +1,14 @@
import { styled, Switch } from '@mui/material';
export const StyledSwitch = styled(Switch)(() => ({
padding: 8,
'& .MuiSwitch-track': {
borderRadius: 22 / 2,
},
'& .MuiSwitch-thumb': {
boxShadow: 'none',
width: 16,
height: 16,
margin: 2,
},
}));
@@ -1,10 +1,10 @@
import { Box, Button, Stack, TextField } from '@mui/material'; import { Box, Button, Stack, TextField } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import AccordionContainer from '../components/AccordionContainer'; import AccordionContainer from '../components/AccordionContainer';
import usePlugin from '@hooks/usePlugin';
import Textarea from '@components/Textarea'; import Textarea from '@components/Textarea';
import { useAppStore } from '@store';
export default function ParseChapter() { export default function ParseChapter() {
const plugin = usePlugin(); const plugin = useAppStore(state => state.plugin);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [chapterPath, setchapterPath] = useState(''); const [chapterPath, setchapterPath] = useState('');
const [chapterText, setChapterText] = useState(''); const [chapterText, setChapterText] = useState('');
@@ -2,12 +2,12 @@ import { Box, Button, Pagination, Stack, TextField } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import AccordionContainer from '../components/AccordionContainer'; import AccordionContainer from '../components/AccordionContainer';
import { Plugin } from '@typings/plugin'; import { Plugin } from '@typings/plugin';
import usePlugin from '@hooks/usePlugin';
import SourceNovelCard from './SourceNovelCard'; import SourceNovelCard from './SourceNovelCard';
import ChapterList from './ChaptertList'; import ChapterList from './ChaptertList';
import { useAppStore } from '@store';
export default function ParseNovel() { export default function ParseNovel() {
const plugin = usePlugin(); const plugin = useAppStore(state => state.plugin);
const [sourceNovel, setSouceNovel] = useState< const [sourceNovel, setSouceNovel] = useState<
Plugin.SourceNovel | undefined Plugin.SourceNovel | undefined
>(); >();
@@ -0,0 +1,11 @@
#filtersContent {
display: flex;
flex-direction: column;
align-content: center;
}
.switch_filter {
display: flex;
flex-direction: column;
align-items: center;
}
@@ -8,40 +8,42 @@ import {
FormControlLabel, FormControlLabel,
FormGroup, FormGroup,
} from '@mui/material'; } from '@mui/material';
import React, { useEffect, useState, ChangeEvent } from 'react'; import React, { useEffect, useState, ChangeEvent, useRef } from 'react';
import AccordionContainer from '../components/AccordionContainer'; import AccordionContainer from '../components/AccordionContainer';
import { Plugin } from '@typings/plugin'; import { Plugin } from '@typings/plugin';
import NovelItemCard from '../components/NovelItemCard'; import NovelItemCard from '../components/NovelItemCard';
import { Filters } from '@libs/filterInputs'; import { Filters, FilterToValues } from '@libs/filterInputs';
import usePlugin from '@hooks/usePlugin'; import { useAppStore } from '@store';
import './index.css';
import { DialogRef, FiltersDialog } from '../components/filters/FiltersDialog';
export default function PopularNovels() { export default function PopularNovels() {
const plugin = usePlugin(); const plugin = useAppStore(state => state.plugin);
const [novels, setNovels] = useState<Plugin.NovelItem[]>([]); const [novels, setNovels] = useState<Plugin.NovelItem[]>([]);
const [filterValues, setFilterValues] = useState<Filters | undefined>(); const [filterValues, setFilterValues] = useState<
FilterToValues<Filters> | undefined
>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [maxIndex, setMaxIndex] = useState(0); const [maxIndex, setMaxIndex] = useState(0);
const [isLatest, setIsLatest] = useState(true); const [isLatest, setIsLatest] = useState(true);
const dialogRef = useRef<DialogRef>(null);
const fetchNovelsByIndex = (index: number) => { const fetchNovelsByIndex = async (index: number) => {
if (plugin && index) { if (plugin && index) {
setLoading(true); setLoading(true);
plugin const novels = await plugin.popularNovels(index, {
.popularNovels(index, { filters: filterValues || {},
filters: filterValues, showLatestNovels: isLatest,
showLatestNovels: isLatest, });
}) if (novels.length !== 0) {
.then(res => { setCurrentIndex(index);
if (res.length !== 0) { if (index > maxIndex) {
setCurrentIndex(index); setMaxIndex(index);
if (index > maxIndex) { }
setMaxIndex(index); setNovels(novels);
} }
setNovels(res); setLoading(false);
}
})
.finally(() => setLoading(false));
} }
}; };
@@ -59,14 +61,15 @@ export default function PopularNovels() {
useEffect(() => { useEffect(() => {
// Reset when changing plugins. // Reset when changing plugins.
// TODO: Fix crashing on plugin change!
setCurrentIndex(0); setCurrentIndex(0);
setMaxIndex(0); setMaxIndex(0);
setNovels([]); setNovels([]);
if (plugin?.filters) { if (plugin?.filters) {
const filters = {}; const filters: FilterToValues<typeof plugin.filters> = {};
for (const fKey in plugin.filters) { for (const fKey in plugin.filters) {
filters[fKey] = { filters[fKey as keyof typeof filters] = {
type: plugin.filters[fKey].type, type: plugin.filters[fKey].type,
value: plugin.filters[fKey].value, value: plugin.filters[fKey].value,
}; };
@@ -74,6 +77,7 @@ export default function PopularNovels() {
setFilterValues(filters); setFilterValues(filters);
} }
}, [plugin]); }, [plugin]);
return ( return (
<AccordionContainer title="Popular Novels" loading={loading}> <AccordionContainer title="Popular Novels" loading={loading}>
<Stack direction={'row'} spacing={2} alignItems={'center'}> <Stack direction={'row'} spacing={2} alignItems={'center'}>
@@ -103,10 +107,25 @@ export default function PopularNovels() {
> >
Next Page Next Page
</Button> </Button>
<Button
disabled={!plugin || isLatest || !plugin.filters}
onClick={() => {
dialogRef.current?.show();
}}
>
Filters
</Button>
<FiltersDialog
ref={dialogRef}
values={filterValues}
filters={plugin?.filters}
setValues={setFilterValues}
refetch={() => fetchNovelsByIndex(1)}
/>
<ToggleButtonGroup <ToggleButtonGroup
value={currentIndex} value={currentIndex}
exclusive exclusive
onChange={(event, value) => fetchNovelsByIndex(value)} onChange={(_event, value) => fetchNovelsByIndex(value)}
> >
{Array.from({ length: maxIndex }, (_, index) => index + 1).map( {Array.from({ length: maxIndex }, (_, index) => index + 1).map(
number => ( number => (
@@ -3,35 +3,34 @@ import React, { useEffect, useState } from 'react';
import AccordionContainer from '../components/AccordionContainer'; import AccordionContainer from '../components/AccordionContainer';
import { Plugin } from '@typings/plugin'; import { Plugin } from '@typings/plugin';
import NovelItemCard from '../components/NovelItemCard'; import NovelItemCard from '../components/NovelItemCard';
import { useSelector } from 'react-redux'; import { useAppStore } from '@store';
import { AppState } from '@redux/store';
import { getPlugin } from '@provider/plugins';
export default function SearchNovels() { export default function SearchNovels() {
const selectedPluginItem = useSelector( const plugin = useAppStore(state => state.plugin);
(state: AppState) => state.plugin.selected,
);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [page] = useState(1); const [page] = useState(1);
const [plugin, setPlugin] = useState<Plugin.PluginBase | undefined>();
const [novels, setNovels] = useState<Plugin.NovelItem[]>([]); const [novels, setNovels] = useState<Plugin.NovelItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => {
if (selectedPluginItem) { const [fetchError, setFetchError] = useState('');
setPlugin(getPlugin(selectedPluginItem.id));
}
}, [selectedPluginItem]);
const fetchNovels = () => { const fetchNovels = () => {
if (plugin) { if (plugin) {
setLoading(true); setLoading(true);
plugin plugin
.searchNovels(searchTerm, page) .searchNovels(searchTerm, page)
.then(res => setNovels(res)) .then(res => setNovels(res))
.finally(() => setLoading(false)); .finally(() => setLoading(false))
.catch(e => {
setFetchError(e.message);
});
} }
}; };
return ( return (
<AccordionContainer title="Search Novels" loading={loading}> <AccordionContainer title="Search Novels" loading={loading}>
{fetchError && <span style={{ color: 'red' }}>{fetchError}</span>}
<Stack direction={'row'} spacing={2}> <Stack direction={'row'} spacing={2}>
<TextField <TextField
value={searchTerm} value={searchTerm}
@@ -0,0 +1,64 @@
import React from 'react';
import { Filter, FilterTypes, ValueOfFilter } from '@libs/filterInputs';
import {
Checkbox,
FormControl,
FormControlLabel,
FormLabel,
Grid,
} from '@mui/material';
type FilterType = FilterTypes.CheckboxGroup;
export const CheckboxFilter = ({
filter: { key, filter },
value,
set,
}: {
filter: { key: string; filter: Filter<FilterType> };
value: ValueOfFilter<FilterType>;
set: (v: typeof value) => void;
}) => {
const label = filter.label.replace(/s+/gi, '-');
return (
<FormControl key={key} variant="outlined">
<FormLabel>{filter.label}</FormLabel>
<Grid
container
columns={{ xs: 3 }}
id={`checkbox_filter_${key}_${label}`}
>
{filter.options.map(option => {
return (
<Grid
key={`${key}_${option.label}`}
id={`checkbox_filter_option_${key}_${option.value.replace(/\s+/gi, '-')}`}
item
xs={1}
justifyContent={'center'}
display={'flex'}
>
<FormControl>
<FormControlLabel
control={
<Checkbox
onChange={() => {
set(
value.includes(option.value)
? value.filter(q => q !== option.value)
: [...value, option.value],
);
}}
checked={value.includes(option.value)}
/>
}
label={option.label}
/>
</FormControl>
</Grid>
);
})}
</Grid>
</FormControl>
);
};
@@ -0,0 +1,211 @@
import React, {
forwardRef,
ReactNode,
useEffect,
useImperativeHandle,
} from 'react';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from '@mui/material';
import { useState } from 'react';
import {
AnyFilterValue,
Filters,
FilterToValues,
FilterTypes,
} from '@libs/filterInputs';
import { PickerFilter } from './PickerFilter';
import { SwitchFilter } from './SwitchFilter';
import { TextFilter } from './TextFilter';
import { CheckboxFilter } from './CheckboxFilter';
import { XCheckboxFilter } from './XCheckboxFilter';
const renderFilters = (
filters: Filters | undefined,
values: FilterToValues<Filters> | undefined,
set: (key: string, v: AnyFilterValue) => void,
): React.ReactNode => {
const isValueCorrectType = <T extends AnyFilterValue>(
o: AnyFilterValue,
f: T,
): o is T => {
const checkIfIsCorrectObjectType = (o: AnyFilterValue, f: T): o is T => {
const areArrays = Array.isArray(o) && Array.isArray(f);
const areObjects = typeof o === 'object' && typeof f === 'object';
return areArrays || areObjects;
};
return typeof o === typeof f || checkIfIsCorrectObjectType(o, f);
};
if (!filters || !values) return false;
return (
<>
{Object.entries(filters).map(([key, filter]) => {
if (!(key in values)) {
console.error(`No filter value for ${key} in filter values!`);
return null;
}
switch (filter.type) {
case FilterTypes.Picker: {
// Check if filterValues have correct type
// this needs to be inside of every case in this switch to get correct value type
const value = values[key].value; // here value has every possible value type
// We could just do `as typeof filter.value` but just to be sure I made a typeguard
if (!isValueCorrectType<typeof filter.value>(value, filter.value)) {
console.error(
`FilterValue for filter [${key}] has a wrong type!`,
);
return null;
}
// value; // here value has only Picker's value
return (
<PickerFilter
key={`picker_filter_${key}`}
filter={{ key, filter }}
value={value}
set={newValue => set(key, newValue)}
/>
);
}
case FilterTypes.Switch: {
// Check if filterValues have correct type
// this needs to be inside of every case in this switch to get correct value type
const value = values[key].value; // here value has every possible value type
// We could just do `as typeof filter.value` but just to be sure I made a typeguard
if (!isValueCorrectType<typeof filter.value>(value, filter.value)) {
console.error(
`FilterValue for filter [${key}] has a wrong type!`,
);
return null;
}
// value; // here value has only Picker's value
return (
<SwitchFilter
filter={{ key, filter }}
key={`switch_filter_${key}`}
value={value}
set={newValue => set(key, newValue)}
/>
);
}
case FilterTypes.TextInput: {
const value = values[key].value;
if (!isValueCorrectType(value, filter.value)) {
console.error(
`FilterValue for filter [${key}] has a wrong type!`,
);
return null;
}
return (
<TextFilter
filter={{ key, filter }}
set={newValue => set(key, newValue)}
value={value}
key={`text_filter_${key}`}
/>
);
}
case FilterTypes.CheckboxGroup: {
const value = values[key].value;
if (!isValueCorrectType(value, filter.value)) {
console.error(
`FilterValue for filter [${key}] has a wrong type!`,
);
return null;
}
return (
<CheckboxFilter
filter={{ key, filter }}
set={newValue => set(key, newValue)}
value={value}
key={`checkbox_filter_${key}`}
/>
);
}
case FilterTypes.ExcludableCheckboxGroup: {
const value = values[key].value;
if (!isValueCorrectType(value, filter.value)) {
console.error(
`FilterValue for filter [${key}] has a wrong type!`,
);
return null;
}
return (
<XCheckboxFilter
filter={{ key, filter }}
set={newValue => set(key, newValue)}
value={value}
key={`checkbox_filter_${key}`}
/>
);
}
}
})}
</>
);
};
export type DialogRef = { show(): void; hide(): void };
export const FiltersDialog = forwardRef<
DialogRef,
{
values: FilterToValues<Filters> | undefined;
filters: Filters | undefined;
setValues: React.Dispatch<
React.SetStateAction<FilterToValues<Filters> | undefined>
>;
refetch(): void;
}
>(function FiltersDialog({ values, setValues, filters, refetch }, ref) {
const [dialogOpen, setDialogOpen] = useState(false);
const [filterElements, setFilterElements] = useState<ReactNode>(false);
const setFilterWithKey = (key: string, newValue: AnyFilterValue) =>
setValues(fValues =>
!fValues
? fValues
: {
...fValues,
[key]: {
...fValues[key],
value: newValue,
},
},
);
useImperativeHandle(ref, () => ({
show() {
setDialogOpen(true);
},
hide() {
setDialogOpen(false);
},
}));
useEffect(() => {
setFilterElements(renderFilters(filters, values, setFilterWithKey));
}, [values]);
return (
<Dialog
scroll="paper"
open={dialogOpen}
onClose={() => setDialogOpen(false)}
>
<DialogTitle>Filters</DialogTitle>
<DialogContent dividers id="filtersContent">
{filterElements}
</DialogContent>
<DialogActions>
<Button onClick={() => refetch()}>Refetch</Button>
<Button style={{ color: 'red' }} onClick={() => setDialogOpen(false)}>
Close
</Button>
</DialogActions>
</Dialog>
);
});
@@ -0,0 +1,61 @@
import React from 'react';
import { Filter, FilterTypes, ValueOfFilter } from '@libs/filterInputs';
import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
type FilterType = FilterTypes.Picker;
export const PickerFilter = ({
filter: { key, filter },
value,
set,
}: {
filter: { key: string; filter: Filter<FilterType> };
value: ValueOfFilter<FilterType>;
set: (v: ValueOfFilter<FilterType>) => void;
}) => {
const label = filter.label.replace(/s+/gi, '-');
return (
<FormControl
key={key}
variant="filled"
fullWidth
margin="dense"
style={{ alignSelf: 'center' }}
>
<InputLabel id={`picker_filter_label_${label}_${key}`}>
{filter.label}
</InputLabel>
<Select
size="medium"
labelId={`picker_filter_label_${label}_${key}`}
id={`picker_filter_${label}_${key}`}
value={value || key}
onChange={({ target: { value: newValue } }) => {
if (newValue === key) newValue = '';
set(newValue);
}}
MenuProps={{ slotProps: { paper: { sx: { maxHeight: 36 * 4 } } } }}
label={filter.label}
>
{filter.options.map(option => {
return (
<MenuItem
style={{ minHeight: 36 }}
key={`${key}_${option.value}`}
value={
/** Because Mui treats empty string as `empty value`
* and doesn't show the MenuItem, if the value is empty, just use the key...
* We could do some other weird stuff, but I doubt there will be a filter
* that has the same name as the value being used inside of it.
* We can warn people in the documentation about this behavior */
option.value || key
}
>
{option.label}
</MenuItem>
);
})}
</Select>
</FormControl>
);
};
@@ -0,0 +1,33 @@
import React from 'react';
import { Filter, FilterTypes, ValueOfFilter } from '@libs/filterInputs';
import { FormControl, FormControlLabel } from '@mui/material';
import { StyledSwitch } from '../../../StyledSwitch';
type FilterType = FilterTypes.Switch;
export const SwitchFilter = ({
filter: { key, filter },
value,
set,
}: {
filter: { key: string; filter: Filter<FilterType> };
value: ValueOfFilter<FilterType>;
set: (v: typeof value) => void;
}) => {
const label = filter.label.replace(/s+/gi, '-');
return (
<FormControl
key={key}
className="switch_filter"
fullWidth
variant="outlined"
>
<FormControlLabel
style={{ userSelect: 'none' }}
label={filter.label}
id={`switch_filter_${label}_${key}`}
control={<StyledSwitch onChange={() => set(!value)} />}
/>
</FormControl>
);
};
@@ -0,0 +1,38 @@
import React, { useRef } from 'react';
import { Filter, FilterTypes, ValueOfFilter } from '@libs/filterInputs';
import { FormControl, TextField } from '@mui/material';
type FilterType = FilterTypes.TextInput;
export const TextFilter = ({
filter: { key, filter },
value,
set,
}: {
filter: { key: string; filter: Filter<FilterType> };
value: ValueOfFilter<FilterType>;
set: (v: typeof value) => void;
}) => {
const label = filter.label.replace(/s+/gi, '-');
const textRef = useRef<HTMLInputElement>(null);
return (
<FormControl key={key} className="switch_filter">
<TextField
color="primary"
id={`text_filter_${label}_${key}`}
inputRef={textRef}
label={filter.label}
value={value}
type="text"
variant="filled"
fullWidth
margin="dense"
onInput={() => {
if (textRef.current) {
set(textRef.current.value ?? '');
}
}}
/>
</FormControl>
);
};
@@ -0,0 +1,140 @@
import { styled } from '@mui/system';
import React, { useRef, useState } from 'react';
class TriStateCheckboxClasses {
root = 'TriStateCheckboxRoot';
base = 'TriStateCheckboxBase';
input = 'TriStateCheckboxInput';
svg = 'TriStateCheckboxSVG';
checked: string;
unchecked: string;
neutral: string;
offsets = {
neutral: 16,
checked: 16 * 2,
unchecked: 0,
};
constructor() {
this.checked = `${this.base}-checked`;
this.neutral = `${this.base}-neutral`;
this.unchecked = `${this.base}-unchecked`;
}
}
const classes = new TriStateCheckboxClasses();
const TriStateCheckboxRoot = styled(`span`, {
name: classes.root,
slot: 'root',
})(({ theme }) => ({
[`&.${classes.checked}`]: {
color: theme.palette.primary.main,
},
[`&.${classes.unchecked}`]: {
color: 'red',
},
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
boxSizing: 'border-box',
outline: 0,
border: 0,
margin: 0,
cursor: 'pointer',
userSelect: 'none',
textDecoration: 'none',
borderRadius: '50%',
padding: '9px',
}));
const TriStateCheckboxInput = styled(`input`, {
name: classes.input,
slot: 'input',
})(() => ({
cursor: 'inherit',
position: 'absolute',
opacity: 0,
height: '100%',
width: '100%',
top: 0,
left: 0,
margin: 0,
padding: 0,
zIndex: 1,
}));
const TriStateCheckboxSVG = styled(`svg`, { name: classes.svg, slot: 'image' })(
() => ({
userSelect: 'none',
width: '1em',
height: '1em',
display: 'inline-block',
fill: 'currentcolor',
flexShrink: 0,
fontSize: '1.5rem',
transition: 'fill 200ms cubic-bezier(0.4,0,0.2,1) 0ms',
}),
);
const emptyPath = (
<path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"></path>
);
const checkmarkPath = (
<path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>
);
const uncheckedPath = (
<path
d={`M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zM8 6l4 4l4-4l2 2l-4 4l4,4l-2 2l-4-4l-4 4l-2-2l4-4l-4-4z`}
></path>
);
export type TriStateCheckboxState = 'checked' | 'unchecked' | 'neutral';
export const TriStateCheckbox = function ({
value,
onChange,
}: {
value?: TriStateCheckboxState;
onChange?: (newValue: TriStateCheckboxState) => void;
}) {
const [state, sSt] = useState<TriStateCheckboxState>(value ?? 'neutral');
const setState = (v: TriStateCheckboxState) => {
onChange?.(v);
sSt(value ?? v);
};
const root = useRef<HTMLSpanElement>(null);
const curVal = value ?? state;
return (
<>
<TriStateCheckboxRoot
className={classes[curVal]}
data-state={curVal}
ref={root}
onClick={() => {
switch (curVal) {
case 'checked':
return setState('unchecked');
case 'neutral':
return setState('checked');
case 'unchecked':
return setState('neutral');
}
}}
>
<TriStateCheckboxInput />
<TriStateCheckboxSVG>
{curVal === 'checked'
? checkmarkPath
: curVal === 'unchecked'
? uncheckedPath
: emptyPath}
</TriStateCheckboxSVG>
</TriStateCheckboxRoot>
</>
);
};
@@ -0,0 +1,191 @@
import { styled } from '@mui/system';
import React, { useRef, useState } from 'react';
class TriStateSwitchClasses {
root = 'TriStateSwitchRoot';
base = 'TriStateSwitchBase';
track = 'TriStateSwitchTrack';
thumb = 'TriStateSwitchThumb';
input = 'TriStateSwitchInput';
checked: string;
unchecked: string;
neutral: string;
offsets = {
neutral: 18,
checked: 18 * 2,
unchecked: 0,
};
width = 16 * 4 + 10;
constructor() {
this.base;
this.checked = `${this.base}-checked`;
this.neutral = `${this.base}-neutral`;
this.unchecked = `${this.base}-unchecked`;
}
}
const classes = new TriStateSwitchClasses();
const TriStateSwitchRoot = styled(`span`, {
name: classes.root,
slot: 'root',
})(() => ({
display: 'inline-flex',
width: `${classes.width}px`,
height: '38px',
overflow: 'hidden',
boxSizing: 'border-box',
position: 'relative',
flexShrink: 0,
zIndex: 0,
verticalAlign: 'middle',
padding: '8px',
}));
const TriStateSwitchTrack = styled(`span`, {
name: classes.track,
slot: 'track',
})(() => ({
// Android12 visuals:
borderRadius: 22 / 2,
// base Track
width: '100%',
height: '100%',
zIndex: -1,
content: `''`,
transition:
'opacity 150ms cubic-bezier(0.4,0,0.2,1) 0ms, background-color 150ms cubic-bezier(0.4,0,0.2,1) 0ms',
backgroundColor: '#000',
opacity: 0.38,
}));
const TriStateSwitchBase = styled(`span`, {
name: classes.base,
slot: 'base',
})(({ theme }) => ({
[`&.${classes.unchecked}`]: {
transform: `translateX(${classes.offsets.unchecked}px)`,
color: '#f33',
},
[`&.${classes.checked}`]: {
transform: `translateX(${classes.offsets.checked}px)`,
color: theme.palette.primary.main,
},
[`&.${classes.neutral}`]: {
transform: `translateX(${classes.offsets.neutral}px)`,
color: 'gray',
},
[`&.${classes.checked} + span`]: {
opacity: 0.5,
backgroundColor: theme.palette.primary.main,
},
[`&.${classes.unchecked} + span`]: {
opacity: 0.5,
backgroundColor: '#f33',
},
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
outline: 0,
border: 0,
margin: 0,
cursor: 'pointer',
userSelect: 'none',
textDecoration: 'none',
padding: '9px',
borderRadius: '50%',
position: 'absolute',
top: 0,
left: 0,
zIndex: 1,
color: '#fff',
transition:
'left 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,transform 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;',
}));
const TriStateSwitchInput = styled(`input`, {
name: classes.input,
slot: 'input',
})(() => ({
left: '-100%',
width: '300%',
cursor: 'inherit',
position: 'absolute',
opacity: 0,
height: '100%',
top: 0,
margin: 0,
padding: 0,
zIndex: 1,
}));
const TriStateSwitchThumb = styled(`span`, {
name: classes.thumb,
slot: 'thumb',
})(() => ({
boxShadow: 'none',
width: '16px',
height: '16px',
margin: '2px',
backgroundColor: 'currentColor',
borderRadius: '50%',
}));
type TriStateSwitchState = 'checked' | 'unchecked' | 'neutral';
export const TriStateSwitch = function ({
value,
onChange,
}: {
value?: TriStateSwitchState;
onChange?: (newValue: TriStateSwitchState) => void;
}) {
const [state, sSt] = useState<TriStateSwitchState>(value ?? 'neutral');
const setState = (v: TriStateSwitchState) => {
onChange?.(v);
sSt(value ?? v);
};
const root = useRef<HTMLSpanElement>(null);
return (
<>
<TriStateSwitchRoot
data-state={state}
ref={root}
onClick={e => {
const offsetX =
e.clientX - (root.current?.getBoundingClientRect().x ?? 0);
const inputWidth = parseFloat(
getComputedStyle(e.currentTarget).width,
);
const clickPercentage = offsetX / inputWidth;
const sidePressed =
clickPercentage < 0.33
? 'left'
: clickPercentage > 0.66
? 'right'
: 'center';
console.log(inputWidth, offsetX);
switch (sidePressed) {
case 'center':
return setState('neutral');
case 'left':
return setState('unchecked');
case 'right':
return setState('checked');
}
}}
>
<TriStateSwitchBase className={classes[value ?? state]}>
<TriStateSwitchInput />
<TriStateSwitchThumb />
</TriStateSwitchBase>
<TriStateSwitchTrack />
</TriStateSwitchRoot>
</>
);
};
@@ -0,0 +1,88 @@
import React from 'react';
import { Filter, FilterTypes, ValueOfFilter } from '@libs/filterInputs';
import { FormControl, FormControlLabel, FormLabel, Grid } from '@mui/material';
import { useAppStore } from '@store';
import { TriStateCheckbox, TriStateCheckboxState } from './TriStateCheckbox';
import { TriStateSwitch } from './TriStateSwitch';
type FilterType = FilterTypes.ExcludableCheckboxGroup;
export const XCheckboxFilter = ({
filter: { key, filter },
value,
set,
}: {
filter: { key: string; filter: Filter<FilterType> };
value: ValueOfFilter<FilterType>;
set: (v: typeof value) => void;
}) => {
const useSwitches = useAppStore(state => state.useSwitches);
const getCheckboxState = (v: string): TriStateCheckboxState => {
if (value.include?.includes(v)) return 'checked';
if (value.exclude?.includes(v)) return 'unchecked';
return 'neutral';
};
const handleChange = (key: string, v: TriStateCheckboxState) => {
console.log(`setting ${key} to`, v);
if (v === 'unchecked')
return set({
exclude: value.exclude
? Array.from(new Set([...value.exclude, key]))
: [key],
include: value.include?.filter(f => f !== key),
});
if (v === 'checked')
return set({
include: value.include
? Array.from(new Set([...value.include, key]))
: [key],
exclude: value.exclude?.filter(f => f !== key),
});
return set({
include: value.include?.filter(f => f !== key),
exclude: value.exclude?.filter(f => f !== key),
});
};
return (
<FormControl key={key} variant="outlined">
<FormLabel>{filter.label}</FormLabel>
<Grid container columns={{ xs: 3 }}>
{filter.options.map(option => {
return (
<Grid
key={`${key}_${option.label}`}
item
xs={1}
justifyContent={'center'}
display={'flex'}
>
<FormControl>
<FormControlLabel
labelPlacement={useSwitches ? 'top' : 'end'}
style={{ userSelect: 'none' }}
control={
useSwitches ? (
<TriStateSwitch
value={getCheckboxState(option.value)}
onChange={c => handleChange(option.value, c)}
/>
) : (
<TriStateCheckbox
value={getCheckboxState(option.value)}
onChange={c => handleChange(option.value, c)}
/>
)
}
label={option.label}
/>
</FormControl>
</Grid>
);
})}
</Grid>
</FormControl>
);
};
-18
View File
@@ -1,18 +0,0 @@
import { AppState } from '@redux/store';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Plugin } from '@typings/plugin';
import { getPlugin } from '@provider/plugins';
export default function usePlugin() {
const selectedPluginItem = useSelector(
(state: AppState) => state.plugin.selected,
);
const [plugin, setPlugin] = useState<Plugin.PluginBase | undefined>();
useEffect(() => {
if (selectedPluginItem) {
setPlugin(getPlugin(selectedPluginItem.id));
}
}, [selectedPluginItem]);
return plugin;
}
+4 -4
View File
@@ -2,7 +2,7 @@
import { parse as parseProto } from 'protobufjs'; import { parse as parseProto } from 'protobufjs';
interface FetchInit { type FetchInit = {
headers?: Record<string, string | undefined> | Headers; headers?: Record<string, string | undefined> | Headers;
method?: string; method?: string;
body?: FormData | string; body?: FormData | string;
@@ -12,7 +12,7 @@ interface FetchInit {
| undefined | undefined
| FormData | FormData
| Headers; | Headers;
} };
const makeInit = async (init?: FetchInit) => { const makeInit = async (init?: FetchInit) => {
const defaultHeaders: Record<string, string> = { const defaultHeaders: Record<string, string> = {
@@ -100,13 +100,13 @@ export const fetchText = async function (
} }
}; };
interface ProtoRequestInit { type ProtoRequestInit = {
// merged .proto file // merged .proto file
proto: string; proto: string;
requestType: string; requestType: string;
requestData?: any; requestData?: any;
responseType: string; responseType: string;
} };
const BYTE_MARK = BigInt((1 << 8) - 1); const BYTE_MARK = BigInt((1 << 8) - 1);
+80 -28
View File
@@ -1,8 +1,14 @@
export interface FilterOption { /**
* Filter's Options user can choose from
*/
export type FilterOption = {
readonly label: string; readonly label: string;
readonly value: string; readonly value: string;
} };
/**
* Every currently implemented FilterType
*/
export enum FilterTypes { export enum FilterTypes {
TextInput = 'Text', TextInput = 'Text',
Picker = 'Picker', Picker = 'Picker',
@@ -11,59 +17,83 @@ export enum FilterTypes {
ExcludableCheckboxGroup = 'XCheckbox', ExcludableCheckboxGroup = 'XCheckbox',
} }
interface SwitchFilter { type SwitchFilter = {
type: FilterTypes.Switch; type: FilterTypes.Switch;
/** Default value */ /** Default value */
value: boolean; value: boolean;
} };
interface TextFilter { type TextFilter = {
type: FilterTypes.TextInput; type: FilterTypes.TextInput;
/** Default value */ /** Default value */
value: string; value: string;
} };
interface CheckboxFilter { type CheckboxFilter = {
type: FilterTypes.CheckboxGroup; type: FilterTypes.CheckboxGroup;
options: readonly FilterOption[]; options: readonly FilterOption[];
/** Default value */ /** Default value */
value: string[]; value: string[];
} };
interface PickerFilter { type PickerFilter = {
type: FilterTypes.Picker; type: FilterTypes.Picker;
options: readonly FilterOption[]; options: readonly FilterOption[];
/** Default value */ /** Default value */
value: string; value: string;
} };
interface ExcludableCheckboxFilter { type ExcludableCheckboxFilter = {
type: FilterTypes.ExcludableCheckboxGroup; type: FilterTypes.ExcludableCheckboxGroup;
options: readonly FilterOption[]; options: readonly FilterOption[];
value: { include?: string[]; exclude?: string[] }; /** Default value */
} value: {
/** Checkboxes marked as included */
include?: string[];
/** Checkboxes marked as excluded */
exclude?: string[];
};
};
export type Filters = Record< /**
string, * key - filter pairs
{ label: string } & ( */
| PickerFilter export type Filters = Record<string, Filter<FilterTypes>>;
| CheckboxFilter
| TextFilter
| SwitchFilter
| ExcludableCheckboxFilter
)
>;
/** Mapping of each FilterType to a Filter */
type FilterFromType = {
[FilterTypes.CheckboxGroup]: CheckboxFilter;
[FilterTypes.ExcludableCheckboxGroup]: ExcludableCheckboxFilter;
[FilterTypes.Picker]: PickerFilter;
[FilterTypes.Switch]: SwitchFilter;
[FilterTypes.TextInput]: TextFilter;
};
/**
* Get type of a single filter type from the {@link FilterType}
*/
export type Filter<Type extends FilterTypes> = {
label: string;
} & FilterFromType[Type];
/**
* Strip {@link FilterObject} object from 'label' and 'options' to get key - filter_value pairs
* @see {@link ValueOfFilter}
*/
export type FilterToValues< export type FilterToValues<
T extends Record<string, { type: FilterTypes }> | undefined, FilterObject extends Record<string, { type: FilterTypes }> | undefined,
> = T extends undefined > = FilterObject extends undefined
? undefined ? undefined
: { : {
[K in keyof T]: Omit< // copy the Filters object, but just get {value,type} pairs instead of the whole Filter object
{ type: NonNullable<T>[K]['type'] } & Filters[string], [SingleFilter in keyof FilterObject]: FilterValueWithType<
'label' | 'options' FilterType<NonNullable<FilterObject>[SingleFilter]>
>; >;
}; };
/**
* Get value type for a {@link Filter} given it's FilterType
* @see {@link FilterTypes}
*/
export type ValueOfFilter<T extends FilterTypes> = export type ValueOfFilter<T extends FilterTypes> =
T extends FilterTypes.CheckboxGroup T extends FilterTypes.CheckboxGroup
? CheckboxFilter['value'] ? CheckboxFilter['value']
@@ -76,3 +106,25 @@ export type ValueOfFilter<T extends FilterTypes> =
: T extends FilterTypes.ExcludableCheckboxGroup : T extends FilterTypes.ExcludableCheckboxGroup
? ExcludableCheckboxFilter['value'] ? ExcludableCheckboxFilter['value']
: never; : never;
/** Get {@link Filter}'s type */
export type FilterType<T extends { type: unknown }> = T extends {
type: infer K;
}
? K extends FilterTypes
? K
: never
: never;
/** Get {type, value} types for given FilterType
* @see {@link ValueOfFilter}
*/
export type FilterValueWithType<T extends FilterTypes> = {
type: T;
value: ValueOfFilter<T>;
};
/**
* Any possible filter value
*/
export type AnyFilterValue = ValueOfFilter<FilterTypes>;
+4 -4
View File
@@ -367,14 +367,14 @@ class LnMTLPlugin implements Plugin.PagePlugin {
export default new LnMTLPlugin(); export default new LnMTLPlugin();
interface ChapterEntry { type ChapterEntry = {
number: number; number: number;
title: string; title: string;
slug: string; slug: string;
created_at: string; created_at: string;
} };
interface VolumeEntry { type VolumeEntry = {
id: string; id: string;
title: string; title: string;
} };
+2 -2
View File
@@ -369,9 +369,9 @@ class RanobesPlugin implements Plugin.PagePlugin {
} }
export default new RanobesPlugin(); export default new RanobesPlugin();
interface ChapterEntry { type ChapterEntry = {
id: number; id: number;
title: string; title: string;
date: string; date: string;
link: string; link: string;
} };
+2 -2
View File
@@ -6,10 +6,10 @@ import { defaultCover } from '@libs/defaultCover';
import { NovelItem } from '../../test_web/static/js'; import { NovelItem } from '../../test_web/static/js';
// import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; // import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
interface FuzzySearchOptions { type FuzzySearchOptions = {
caseSensitive: boolean; caseSensitive: boolean;
sort: boolean; sort: boolean;
} };
// Shamelessly stolen from 'https://raw.githubusercontent.com/wouterrutgers/fuzzy-search/master/src/FuzzySearch.mjs' // Shamelessly stolen from 'https://raw.githubusercontent.com/wouterrutgers/fuzzy-search/master/src/FuzzySearch.mjs'
// I did modify the code a fair bit, but the algorithm is still the same under the hood. // I did modify the code a fair bit, but the algorithm is still the same under the hood.
+4 -4
View File
@@ -722,18 +722,18 @@ class RoyalRoad implements Plugin.PluginBase {
export default new RoyalRoad(); export default new RoyalRoad();
interface ChapterEntry { type ChapterEntry = {
id: number; id: number;
volumeId: number; volumeId: number;
title: string; title: string;
date: string; date: string;
order: number; order: number;
url: string; url: string;
} };
interface VolumeEntry { type VolumeEntry = {
id: number; id: number;
title: string; title: string;
cover: string; cover: string;
order: number; order: number;
} };
+22 -22
View File
@@ -205,22 +205,22 @@ class WTRLAB implements Plugin.PluginBase {
} satisfies Filters; } satisfies Filters;
} }
interface NovelJson { type NovelJson = {
props: Props; props: Props;
page: string; page: string;
} };
interface Props { type Props = {
pageProps: PageProps; pageProps: PageProps;
__N_SSP: boolean; __N_SSP: boolean;
} };
interface PageProps { type PageProps = {
serie: Serie; serie: Serie;
server_time: Date; server_time: Date;
} };
interface Serie { type Serie = {
serie_data: SerieData; serie_data: SerieData;
chapters: Chapter[]; chapters: Chapter[];
recommendation: SerieData[]; recommendation: SerieData[];
@@ -231,9 +231,9 @@ interface Serie {
data: Data; data: Data;
is_default: boolean; is_default: boolean;
raw_type: string; raw_type: string;
} };
interface Chapter { type Chapter = {
serie_id: number; serie_id: number;
id: number; id: number;
order: number; order: number;
@@ -242,16 +242,16 @@ interface Chapter {
name: string; name: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} };
interface ChapterData { type ChapterData = {
data: ChapterContent; data: ChapterContent;
} };
interface ChapterContent { type ChapterContent = {
title: string; title: string;
body: string; body: string;
} };
interface SerieData { type SerieData = {
serie_id?: number; serie_id?: number;
recommendation_id?: number; recommendation_id?: number;
score?: string; score?: string;
@@ -273,26 +273,26 @@ interface SerieData {
from: null; from: null;
raw_id: number; raw_id: number;
genres?: number[]; genres?: number[];
} };
interface Data { type Data = {
title: string; title: string;
author: string; author: string;
description: string; description: string;
image: string; image: string;
} };
interface JsonNovel { type JsonNovel = {
success: boolean; success: boolean;
data: Datum[]; data: Datum[];
} };
interface Datum { type Datum = {
serie: Serie; serie: Serie;
chapters: Chapter[]; chapters: Chapter[];
updated_at: Date; updated_at: Date;
raw_id: number; raw_id: number;
slug: string; slug: string;
data: Data; data: Data;
} };
export default new WTRLAB(); export default new WTRLAB();
+28 -28
View File
@@ -4,35 +4,35 @@ import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs'; import { Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
interface StringValue { type StringValue = {
value: string; value: string;
} };
interface BoolValue { type BoolValue = {
value: boolean; value: boolean;
} };
interface DecimalValue { type DecimalValue = {
units: bigint; units: bigint;
nanos: number; nanos: number;
} };
interface Timestamp { type Timestamp = {
seconds: number; seconds: number;
nanos: number; nanos: number;
} };
interface NovelEntry { type NovelEntry = {
name: string; name: string;
coverUrl: string; coverUrl: string;
slug: string; slug: string;
} };
interface RelatedChapterUserInfo { type RelatedChapterUserInfo = {
isChapterUnlocked?: BoolValue; isChapterUnlocked?: BoolValue;
} };
interface ChapterItem { type ChapterItem = {
entityId: number; entityId: number;
name: string; name: string;
slug: string; slug: string;
@@ -41,19 +41,19 @@ interface ChapterItem {
relatedUserInfo?: RelatedChapterUserInfo; relatedUserInfo?: RelatedChapterUserInfo;
offset: number; offset: number;
publishedAt?: Timestamp; publishedAt?: Timestamp;
} };
interface GetChapterResponse { type GetChapterResponse = {
item?: ChapterItem; item?: ChapterItem;
} };
interface ChapterGroupCounts { type ChapterGroupCounts = {
total: number; total: number;
advance: number; advance: number;
normal: number; normal: number;
} };
interface ChapterGroupItem { type ChapterGroupItem = {
id: number; id: number;
title: string; title: string;
order: number; order: number;
@@ -61,11 +61,11 @@ interface ChapterGroupItem {
toChapterNumber?: DecimalValue; toChapterNumber?: DecimalValue;
chapterList: ChapterItem[]; chapterList: ChapterItem[];
counts?: ChapterGroupCounts; counts?: ChapterGroupCounts;
} };
interface GetChapterListResponse { type GetChapterListResponse = {
items: ChapterGroupItem[]; items: ChapterGroupItem[];
} };
enum NovelItem_Status { enum NovelItem_Status {
Finished = 0, Finished = 0,
@@ -74,14 +74,14 @@ enum NovelItem_Status {
All = -1, All = -1,
} }
interface NovelKarmaInfo { type NovelKarmaInfo = {
isActive: boolean; isActive: boolean;
isFree: boolean; isFree: boolean;
maxFreeChapter?: DecimalValue; maxFreeChapter?: DecimalValue;
canUnlockWithVip: boolean; canUnlockWithVip: boolean;
} };
interface NovelItem { type NovelItem = {
id: number; id: number;
name: string; name: string;
slug: string; slug: string;
@@ -94,11 +94,11 @@ interface NovelItem {
authorName?: StringValue; authorName?: StringValue;
karmaInfo?: NovelKarmaInfo; karmaInfo?: NovelKarmaInfo;
genres: string[]; genres: string[];
} };
interface GetNovelResponse { type GetNovelResponse = {
item?: NovelItem; item?: NovelItem;
} };
class WuxiaWorld implements Plugin.PluginBase { class WuxiaWorld implements Plugin.PluginBase {
id = 'wuxiaworld'; id = 'wuxiaworld';
+3 -2
View File
@@ -1,3 +1,4 @@
import { Plugin } from '@typings/plugin'; import { Plugin } from '../types/plugin';
export default [] as Plugin.PluginBase[]; declare const PLUGINS: Plugin.PluginItem[];
export default PLUGINS;
+5 -6
View File
@@ -3,7 +3,6 @@ import { fetchApi } from '@libs/fetch';
import { Plugin } from '@typings/plugin'; import { Plugin } from '@typings/plugin';
import { defaultCover } from '@libs/defaultCover'; import { defaultCover } from '@libs/defaultCover';
import { FilterTypes, Filters } from '@libs/filterInputs'; import { FilterTypes, Filters } from '@libs/filterInputs';
import { NovelItem } from '../../test_web/static/js';
// const novelStatus = require('@libs/novelStatus'); // const novelStatus = require('@libs/novelStatus');
// const isUrlAbsolute = require('@libs/isAbsoluteUrl'); // const isUrlAbsolute = require('@libs/isAbsoluteUrl');
// const parseDate = require('@libs/parseDate'); // const parseDate = require('@libs/parseDate');
@@ -52,13 +51,13 @@ class Syosetu implements Plugin.PluginBase {
if (parseInt(loadedCheerio('.is-current').html() || '1') !== pagenumber) if (parseInt(loadedCheerio('.is-current').html() || '1') !== pagenumber)
return []; return [];
const novels: NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
loadedCheerio('.c-card').each((_, e) => { loadedCheerio('.c-card').each((_, e) => {
const anchor = loadedCheerio(e).find('.p-ranklist-item__title a'); const anchor = loadedCheerio(e).find('.p-ranklist-item__title a');
const url = anchor.attr('href'); const url = anchor.attr('href');
if (!url) return; if (!url) return;
const name = anchor.text(); const name = anchor.text();
const novel: NovelItem = { const novel: Plugin.NovelItem = {
path: url.replace(this.novelPrefix, ''), path: url.replace(this.novelPrefix, ''),
name, name,
cover: defaultCover, cover: defaultCover,
@@ -93,12 +92,12 @@ class Syosetu implements Plugin.PluginBase {
novel.summary = loadedCheerio('#novel_ex') novel.summary = loadedCheerio('#novel_ex')
.text() .text()
.replace(/<\s*br.*?>/g, '\n'); .replace(/<\s*br.*?>/g, '\n');
cqGetChapters.each(() => { cqGetChapters.each((_, e) => {
const chapterA = loadedCheerio(this).find('a'); const chapterA = loadedCheerio(e).find('a');
const [chapterName, releaseDate, chapterUrl] = [ const [chapterName, releaseDate, chapterUrl] = [
// set the variables // set the variables
chapterA.text(), chapterA.text(),
loadedCheerio(this) loadedCheerio(e)
.find('dt') // get title .find('dt') // get title
.text() // get text .text() // get text
.replace(/(.)/g, '') // remove "(edited)" mark .replace(/(.)/g, '') // remove "(edited)" mark
+8 -8
View File
@@ -181,12 +181,12 @@ class Agitoon implements Plugin.PluginBase {
export default new Agitoon(); export default new Agitoon();
interface response { type response = {
list_limit: number; list_limit: number;
list?: ListEntity[] | null; list?: ListEntity[] | null;
list_count: number; list_count: number;
} };
interface ListEntity { type ListEntity = {
wr_id: string; wr_id: string;
wr_subject: string; wr_subject: string;
np_dir: string; np_dir: string;
@@ -200,13 +200,13 @@ interface ListEntity {
np_country: string; np_country: string;
np_age: string; np_age: string;
is_scrap: number; is_scrap: number;
} };
interface responseBook { type responseBook = {
list?: ListEntity2[] | null; list?: ListEntity2[] | null;
download_time: string; download_time: string;
} };
interface ListEntity2 { type ListEntity2 = {
wr_id: string; wr_id: string;
wr_subject: string; wr_subject: string;
wr_datetime: string; wr_datetime: string;
@@ -214,4 +214,4 @@ interface ListEntity2 {
novel_c_style1: string; novel_c_style1: string;
novel_c_str1: string; novel_c_str1: string;
data_novel_c_view: string; data_novel_c_view: string;
} };
+22 -22
View File
@@ -368,14 +368,14 @@ function decrypt(
return text; return text;
} }
interface authorization { type authorization = {
userId: number; userId: number;
token: string; token: string;
issued: string; issued: string;
expires: string; expires: string;
} };
interface currentUser { type currentUser = {
id: number; id: number;
userName: string; userName: string;
fio: string; fio: string;
@@ -391,9 +391,9 @@ interface currentUser {
emailConfirmed: boolean; emailConfirmed: boolean;
isDeleted: boolean; isDeleted: boolean;
isDisabled: boolean; isDisabled: boolean;
} };
interface response { type response = {
searchResults?: SearchResultsEntity[] | null; searchResults?: SearchResultsEntity[] | null;
realTotalCount: number; realTotalCount: number;
code?: string; code?: string;
@@ -402,8 +402,8 @@ interface response {
format: Format; format: Format;
duration: Duration; duration: Duration;
isWorkMarkEnabled: boolean; isWorkMarkEnabled: boolean;
} };
interface SearchResultsEntity { type SearchResultsEntity = {
id: number; id: number;
title: string; title: string;
annotation: string; annotation: string;
@@ -446,20 +446,20 @@ interface SearchResultsEntity {
isLiked: boolean; isLiked: boolean;
isWorkMarkEnabled: boolean; isWorkMarkEnabled: boolean;
workInLibraryState: string; workInLibraryState: string;
} };
interface Format { type Format = {
enumValue: string; enumValue: string;
value: string; value: string;
title: string; title: string;
mobileTitle: string; mobileTitle: string;
} };
interface Duration { type Duration = {
value: string; value: string;
title: string; title: string;
mobileTitle: string; mobileTitle: string;
} };
interface responseBook { type responseBook = {
message?: string; message?: string;
chapters?: ChaptersEntity[] | null; chapters?: ChaptersEntity[] | null;
allowDownloads: boolean; allowDownloads: boolean;
@@ -543,8 +543,8 @@ interface responseBook {
purchaseTime?: null; purchaseTime?: null;
likeTime?: null; likeTime?: null;
markTime?: null; markTime?: null;
} };
interface ChaptersEntity { type ChaptersEntity = {
id: number; id: number;
workId: number; workId: number;
title: string; title: string;
@@ -554,16 +554,16 @@ interface ChaptersEntity {
lastModificationTime: string; lastModificationTime: string;
textLength: number; textLength: number;
isAvailable: boolean; isAvailable: boolean;
} };
interface GalleryImagesEntity { type GalleryImagesEntity = {
id: string; id: string;
caption?: null; caption?: null;
url: string; url: string;
height: number; height: number;
width: number; width: number;
} };
interface RecommendationsEntity { type RecommendationsEntity = {
title: string; title: string;
coverUrl: string; coverUrl: string;
authorId: number; authorId: number;
@@ -590,9 +590,9 @@ interface RecommendationsEntity {
removalReason?: null; removalReason?: null;
removalReasonComment?: null; removalReasonComment?: null;
id: number; id: number;
} };
interface encryptedСhapter { type encryptedСhapter = {
id: number; id: number;
title: string; title: string;
isDraft: boolean; isDraft: boolean;
@@ -602,4 +602,4 @@ interface encryptedСhapter {
key: string; key: string;
code?: string; code?: string;
message?: string; message?: string;
} };
+12 -12
View File
@@ -226,7 +226,7 @@ class Bookriver implements Plugin.PluginBase {
} }
export default new Bookriver(); export default new Bookriver();
interface response { type response = {
props: { props: {
pageProps: { pageProps: {
state: { state: {
@@ -241,9 +241,9 @@ interface response {
}; };
}; };
}; };
} };
interface BooksEntity { type BooksEntity = {
name: string; name: string;
coverImages: { url: string }[]; coverImages: { url: string }[];
slug: string; slug: string;
@@ -256,17 +256,17 @@ interface BooksEntity {
chapters: Chapter[]; chapters: Chapter[];
}; };
statusComplete: string; statusComplete: string;
} };
interface Chapter { type Chapter = {
name: string; name: string;
available: boolean; available: boolean;
firstPublishedAt?: Date | null; firstPublishedAt?: Date | null;
createdAt?: Date | null; createdAt?: Date | null;
chapterId: number | string; chapterId: number | string;
} };
interface responseChapter { type responseChapter = {
data: { data: {
id: number; id: number;
bookId: number; bookId: number;
@@ -288,12 +288,12 @@ interface responseChapter {
}; };
publicationScheduledFor?: null; publicationScheduledFor?: null;
}; };
} };
interface responseSearch { type responseSearch = {
data: Data; data: Data;
total: number; total: number;
} };
interface Data { type Data = {
books?: BooksEntity[] | null; books?: BooksEntity[] | null;
} };
+4 -4
View File
@@ -2650,11 +2650,11 @@ class ficbook implements Plugin.PluginBase {
export default new ficbook(); export default new ficbook();
interface Data { type Data = {
data?: DataEntity[] | null; data?: DataEntity[] | null;
more: boolean; more: boolean;
} };
interface DataEntity { type DataEntity = {
id: string; id: string;
slug: string; slug: string;
author_id: number; author_id: number;
@@ -2673,4 +2673,4 @@ interface DataEntity {
readedDate?: string | null; readedDate?: string | null;
reward_cnt: number; reward_cnt: number;
cover?: string | null; cover?: string | null;
} };
+38 -38
View File
@@ -213,13 +213,13 @@ class Neobook implements Plugin.PluginBase {
export default new Neobook(); export default new Neobook();
interface BundleBooks { type BundleBooks = {
categories: any[]; categories: any[];
topSection: any[]; topSection: any[];
feed: Novels[]; feed: Novels[];
} };
interface BottomSection { type BottomSection = {
id: string; id: string;
boost: string; boost: string;
preferenceid: string; preferenceid: string;
@@ -230,9 +230,9 @@ interface BottomSection {
more: string; more: string;
part: string; part: string;
items: Novels[]; items: Novels[];
} };
interface Novels { type Novels = {
status?: string; status?: string;
error?: string; error?: string;
login?: Login; login?: Login;
@@ -290,9 +290,9 @@ interface Novels {
carouselItems?: Novels[]; carouselItems?: Novels[];
book?: Novels; book?: Novels;
activeChapterIndex?: number; activeChapterIndex?: number;
} };
interface Attachment { type Attachment = {
has: string; has: string;
id: string; id: string;
typeid: string; typeid: string;
@@ -304,16 +304,16 @@ interface Attachment {
video: string; video: string;
origin: string; origin: string;
ext: string; ext: string;
} };
interface Image { type Image = {
s: string; s: string;
m: string; m: string;
l: string; l: string;
has?: string; has?: string;
} };
interface Chapter { type Chapter = {
id: string; id: string;
token: string; token: string;
status: string; status: string;
@@ -324,9 +324,9 @@ interface Chapter {
access: string; access: string;
loaded: string; loaded: string;
data?: Data; data?: Data;
} };
interface Data { type Data = {
available: string; available: string;
access: string; access: string;
id: string; id: string;
@@ -339,27 +339,27 @@ interface Data {
datePublish: string; datePublish: string;
attachment: Attachment; attachment: Attachment;
lastComments: any[]; lastComments: any[];
} };
interface NovelsCounters { type NovelsCounters = {
impressions: Bookmarks; impressions: Bookmarks;
views: Bookmarks; views: Bookmarks;
likes: Bookmarks; likes: Bookmarks;
comments: Bookmarks; comments: Bookmarks;
bookmarks: Bookmarks; bookmarks: Bookmarks;
} };
interface Bookmarks { type Bookmarks = {
value: string; value: string;
formatted: string; formatted: string;
} };
interface Feed { type Feed = {
name: string; name: string;
canRemoved: string; canRemoved: string;
} };
interface Login { type Login = {
uid: string; uid: string;
utoken: string; utoken: string;
has: string; has: string;
@@ -376,14 +376,14 @@ interface Login {
deposit: any[]; deposit: any[];
affiliate: any[]; affiliate: any[];
preferences: any[]; preferences: any[];
} };
interface LoginCounters { type LoginCounters = {
notifications: string; notifications: string;
messages: string; messages: string;
} };
interface LoginUser { type LoginUser = {
id: string; id: string;
username: string; username: string;
firstname: string; firstname: string;
@@ -392,39 +392,39 @@ interface LoginUser {
status: string; status: string;
balance: Balance; balance: Balance;
connectedService: string; connectedService: string;
} };
interface Balance { type Balance = {
neo: Price; neo: Price;
} };
interface Price { type Price = {
has: string; has: string;
value: string; value: string;
currency: Currency; currency: Currency;
formatted: string; formatted: string;
deadline: string; deadline: string;
} };
type Currency = 'NEO'; type Currency = 'NEO';
interface Rating { type Rating = {
value: string; value: string;
count: string; count: string;
my: string; my: string;
} };
interface ReadProgress { type ReadProgress = {
chapter: string; chapter: string;
progress: string; progress: string;
} };
interface ReadTime { type ReadTime = {
hours: string; hours: string;
minutes: string; minutes: string;
} };
interface NovelsUser { type NovelsUser = {
available: string; available: string;
boosted: string; boosted: string;
id: string; id: string;
@@ -452,4 +452,4 @@ interface NovelsUser {
isSubscription: string; isSubscription: string;
isBlock: string; isBlock: string;
gr: string; gr: string;
} };
+38 -38
View File
@@ -183,7 +183,7 @@ class novelOvh implements Plugin.PluginBase {
export default new novelOvh(); export default new novelOvh();
interface BooksEntity { type BooksEntity = {
id: string; id: string;
slug: string; slug: string;
type: string; type: string;
@@ -211,18 +211,18 @@ interface BooksEntity {
chaptersCount: number; chaptersCount: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} };
interface Name { type Name = {
ru: string; ru: string;
original: string; original: string;
en: string; en: string;
} };
interface AltNamesEntity { type AltNamesEntity = {
language: string; language: string;
name: string; name: string;
} };
interface responseNovel { type responseNovel = {
book: Book; book: Book;
branches: BranchesEntity[]; branches: BranchesEntity[];
chapters: ChaptersEntity[]; chapters: ChaptersEntity[];
@@ -233,8 +233,8 @@ interface responseNovel {
promotionVideos?: null[] | null; promotionVideos?: null[] | null;
folderItems?: null[] | null; folderItems?: null[] | null;
contentNotices?: null[] | null; contentNotices?: null[] | null;
} };
interface Book { type Book = {
id: string; id: string;
slug: string; slug: string;
type: string; type: string;
@@ -273,17 +273,17 @@ interface Book {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
rating?: null; rating?: null;
} };
interface LabelsEntity { type LabelsEntity = {
id: string; id: string;
slug: string; slug: string;
name: string; name: string;
} };
interface RelationsEntity { type RelationsEntity = {
type: string; type: string;
publisher: Publisher; publisher: Publisher;
} };
interface Publisher { type Publisher = {
id: string; id: string;
kind: string; kind: string;
slug: string; slug: string;
@@ -298,8 +298,8 @@ interface Publisher {
tonAddress?: null; tonAddress?: null;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} };
interface BranchesEntity { type BranchesEntity = {
id: string; id: string;
licensed: boolean; licensed: boolean;
editorsChoice: boolean; editorsChoice: boolean;
@@ -309,8 +309,8 @@ interface BranchesEntity {
publishers?: PublishersEntity[] | null; publishers?: PublishersEntity[] | null;
subscription?: null; subscription?: null;
bookmark?: null; bookmark?: null;
} };
interface PublishersEntity { type PublishersEntity = {
id: string; id: string;
kind: string; kind: string;
slug: string; slug: string;
@@ -324,8 +324,8 @@ interface PublishersEntity {
tonAddress?: null; tonAddress?: null;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} };
interface ChaptersEntity { type ChaptersEntity = {
id: string; id: string;
name?: string | null; name?: string | null;
title?: string | null; title?: string | null;
@@ -341,8 +341,8 @@ interface ChaptersEntity {
createdAt: string; createdAt: string;
expiredAt: string; expiredAt: string;
updatedAt: string; updatedAt: string;
} };
interface RelatedEntityOrBook { type RelatedEntityOrBook = {
id: string; id: string;
type: string; type: string;
slug: string; slug: string;
@@ -361,23 +361,23 @@ interface RelatedEntityOrBook {
averageRating: number; averageRating: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} };
interface AssociatedEntity { type AssociatedEntity = {
id: string; id: string;
kind: string; kind: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
book: RelatedEntityOrBook; book: RelatedEntityOrBook;
} };
interface CoversEntity { type CoversEntity = {
id: string; id: string;
volume: number; volume: number;
image: string; image: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} };
interface responseChapter { type responseChapter = {
id: string; id: string;
name?: string; name?: string;
title: string; title: string;
@@ -397,22 +397,22 @@ interface responseChapter {
createdAt: string; createdAt: string;
expiredAt: string; expiredAt: string;
updatedAt: string; updatedAt: string;
} };
interface Content { type Content = {
type: string; type: string;
content?: ContentEntity[] | null; content?: ContentEntity[] | null;
} };
interface ContentEntity { type ContentEntity = {
text?: string | null; text?: string | null;
type: string; type: string;
attrs?: Attrs | null; attrs?: Attrs | null;
content?: Content[] | null; content?: Content[] | null;
} };
interface Attrs { type Attrs = {
textAlign?: string; textAlign?: string;
pages?: [string]; pages?: [string];
} };
interface PagesEntity { type PagesEntity = {
id: string; id: string;
image: string; image: string;
index: number; index: number;
@@ -420,4 +420,4 @@ interface PagesEntity {
height: number; height: number;
width: number; width: number;
createdAt: string; createdAt: string;
} };
+32 -32
View File
@@ -851,25 +851,25 @@ class TL implements Plugin.PluginBase {
export default new TL(); export default new TL();
interface response { type response = {
data: Data; data: Data;
} };
interface Data { type Data = {
projects?: Projects; projects?: Projects;
project?: Project; project?: Project;
chapter?: DataChapter; chapter?: DataChapter;
} };
interface DataChapter { type DataChapter = {
text: Text; text: Text;
} };
interface Text { type Text = {
text: string; text: string;
} };
interface Project { type Project = {
title: string; title: string;
translationStatus: string; translationStatus: string;
fullUrl: string; fullUrl: string;
@@ -879,58 +879,58 @@ interface Project {
tags: Genre[]; tags: Genre[];
annotation: Text; annotation: Text;
subprojects: Subprojects; subprojects: Subprojects;
} };
interface Cover { type Cover = {
url: string; url: string;
} };
interface Genre { type Genre = {
nameRu: string; nameRu: string;
nameEng: string; nameEng: string;
} };
interface Person { type Person = {
role: string; role: string;
name: Name; name: Name;
} };
interface Name { type Name = {
firstName: string; firstName: string;
lastName: string; lastName: string;
} };
interface Subprojects { type Subprojects = {
content: SubprojectsContent[]; content: SubprojectsContent[];
} };
interface SubprojectsContent { type SubprojectsContent = {
title: null | string; title: null | string;
volumes: Volumes; volumes: Volumes;
} };
interface Volumes { type Volumes = {
content: VolumesContent[]; content: VolumesContent[];
} };
interface VolumesContent { type VolumesContent = {
shortName: null | string; shortName: null | string;
chapters: ChapterElement[]; chapters: ChapterElement[];
} };
interface ChapterElement { type ChapterElement = {
title: null | string; title: null | string;
publishDate: Date; publishDate: Date;
fullUrl: string; fullUrl: string;
published: boolean; published: boolean;
} };
interface Projects { type Projects = {
content: ProjectsContent[]; content: ProjectsContent[];
} };
interface ProjectsContent { type ProjectsContent = {
title: string; title: string;
fullUrl: string; fullUrl: string;
covers: Cover[]; covers: Cover[];
} };
+40 -42
View File
@@ -1147,7 +1147,7 @@ class RNBH implements Plugin.PluginBase {
export default new RNBH(); export default new RNBH();
interface responseNovels { type responseNovels = {
id: number; id: number;
names: Names; names: Names;
rating: number; rating: number;
@@ -1158,27 +1158,27 @@ interface responseNovels {
status: string; status: string;
user?: User; user?: User;
counts: Counts; counts: Counts;
} };
interface Names { type Names = {
eng?: string; eng?: string;
rus?: string; rus?: string;
original: string; original: string;
} };
interface Poster { type Poster = {
medium: string; medium: string;
small: string; small: string;
color: string; color: string;
} };
interface User { type User = {
status?: null; status?: null;
liked: boolean; liked: boolean;
} };
interface Counts { type Counts = {
volumes: string; volumes: string;
chapters: string; chapters: string;
} };
interface responseNovel { type responseNovel = {
id: number; id: number;
names: Names; names: Names;
rating: number; rating: number;
@@ -1195,55 +1195,53 @@ interface responseNovel {
start_reading_url: string; start_reading_url: string;
html: string; html: string;
tags: Tags; tags: Tags;
} };
interface Posters { type Posters = {
big: string; big: string;
medium: string; medium: string;
small: string; small: string;
tiny: string; tiny: string;
color: string; color: string;
} };
interface AuthorsEntity { type AuthorsEntity = {
name_eng: string; name_eng: string;
pivot: Pivot; pivot: Pivot;
} };
interface Pivot { type Pivot = {
ranobe_id: number; ranobe_id: number;
author_id?: number; author_id?: number;
translator_id?: number; translator_id?: number;
} };
interface TranslatorsEntity { type TranslatorsEntity = {
name: string; name: string;
pivot: Pivot; pivot: Pivot;
} };
interface Status { type Status = {
id: number; id: number;
title: string; title: string;
} name: string;
interface Tags { };
type Tags = {
events?: GenresOrEntity[] | null; events?: GenresOrEntity[] | null;
genres?: GenresOrEntity[] | null; genres?: GenresOrEntity[] | null;
} };
interface GenresOrEntity { type GenresOrEntity = {
id: number; id: number;
names: Names; names: Names;
url: string; url: string;
title: string; title: string;
description?: string | null; description?: string | null;
} };
interface VolumesEntity { type VolumesEntity = {
id: number; id: number;
num: number; num: number;
name: string; name: string;
status: Status; status: Status;
chapters?: ChaptersEntity[] | null; chapters?: ChaptersEntity[] | null;
} };
interface Status {
id: number; type ChaptersEntity = {
name: string;
}
interface ChaptersEntity {
id: number; id: number;
name: string; name: string;
num: number; num: number;
@@ -1252,18 +1250,18 @@ interface ChaptersEntity {
has_images: boolean; has_images: boolean;
changed_at: string; changed_at: string;
comments_count: string; comments_count: string;
} };
interface responseSearch { type responseSearch = {
meta: Meta; meta: Meta;
collections?: DataEntity[] | null; collections?: DataEntity[] | null;
data?: DataEntity[] | null; data?: DataEntity[] | null;
} };
interface Meta { type Meta = {
key: string; key: string;
title: string; title: string;
} };
interface DataEntity { type DataEntity = {
id: number; id: number;
names?: Names | null; names?: Names | null;
description?: string | null; description?: string | null;
@@ -1275,10 +1273,10 @@ interface DataEntity {
roles?: null[] | null; roles?: null[] | null;
avatar?: Avatar | null; avatar?: Avatar | null;
has_plus?: boolean | null; has_plus?: boolean | null;
} };
interface Avatar { type Avatar = {
big: string; big: string;
color: string; color: string;
thumb: string; thumb: string;
is_default: boolean; is_default: boolean;
} };
+44 -44
View File
@@ -526,82 +526,82 @@ function jsonToHtml(json: HTML[], images, html = '') {
return html; return html;
} }
interface HTML { type HTML = {
type: string; type: string;
content?: HTML[]; content?: HTML[];
attrs?: Attrs; attrs?: Attrs;
text?: string; text?: string;
} };
interface Attrs { type Attrs = {
src: string; src: string;
alt: string | null; alt: string | null;
title: string | null; title: string | null;
} };
interface authorization { type authorization = {
token: Token; token: Token;
auth: Auth; auth: Auth;
timestamp: number; timestamp: number;
} };
interface Token { type Token = {
token_type: string; token_type: string;
expires_in: number; expires_in: number;
access_token: string; access_token: string;
refresh_token: string; refresh_token: string;
timestamp: number; timestamp: number;
} };
interface Auth { type Auth = {
id: number; id: number;
username: string; username: string;
avatar: Cover; avatar: Cover;
last_online_at: string; last_online_at: string;
metadata: Metadata; metadata: Metadata;
} };
interface Metadata { type Metadata = {
auth_domains: string; auth_domains: string;
} };
interface TopLevel { type TopLevel = {
data: DataClass | DataClass[]; data: DataClass | DataClass[];
links?: Links; links?: Links;
meta?: Meta; meta?: Meta;
} };
interface AgeRestriction { type AgeRestriction = {
id: number; id: number;
label: string; label: string;
} };
interface Branch { type Branch = {
id: number; id: number;
branch_id: null; branch_id: null;
created_at: string; created_at: string;
teams: BranchTeam[]; teams: BranchTeam[];
user: User; user: User;
} };
interface BranchTeam { type BranchTeam = {
id: number; id: number;
slug: string; slug: string;
slug_url: string; slug_url: string;
model: string; model: string;
name: string; name: string;
cover: Cover; cover: Cover;
} };
interface Cover { type Cover = {
filename: null | string; filename: null | string;
thumbnail: string; thumbnail: string;
default: string; default: string;
} };
interface User { type User = {
username: string; username: string;
id: number; id: number;
} };
interface DataClass { type DataClass = {
id: number; id: number;
name: string; name: string;
rus_name?: string; rus_name?: string;
@@ -633,9 +633,9 @@ interface DataClass {
likes_count?: number; likes_count?: number;
content?: any; content?: any;
attachments?: Attachment[]; attachments?: Attachment[];
} };
interface Artist { type Artist = {
id: number; id: number;
slug: string; slug: string;
slug_url: string; slug_url: string;
@@ -648,16 +648,16 @@ interface Artist {
confirmed: null; confirmed: null;
user_id: number; user_id: number;
titles_count_details: null; titles_count_details: null;
} };
interface Subscription { type Subscription = {
is_subscribed: boolean; is_subscribed: boolean;
source_type: string; source_type: string;
source_id: number; source_id: number;
relation: null; relation: null;
} };
interface Attachment { type Attachment = {
id?: string; id?: string;
filename: string; filename: string;
name: string; name: string;
@@ -665,14 +665,14 @@ interface Attachment {
url: string; url: string;
width: number; width: number;
height: number; height: number;
} };
interface Genre { type Genre = {
id: number; id: number;
name: string; name: string;
} };
interface DataTeam { type DataTeam = {
id: number; id: number;
slug: string; slug: string;
slug_url: string; slug_url: string;
@@ -682,22 +682,22 @@ interface DataTeam {
details?: Details; details?: Details;
vk?: string; vk?: string;
discord?: null; discord?: null;
} };
interface Details { type Details = {
branch_id: null; branch_id: null;
is_active: boolean; is_active: boolean;
subscriptions_count: null; subscriptions_count: null;
} };
interface Links { type Links = {
first: string; first: string;
last: null; last: null;
prev: null; prev: null;
next: string; next: string;
} };
interface Meta { type Meta = {
current_page?: number; current_page?: number;
from?: number; from?: number;
path?: string; path?: string;
@@ -707,9 +707,9 @@ interface Meta {
has_next_page?: boolean; has_next_page?: boolean;
seed?: string; seed?: string;
country?: string; country?: string;
} };
interface DataChapter { type DataChapter = {
id: number; id: number;
index: number; index: number;
item_number: number; item_number: number;
@@ -719,4 +719,4 @@ interface DataChapter {
name: string; name: string;
branches_count: number; branches_count: number;
branches: Branch[]; branches: Branch[];
} };
+54 -54
View File
@@ -143,7 +143,7 @@ class RNRF implements Plugin.PluginBase {
export default new RNRF(); export default new RNRF();
interface response { type response = {
props: Props; props: Props;
page: string; page: string;
query: Query; query: Query;
@@ -152,25 +152,25 @@ interface response {
gssp: boolean; gssp: boolean;
appGip: boolean; appGip: boolean;
dynamicIds?: string[]; dynamicIds?: string[];
} };
interface Props { type Props = {
pageProps: PageProps; pageProps: PageProps;
isMobile: boolean; isMobile: boolean;
theme: string; theme: string;
__N_SSP: boolean; __N_SSP: boolean;
} };
interface PageProps { type PageProps = {
initialReduxState: InitialReduxState; initialReduxState: InitialReduxState;
book?: PagePropsBook; book?: PagePropsBook;
totalData?: TotalData; totalData?: TotalData;
layoutViewCookies?: string; layoutViewCookies?: string;
displayBanner?: boolean; displayBanner?: boolean;
chapter?: PagePropsChapter; chapter?: PagePropsChapter;
} };
interface PagePropsBook { type PagePropsBook = {
id: number; id: number;
countryId: number; countryId: number;
creatorId: number; creatorId: number;
@@ -215,9 +215,9 @@ interface PagePropsBook {
verticalImage: Image; verticalImage: Image;
horizontalImage: Image; horizontalImage: Image;
squareImage: Image; squareImage: Image;
} };
interface ChapterElement { type ChapterElement = {
id: number; id: number;
bookId: number; bookId: number;
title: string; title: string;
@@ -233,35 +233,35 @@ interface ChapterElement {
price: number; price: number;
views: number; views: number;
isUserPaid?: boolean; isUserPaid?: boolean;
} };
interface Country { type Country = {
id: number; id: number;
title: string; title: string;
code: string; code: string;
} };
interface Genre { type Genre = {
id: number; id: number;
title: string; title: string;
slug: string; slug: string;
} };
interface Image { type Image = {
id: number; id: number;
alt: string; alt: string;
path: string; path: string;
name: string; name: string;
url: string; url: string;
} };
interface Timer { type Timer = {
value: null; value: null;
for: null; for: null;
message: string; message: string;
} };
interface PagePropsChapter { type PagePropsChapter = {
id: number; id: number;
bookId: number; bookId: number;
editorId: null; editorId: null;
@@ -289,94 +289,94 @@ interface PagePropsChapter {
nextChapter: null; nextChapter: null;
previousChapter: PreviousChapter; previousChapter: PreviousChapter;
corrections: null; corrections: null;
} };
interface ChapterBook { type ChapterBook = {
id: number; id: number;
title: string; title: string;
titleEn: string; titleEn: string;
url: string; url: string;
} };
interface Content { type Content = {
id: number; id: number;
text: string; text: string;
symbolsCount: number; symbolsCount: number;
} };
interface PreviousChapter { type PreviousChapter = {
id: number; id: number;
title: string; title: string;
views: number; views: number;
publishedAt: Date; publishedAt: Date;
url: string; url: string;
numberChapter: string; numberChapter: string;
} };
interface InitialReduxState { type InitialReduxState = {
data?: Data; data?: Data;
auth: Auth; auth: Auth;
banners?: Banners; banners?: Banners;
corrections?: Corrections; corrections?: Corrections;
readerSettings?: ReaderSettings; readerSettings?: ReaderSettings;
} };
interface Auth { type Auth = {
isAuth: boolean; isAuth: boolean;
user: null; user: null;
} };
interface Banners { type Banners = {
displaySale: boolean; displaySale: boolean;
} };
interface Corrections { type Corrections = {
items: null; items: null;
data: null; data: null;
} };
interface Data { type Data = {
bookPage: BookPage | null; bookPage: BookPage | null;
chapterPage: ChapterPage | null; chapterPage: ChapterPage | null;
} };
interface BookPage { type BookPage = {
bookId: number; bookId: number;
bookmark: null; bookmark: null;
} };
interface ChapterPage { type ChapterPage = {
chapterId: number; chapterId: number;
bookId: number; bookId: number;
bookmark: null; bookmark: null;
} };
interface ReaderSettings { type ReaderSettings = {
fontSize: number; fontSize: number;
lineHeight: number; lineHeight: number;
textIndent: number; textIndent: number;
sidebarHide: number; sidebarHide: number;
textAlign: string; textAlign: string;
} };
interface TotalData { type TotalData = {
items: Item[]; items: Item[];
_links: Links; _links: Links;
pagesData: PagesData; pagesData: PagesData;
} };
interface Links { type Links = {
self: First; self: First;
first: First; first: First;
last: First; last: First;
next: First; next: First;
} };
interface First { type First = {
href: string; href: string;
} };
interface Item { type Item = {
id: number; id: number;
title: string; title: string;
description: string; description: string;
@@ -390,16 +390,16 @@ interface Item {
verticalImage: Image; verticalImage: Image;
horizontalImage: Image; horizontalImage: Image;
squareImage: Image; squareImage: Image;
} };
interface PagesData { type PagesData = {
totalCount: number; totalCount: number;
pageCount: number; pageCount: number;
currentPage: number; currentPage: number;
perPage: number; perPage: number;
} };
interface Query { type Query = {
book?: string; book?: string;
chapter?: string; chapter?: string;
} };
+20 -20
View File
@@ -946,7 +946,7 @@ class ReN implements Plugin.PluginBase {
export default new ReN(); export default new ReN();
interface responseNovels { type responseNovels = {
id: number; id: number;
secondary_name: string; secondary_name: string;
main_name: string; main_name: string;
@@ -969,15 +969,15 @@ interface responseNovels {
categories?: ItemEntity[] | null; categories?: ItemEntity[] | null;
rus_name?: string; rus_name?: string;
en_name?: string; en_name?: string;
} };
interface Img { type Img = {
high?: string; high?: string;
mid?: string; mid?: string;
low: string; low: string;
} };
interface responseNovel { type responseNovel = {
id: number; id: number;
img: Img; img: Img;
secondary_name: string; secondary_name: string;
@@ -1010,42 +1010,42 @@ interface responseNovel {
uploaded: number; uploaded: number;
can_post_comments: boolean; can_post_comments: boolean;
adaptation: Adaptation | null; adaptation: Adaptation | null;
} };
interface Adaptation { type Adaptation = {
main_name: string; main_name: string;
secondary_name: string; secondary_name: string;
dir: string; dir: string;
img: string; img: string;
} };
interface ItemEntity { type ItemEntity = {
id: number; id: number;
name: string; name: string;
} };
interface PublishersEntity { type PublishersEntity = {
id: number; id: number;
name: string; name: string;
img: string; img: string;
dir: string; dir: string;
tagline: string; tagline: string;
type: string; type: string;
} };
interface BranchesEntity { type BranchesEntity = {
id: number; id: number;
img: string; img: string;
publishers?: PublishersEntity[] | null; publishers?: PublishersEntity[] | null;
subscribed: boolean; subscribed: boolean;
total_votes: number; total_votes: number;
count_chapters: number; count_chapters: number;
} };
interface FirstChapter { type FirstChapter = {
id: number; id: number;
tome: number; tome: number;
chapter: string; chapter: string;
} };
interface responseСhapters { type responseСhapters = {
id: number; id: number;
rated?: null; rated?: null;
viewed?: null; viewed?: null;
@@ -1060,9 +1060,9 @@ interface responseСhapters {
pub_date?: null; pub_date?: null;
upload_date: string; upload_date: string;
is_paid: boolean; is_paid: boolean;
} };
interface responseСhapter { type responseСhapter = {
id: number; id: number;
chapter: string; chapter: string;
name: string; name: string;
@@ -1078,4 +1078,4 @@ interface responseСhapter {
index: number; index: number;
publishers?: PublishersEntity[] | null; publishers?: PublishersEntity[] | null;
tome: number; tome: number;
} };
+4 -4
View File
@@ -125,7 +125,7 @@ class RV implements Plugin.PluginBase {
export default new RV(); export default new RV();
interface book { type book = {
id: number; id: number;
name: string; name: string;
original_name: string; original_name: string;
@@ -151,9 +151,9 @@ interface book {
updated_at: string; updated_at: string;
slug: string; slug: string;
chapters_count: number; chapters_count: number;
} };
interface chapters { type chapters = {
id: number; id: number;
name: string; name: string;
number: string; number: string;
@@ -173,7 +173,7 @@ interface chapters {
is_new: boolean; is_new: boolean;
purchased_by_user: boolean; purchased_by_user: boolean;
book_slug: string; book_slug: string;
} };
function unicodeToUtf8(unicode: string) { function unicodeToUtf8(unicode: string) {
return unicode.replace(/\\u([\d\w]{4})/gi, (match, hex) => return unicode.replace(/\\u([\d\w]{4})/gi, (match, hex) =>
+28 -28
View File
@@ -111,10 +111,10 @@ class SkyNovels implements Plugin.PluginBase {
export default new SkyNovels(); export default new SkyNovels();
interface response { type response = {
novels?: NovelsEntity[] | null; novels?: NovelsEntity[] | null;
} };
interface NovelsEntity { type NovelsEntity = {
id: number; id: number;
nvl_author?: number | null; nvl_author?: number | null;
nvl_content: string; nvl_content: string;
@@ -135,16 +135,16 @@ interface NovelsEntity {
nvl_rating?: number | null; nvl_rating?: number | null;
nvl_ratings_count: number; nvl_ratings_count: number;
genres?: GenresEntity[] | null; genres?: GenresEntity[] | null;
} };
interface GenresEntity { type GenresEntity = {
id: number; id: number;
genre_name: string; genre_name: string;
} };
interface responseBook { type responseBook = {
novel?: NovelEntity[] | null; novel?: NovelEntity[] | null;
} };
interface NovelEntity { type NovelEntity = {
id: number; id: number;
nvl_author: number; nvl_author: number;
nvl_content: string; nvl_content: string;
@@ -168,29 +168,29 @@ interface NovelEntity {
novel_ratings?: NovelRatingsEntity[] | null; novel_ratings?: NovelRatingsEntity[] | null;
collaborators?: CollaboratorsEntity[] | null; collaborators?: CollaboratorsEntity[] | null;
genres?: GenresEntity[] | null; genres?: GenresEntity[] | null;
} };
interface BookmarksEntity { type BookmarksEntity = {
id: number; id: number;
user_id: number; user_id: number;
chp_id: number; chp_id: number;
chp_name: string; chp_name: string;
} };
interface VolumesEntity { type VolumesEntity = {
vlm_title: string; vlm_title: string;
id: number; id: number;
nvl_id: number; nvl_id: number;
user_id?: number | null; user_id?: number | null;
chapters?: ChaptersEntity[] | null; chapters?: ChaptersEntity[] | null;
} };
interface ChaptersEntity { type ChaptersEntity = {
id: number; id: number;
chp_index_title: string; chp_index_title: string;
chp_name: string; chp_name: string;
chp_number: number; chp_number: number;
chp_status: string; chp_status: string;
createdAt: string; createdAt: string;
} };
interface NovelRatingsEntity { type NovelRatingsEntity = {
user_id: number; user_id: number;
rate_value: number; rate_value: number;
rate_comment: string; rate_comment: string;
@@ -201,21 +201,21 @@ interface NovelRatingsEntity {
user_login: string; user_login: string;
image?: string | null; image?: string | null;
likes?: (LikesEntity | null)[] | null; likes?: (LikesEntity | null)[] | null;
} };
interface LikesEntity { type LikesEntity = {
id: number; id: number;
user_id: number; user_id: number;
user_login: string; user_login: string;
} };
interface CollaboratorsEntity { type CollaboratorsEntity = {
user_id: number; user_id: number;
user_login: string; user_login: string;
} };
interface responseChapter { type responseChapter = {
chapter?: ChapterEntity[] | null; chapter?: ChapterEntity[] | null;
} };
interface ChapterEntity { type ChapterEntity = {
id: number; id: number;
chp_author: number; chp_author: number;
chp_translator?: null; chp_translator?: null;
@@ -237,9 +237,9 @@ interface ChapterEntity {
comments?: null[] | null; comments?: null[] | null;
reactions?: null[] | null; reactions?: null[] | null;
total_reactions?: TotalReactionsEntity[] | null; total_reactions?: TotalReactionsEntity[] | null;
} };
interface TotalReactionsEntity { type TotalReactionsEntity = {
reaction_id: number; reaction_id: number;
reaction_name: string; reaction_name: string;
reaction_count: number; reaction_count: number;
} };
+1 -1
View File
@@ -140,7 +140,7 @@ class LSHNovel implements Plugin.PluginBase {
async searchNovels( async searchNovels(
searchTerm: string, searchTerm: string,
pageNo?: number | undefined, //pageNo?: number | undefined,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
const url = `${this.site}?s=${searchTerm}`; const url = `${this.site}?s=${searchTerm}`;
const headers = new Headers(); const headers = new Headers();
+26 -26
View File
@@ -142,55 +142,55 @@ function jsonToHtml(json: HTML[], html = '') {
return html; return html;
} }
interface response { type response = {
projects?: TopLevelProject[]; projects?: TopLevelProject[];
updates?: Update[]; updates?: Update[];
project?: TopLevelProject; project?: TopLevelProject;
books?: BookElement[]; books?: BookElement[];
chapter?: TopLevelChapter; chapter?: TopLevelChapter;
} };
interface BookElement { type BookElement = {
id: number; id: number;
rank: number; rank: number;
title: string; title: string;
chapters: PurpleChapter[]; chapters: PurpleChapter[];
} };
interface PurpleChapter { type PurpleChapter = {
id: number; id: number;
title: string; title: string;
rank: string; rank: string;
modifiedAt: Date; modifiedAt: Date;
} };
interface TopLevelChapter { type TopLevelChapter = {
id: number; id: number;
title: string; title: string;
rank: string; rank: string;
content: string; content: string;
modifiedAt: Date; modifiedAt: Date;
book: ChapterBook; book: ChapterBook;
} };
interface ChapterBook { type ChapterBook = {
id: number; id: number;
rank: number; rank: number;
title: string; title: string;
chapters: FluffyChapter[]; chapters: FluffyChapter[];
project: BookProject; project: BookProject;
} };
interface FluffyChapter { type FluffyChapter = {
id: number; id: number;
rank: string; rank: string;
} };
interface BookProject { type BookProject = {
id: number; id: number;
} };
interface TopLevelProject { type TopLevelProject = {
id: number; id: number;
title: string; title: string;
description: string; description: string;
@@ -205,37 +205,37 @@ interface TopLevelProject {
image: Image; image: Image;
tags?: Genre[]; tags?: Genre[];
genres?: Genre[]; genres?: Genre[];
} };
interface Genre { type Genre = {
id: number; id: number;
title: string; title: string;
} };
interface Image { type Image = {
id: number; id: number;
url: string; url: string;
name: string; name: string;
} };
interface Update { type Update = {
id: number; id: number;
title: string; title: string;
author: string; author: string;
translator: string; translator: string;
modifiedAt: Date; modifiedAt: Date;
image: Image; image: Image;
} };
interface HTML { type HTML = {
type: string; type: string;
content?: HTML[]; content?: HTML[];
attrs?: Attrs; attrs?: Attrs;
text?: string; text?: string;
} };
interface Attrs { type Attrs = {
src: string; src: string;
alt: string | null; alt: string | null;
title: string | null; title: string | null;
} };
+6 -6
View File
@@ -4,21 +4,21 @@ import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs'; import { FilterTypes, Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
interface VolumeData { type VolumeData = {
story: string; story: string;
navigation: string; navigation: string;
value: string; value: string;
} };
interface ChapterSelect { type ChapterSelect = {
chap_selector: string; chap_selector: string;
eps_selector: boolean | string; eps_selector: boolean | string;
} };
interface VolumeList { type VolumeList = {
chapters: Plugin.ChapterItem[]; chapters: Plugin.ChapterItem[];
volumes?: VolumeData[]; volumes?: VolumeData[];
} };
class TruyenConect implements Plugin.PagePlugin { class TruyenConect implements Plugin.PagePlugin {
id = 'truyenconect'; id = 'truyenconect';
+4 -4
View File
@@ -4,14 +4,14 @@ import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs'; import { Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
interface SearchedNovel { type SearchedNovel = {
name: string; name: string;
slug: string; slug: string;
coverUrl: string; coverUrl: string;
} };
interface SearchedResult { type SearchedResult = {
data?: SearchedNovel[]; data?: SearchedNovel[];
} };
class LightNovelVN implements Plugin.PagePlugin { class LightNovelVN implements Plugin.PagePlugin {
id = 'lightnovel.vn'; id = 'lightnovel.vn';
+2 -5
View File
@@ -1,11 +1,8 @@
import { AppState } from '@redux/store';
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; // import { useAppStore } from '@store';
export default function DialogProvider() { export default function DialogProvider() {
const activeDialogs = useSelector( // const _activeDialogs = useAppStore(state => state.activeDialogs);
(state: AppState) => state.dialog.activeDialogs,
);
return <></>; return <></>;
} }
+2 -3
View File
@@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import DialogProvider from './DialogProvider'; import DialogProvider from './DialogProvider';
import { Backdrop, Box, CircularProgress, Container } from '@mui/material'; import { Backdrop, Box, CircularProgress, Container } from '@mui/material';
import { useSelector } from 'react-redux'; import { useAppStore } from '@store';
import { AppState } from '@redux/store';
export default function AppProvider() { export default function AppProvider() {
const loading = useSelector((state: AppState) => state.overlay.loading); const loading = useAppStore(state => state.loading);
return ( return (
<Container sx={{ position: 'absolute' }}> <Container sx={{ position: 'absolute' }}>
<Backdrop <Backdrop
-31
View File
@@ -1,31 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export enum EDialog {}
interface DialogState {
activeDialogs: EDialog[];
}
const initialState: DialogState = { activeDialogs: [] };
export const dialogSlice = createSlice({
name: 'dialog',
initialState: initialState,
reducers: {
showDialog(state, action: PayloadAction<EDialog>) {
state.activeDialogs = [...state.activeDialogs, action.payload];
},
hideDialog(state, action: PayloadAction<EDialog>) {
state.activeDialogs = state.activeDialogs.filter(
dialog => dialog !== action.payload,
);
},
hideAllDialogs(state) {
state.activeDialogs = [];
},
},
});
export const { showDialog, hideDialog } = dialogSlice.actions;
export default dialogSlice.reducer;
-22
View File
@@ -1,22 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
loading: false,
};
export const overlaySlice = createSlice({
name: 'overlay',
initialState: initialState,
reducers: {
showLoading(state) {
state.loading = true;
},
hideLoading(state) {
state.loading = false;
},
},
});
export const { showLoading, hideLoading } = overlaySlice.actions;
export default overlaySlice.reducer;
-22
View File
@@ -1,22 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Plugin } from '@typings/plugin';
const initialState: {
selected?: Plugin.PluginItem;
} = {
selected: undefined,
};
export const pluginSlice = createSlice({
name: 'plugin',
initialState: initialState,
reducers: {
selectPlugin(state, acion: PayloadAction<Plugin.PluginItem>) {
state.selected = acion.payload;
},
},
});
export const { selectPlugin } = pluginSlice.actions;
export default pluginSlice.reducer;
-16
View File
@@ -1,16 +0,0 @@
import { configureStore } from '@reduxjs/toolkit';
import dialogReducer from './dialogSlice';
import overlaySlice from './overlaySlice';
import pluginReducer from './pluginSlice';
const store = configureStore({
reducer: {
dialog: dialogReducer,
overlay: overlaySlice,
plugin: pluginReducer,
},
});
export default store;
export type AppState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
+39
View File
@@ -0,0 +1,39 @@
import { StoreCreator } from '.';
export enum EDialog {}
export type DialogStore = {
activeDialogs: EDialog[];
showDialog(dialog: EDialog): void;
hideDialog(dialog: EDialog): void;
hideAllDialogs(): void;
};
/**
* @param set Use this inside actions to change the state
* @param get Use this inside actions to get the current state
*/
export const DialogStore: StoreCreator<DialogStore> = set => ({
// this is initial state
activeDialogs: [],
// those are actions
showDialog(dialog: EDialog) {
set(state => ({
...state,
activeDialogs: [...state.activeDialogs, dialog],
}));
},
hideDialog(dialog) {
set(state => ({
...state,
activeDialogs: state.activeDialogs.filter(d => d !== dialog),
}));
},
hideAllDialogs() {
set(state => ({
...state,
activeDialogs: [],
}));
},
});
+28
View File
@@ -0,0 +1,28 @@
import { create, StateCreator } from 'zustand';
import { PluginStore } from './pluginStore';
import { DialogStore } from './dialogStore';
import { OverlayStore } from './overlayStore';
export type AppStore = DialogStore &
OverlayStore &
PluginStore & { useSwitches: boolean; setUseSwitches(value: boolean): void };
// Helper types to use "slicing" like in Redux... We could just not use slicing, but eh
export type SetStore = Parameters<StateCreator<AppStore>>[0];
export type GetStore = Parameters<StateCreator<AppStore>>[1];
export type StoreCreator<T> = (s: SetStore, g: GetStore) => T;
export const useAppStore = create<AppStore>((set: SetStore, get: GetStore) => ({
...DialogStore(set, get),
...OverlayStore(set, get),
...PluginStore(set, get),
useSwitches: !!localStorage.getItem('useSwitches'),
setUseSwitches(uS: boolean) {
set(state => ({
...state,
useSwitches: uS,
}));
if (uS) localStorage.setItem('useSwitches', 'true');
else localStorage.removeItem('useSwitches');
},
}));
+30
View File
@@ -0,0 +1,30 @@
import { StoreCreator } from '.';
export type OverlayStore = {
loading: boolean;
showLoading(): void;
hideLoading(): void;
};
/**
* @param set Use this inside actions to change the state
* @param get Use this inside actions to get the current state
*/
export const OverlayStore: StoreCreator<OverlayStore> = set => ({
// this is initial state
loading: false,
// those are actions
showLoading() {
set(state => ({
...state,
loading: true,
}));
},
hideLoading() {
set(state => ({
...state,
loading: false,
}));
},
});
+27
View File
@@ -0,0 +1,27 @@
import { Plugin } from '@typings/plugin';
import { StoreCreator } from '.';
import { getPlugin } from '@provider/plugins';
export type PluginStore = {
pluginItem?: Plugin.PluginItem;
plugin?: Plugin.PluginBase;
selectPlugin(plugin: Plugin.PluginItem): void;
};
/**
* @param set State setter for use inside actions
* @param get State getter for use inside actions, outside of State setter
*/
export const PluginStore: StoreCreator<PluginStore> = set => ({
// this is initial state
// selectedPlugin: undefined,
// those are actions
selectPlugin(pluginItem) {
set(state => ({
...state,
pluginItem,
plugin: getPlugin(pluginItem.id),
}));
},
});
+26 -27
View File
@@ -1,6 +1,6 @@
import { FilterToValues, Filters } from '@libs/filterInputs'; import { FilterToValues, Filters } from '@libs/filterInputs';
export namespace Plugin { export namespace Plugin {
export interface ChapterItem { export type ChapterItem = {
name: string; name: string;
path: string; path: string;
/** /**
@@ -17,13 +17,13 @@ export namespace Plugin {
* For novel without pages only * For novel without pages only
*/ */
page?: string; page?: string;
} };
export interface NovelItem { export type NovelItem = {
name: string; name: string;
path: string; path: string;
cover?: string; cover?: string;
} };
export interface SourceNovel extends NovelItem { export type SourceNovel = {
/** Comma separated genre list -> "action,fantasy,romance" */ /** Comma separated genre list -> "action,fantasy,romance" */
genres?: string; genres?: string;
summary?: string; summary?: string;
@@ -31,32 +31,32 @@ export namespace Plugin {
artist?: string; artist?: string;
status?: string; status?: string;
chapters?: ChapterItem[]; chapters?: ChapterItem[];
} } & NovelItem;
export interface SourcePage { export type SourcePage = {
chapters: ChapterItem[]; chapters: ChapterItem[];
} };
export interface PopularNovelsOptions< export type PopularNovelsOptions<
Q extends Filters | undefined = Filters | undefined, Q extends Filters | undefined = Filters | undefined,
> { > = {
showLatestNovels?: boolean; showLatestNovels?: boolean;
filters?: Q extends undefined ? undefined : FilterToValues<Q>; filters: Q extends undefined ? undefined : FilterToValues<Q>;
} };
export interface PluginItem { export type PluginItem = {
id: string; id: string;
name: string; name: string;
version: string; version: string;
icon: string; icon: string;
site: string; site: string;
} };
export interface ImageRequestInit { export type ImageRequestInit = {
method?: string; method?: string;
headers?: Record<string, string>; headers?: Record<string, string>;
body?: string; body?: string;
} };
export interface PluginBase { export type PluginBase = {
id: string; id: string;
name: string; name: string;
/** /**
@@ -87,32 +87,31 @@ export namespace Plugin {
parseChapter(chapterPath: string): Promise<string>; parseChapter(chapterPath: string): Promise<string>;
searchNovels(searchTerm: string, pageNo: number): Promise<NovelItem[]>; searchNovels(searchTerm: string, pageNo: number): Promise<NovelItem[]>;
resolveUrl?(path: string, isNovel?: boolean): string; resolveUrl?(path: string, isNovel?: boolean): string;
} };
export interface PagePlugin extends PluginBase { export type PagePlugin = {
parseNovel( parseNovel(
novelPath: string, novelPath: string,
): Promise<SourceNovel & { totalPages: number }>; ): Promise<SourceNovel & { totalPages: number }>;
parsePage(novelPath: string, page: string): Promise<SourcePage>; parsePage(novelPath: string, page: string): Promise<SourcePage>;
} } & PluginBase;
} }
export namespace HTMLParser2Util { export namespace HTMLParser2Util {
interface HandlerBase { type HandlerBase = {
onopentag?(name: string, attribs: Record<string, string>): void; onopentag?(name: string, attribs: Record<string, string>): void;
ontext?(data: string): void; ontext?(data: string): void;
onclosetag?(name: string, isImplied: boolean): void; onclosetag?(name: string, isImplied: boolean): void;
} };
export interface Handler extends HandlerBase { export type Handler = {
isStarted?: boolean; isStarted?: boolean;
isDone?: boolean; isDone?: boolean;
} } & HandlerBase;
// route htmlparser2 event to handlers // route htmlparser2 event to handlers
export interface HandlerRouter<ActionType extends string> export type HandlerRouter<ActionType extends string> = {
extends HandlerBase {
handlers: Record<ActionType, Handler | undefined>; handlers: Record<ActionType, Handler | undefined>;
action: ActionType; action: ActionType;
} } & HandlerBase;
} }
+118 -117
View File
@@ -1,128 +1,129 @@
{ {
"compilerOptions": { "compilerOptions": {
"jsx": "react", "jsx": "react",
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */ /* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "ES5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "target": "ES5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [ // "lib": [
// "es2016", // "es2016",
// "dom", // "dom",
// "es5" // "es5"
// ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* Modules */
"module": "ES2020" /* Specify what module code is generated. */, "module": "ES2020" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */,
"paths": { "paths": {
"@libs/*": ["./src/libs/*"], "@libs/*": ["./src/libs/*"],
"@icons/*": ["./src/icons/*"], "@icons/*": ["./src/icons/*"],
"@components/*": ["./src/components/*"], "@components/*": ["./src/components/*"],
"@redux/*": ["./src/redux/*"], "@redux/*": ["./src/redux/*"],
"@provider/*": ["./src/provider/*"], "@provider/*": ["./src/provider/*"],
"@plugins/*": ["./src/plugins/*"], "@plugins/*": ["./src/plugins/*"],
"@hooks/*": ["./src/hooks/*"], "@hooks/*": ["./src/hooks/*"],
"@scripts/*": ["./scripts/*"], "@scripts/*": ["./scripts/*"],
"@typings/*": ["./src/types/*"] "@typings/*": ["./src/types/*"],
} /* Specify a set of entries that re-map imports to additional lookup locations. */, "@store": ["./src/store"]
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ } /* Specify a set of entries that re-map imports to additional lookup locations. */,
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
"resolveJsonModule": true /* Enable importing .json files. */, // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ "resolveJsonModule": true /* Enable importing .json files. */,
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */ /* JavaScript Support */
// "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, // "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
// "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, // "checkJs": true /* Enable error reporting in type-checked JavaScript files. */,
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */ /* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./.js/" /* Specify an output folder for all emitted files. */, "outDir": "./.js/" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
"allowJs": true, "allowJs": true,
"checkJs": false, "checkJs": false,
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true /* Enable all strict type-checking options. */, "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
}, },
"exclude": ["./src/plugins/**/*.broken.ts", "./.js/**/*", "scripts", "docs"] "exclude": ["./src/plugins/**/*.broken.ts", "./.js/**/*", "scripts", "docs"]
} }
+1
View File
@@ -19,6 +19,7 @@ export default defineConfig({
'@hooks': path.resolve(dirname, './src/hooks'), '@hooks': path.resolve(dirname, './src/hooks'),
'@typings': path.resolve(dirname, './src/types'), '@typings': path.resolve(dirname, './src/types'),
'@scripts': path.resolve(dirname, './scripts'), '@scripts': path.resolve(dirname, './scripts'),
'@store': path.resolve(dirname, './src/store'),
}, },
}, },
server: { server: {