docs: Fix Documentation Accuracy, Dead Links, and Grammar in Docs (#2402)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
f20fb69e44
commit
be2dd52cd2
+480
-204
@@ -6,8 +6,10 @@
|
||||
- [ChapterItem](#chapteritem)
|
||||
- [Filters](#filters)
|
||||
- [PluginSettings](#pluginsettings)
|
||||
- [NovelStatus](#novelstatus)
|
||||
- [Using Cheerio](#using-cheerio)
|
||||
- [Custom fetching functions](#custom-fetching-functions)
|
||||
- [Other libraries](#other-libraries)
|
||||
|
||||
Most of the Plugin/Novel type definitions accessed using the `Plugin` namespace imported via
|
||||
|
||||
@@ -23,20 +25,24 @@ PluginBase is a base class for all plugins.
|
||||
class ExamplePlugin implements Plugin.PluginBase {}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
| -------------------------------------------------------------- | -------- | ----------------------------------------------------- |
|
||||
| [id](#pluginbaseid) | yes | Plugin ID |
|
||||
| [name](#pluginbasename) | yes | Plugin Name |
|
||||
| [icon](#pluginbasename) | yes | Plugin Icon |
|
||||
| [site](#pluginbasesite) | yes | Plugin site link |
|
||||
| [version](#pluginbaseversion) | yes | Plugin version |
|
||||
| [imageRequestInit](#pluginbaseimagerequestinit) | no | Plugin Image Request Init |
|
||||
| [filters](#pluginbasefilters) | no | [Filter definition](#filter-definition-object) object |
|
||||
| [pluginSettings](#pluginbasepluginsettings) | no | [Plugin settings](#pluginsettings) object |
|
||||
| [popularNovels(page, options)](#pluginbasepopularnovels) | yes | Novel list getter |
|
||||
| [parseNovel(path)](#pluginbaseparsenovel) | yes | Novel info and chapter list getter |
|
||||
| [parseChapter(path)](#pluginbaseparsechapter) | yes | Chapter text getter |
|
||||
| [searchNovels(searchTerm, page)](#pluginbasesearchnovels) | yes | Novel searching getter |
|
||||
| Field | Required | Description |
|
||||
| ----------------------------------------------------------- | -------- | ------------------------------------------------------- |
|
||||
| [id](#pluginbaseid) | yes | Plugin ID |
|
||||
| [name](#pluginbasename) | yes | Plugin Name |
|
||||
| [icon](#pluginbaseicon) | yes | Plugin Icon |
|
||||
| [site](#pluginbasesite) | yes | Plugin site link |
|
||||
| [version](#pluginbaseversion) | yes | Plugin version |
|
||||
| [imageRequestInit](#pluginbaseimagerequestinit) | no | Plugin Image Request Init |
|
||||
| [filters](#pluginbasefilters) | no | [Filter definition](#filter-definition-object) object |
|
||||
| [pluginSettings](#pluginbasepluginsettings) | no | [Plugin settings](#pluginsettings) object |
|
||||
| [webStorageUtilized](#pluginbasewebstorageutilized) | no | Flag for plugins that need `localStorage`/`sessionStorage` |
|
||||
| [customJS](#pluginbasecustomjs) | no | Path to a custom JS file bundled with the plugin |
|
||||
| [customCSS](#pluginbasecustomcss) | no | Path to a custom CSS file bundled with the plugin |
|
||||
| [popularNovels(page, options)](#pluginbasepopularnovels) | yes | Novel list getter |
|
||||
| [parseNovel(path)](#pluginbaseparsenovel) | yes | Novel info and chapter list getter |
|
||||
| [parseChapter(path)](#pluginbaseparsechapter) | yes | Chapter text getter |
|
||||
| [searchNovels(searchTerm, page)](#pluginbasesearchnovels) | yes | Novel searching getter |
|
||||
| [resolveUrl(path, isNovel)](#pluginbaseresolveurl) | no | Helper that turns a novel/chapter path into a full URL |
|
||||
|
||||
#### PluginBase::id
|
||||
|
||||
@@ -44,9 +50,9 @@ Unique ID of your plugin
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
id = "templateID";
|
||||
...
|
||||
...
|
||||
id = 'templateID';
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -56,21 +62,23 @@ The name of your plugin that is shown in-app
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
name = "template Plugin";
|
||||
...
|
||||
...
|
||||
name = 'template Plugin';
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### PluginBase::icon
|
||||
|
||||
The path to your plugin's icon inside of `public/static` folder
|
||||
The path to your plugin's icon, relative to `public/static` (do **not** include the
|
||||
`public/static` prefix itself). The file must actually live at
|
||||
`public/static/<icon>` in this repo.
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
icon = "src/en/templateplugin/icon.png";
|
||||
...
|
||||
...
|
||||
icon = 'src/en/templateplugin/icon.png';
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -85,9 +93,9 @@ The url to the plugin's site
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
site = "https://example.com";
|
||||
...
|
||||
...
|
||||
site = 'https://example.com';
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -105,9 +113,9 @@ Where
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
version = "1.0.0";
|
||||
...
|
||||
...
|
||||
version = '1.0.0';
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -121,48 +129,90 @@ Used if images failed to load due to site's protection
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
imageRequestInit: Plugin.ImageRequestInit = {
|
||||
headers: {
|
||||
Referer: 'https://example.com',
|
||||
},
|
||||
};
|
||||
...
|
||||
...
|
||||
imageRequestInit: Plugin.ImageRequestInit = {
|
||||
headers: {
|
||||
Referer: 'https://example.com',
|
||||
},
|
||||
};
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### PluginBase::webStorageUtilized
|
||||
|
||||
Optional flag that tells the app your plugin needs access to `localStorage`/`sessionStorage`
|
||||
(see [Other libraries](#other-libraries)). Leave it unset if your plugin only uses `storage` for
|
||||
[plugin settings](#pluginsettings).
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
webStorageUtilized = true;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### PluginBase::customJS
|
||||
|
||||
Path to a custom JavaScript file, relative to `public/static` (same convention as
|
||||
[icon](#pluginbaseicon)). Used by some multi-source templates to run extra JS against the parsed
|
||||
page (e.g. stripping a site's injected copyright notice).
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
customJS = 'src/en/templateplugin/customJS.js';
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### PluginBase::customCSS
|
||||
|
||||
Path to a custom CSS file, relative to `public/static` (same convention as
|
||||
[icon](#pluginbaseicon)), applied when rendering the chapter/novel page in-app.
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
customCSS = 'src/en/templateplugin/customCSS.css';
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### PluginBase::filters
|
||||
|
||||
A [Filter definition]() object that holds filters used in [popularNovels](#pluginbasepopularnovels) function
|
||||
A [Filter definition](#filter-definition-object) object that holds filters used in the
|
||||
[popularNovels](#pluginbasepopularnovels) function
|
||||
|
||||
###### Example
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
filters = {
|
||||
order: {
|
||||
label:"Order",
|
||||
options: [
|
||||
{ label: "Popular", value: "" },
|
||||
{ label: "Newest", value: "newest" }
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
value: ""
|
||||
},
|
||||
status: {
|
||||
label: "Status",
|
||||
options: [
|
||||
{ label: "All", value: "" },
|
||||
{ label: "Ongoing", value: "ongoing" },
|
||||
{ label: "Hiatus", value: "hiatus" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
value: "",
|
||||
}
|
||||
}
|
||||
...
|
||||
...
|
||||
filters = {
|
||||
order: {
|
||||
label: 'Order',
|
||||
options: [
|
||||
{ label: 'Popular', value: '' },
|
||||
{ label: 'Newest', value: 'newest' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
value: '',
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
options: [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: 'Ongoing', value: 'ongoing' },
|
||||
{ label: 'Hiatus', value: 'hiatus' },
|
||||
{ label: 'Completed', value: 'completed' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
value: '',
|
||||
},
|
||||
} satisfies Filters;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -188,25 +238,25 @@ See [Using cheerio](#using-cheerio) for more information on how to parse HTML do
|
||||
|
||||
`NovelItem[]` An array of filtered main-page [NovelItems](#novelitem)
|
||||
|
||||
###### Example:
|
||||
###### Example
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
async popularNovels(
|
||||
page: number,
|
||||
options: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
if(options.filters.example.value === "test"){
|
||||
novels.push({
|
||||
name: "Novel1",
|
||||
path: "/novel1",
|
||||
cover:defaultCover
|
||||
})
|
||||
}
|
||||
return novels;
|
||||
...
|
||||
async popularNovels(
|
||||
page: number,
|
||||
options: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
if (options.filters.example.value === 'test') {
|
||||
novels.push({
|
||||
name: 'Novel1',
|
||||
path: '/novel1',
|
||||
cover: defaultCover,
|
||||
});
|
||||
}
|
||||
return novels;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -214,13 +264,13 @@ class ExamplePlugin implements Plugin.PluginBase {
|
||||
|
||||
This type is used for getting the options of the [popularNovels](#pluginbasepopularnovels) function
|
||||
|
||||
- <span id='popularnovelsoptions-showlatestnovels'></span>`showLatestNovels: boolean` flag set when opened with `Latest` button
|
||||
- <span id='popularnovelsoptions-showlatestnovels'></span>`showLatestNovels: boolean` flag set when opened with the `Latest` button
|
||||
|
||||
- <span id='popularnovelsoptions-showlatestnovels'></span>`filters: FilterValues<typeof filters>` object containing all selected filter values. [More about Filters](#filters)
|
||||
- <span id='popularnovelsoptions-filters'></span>`filters: FilterToValues<typeof filters>` object containing all selected filter values. [More about Filters](#filters)
|
||||
|
||||
#### PluginBase::parseNovel
|
||||
|
||||
Function that is used to get the information about particular novel and the list of it's chapters
|
||||
Function that is used to get the information about a particular novel and the list of its chapters
|
||||
|
||||
```ts
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel>
|
||||
@@ -236,42 +286,43 @@ See [Using cheerio](#using-cheerio) for more information on how to parse HTML do
|
||||
|
||||
`SourceNovel` Novel information and chapter list as [SourceNovel](#sourcenovel) object
|
||||
|
||||
> [!CAUTION] > [SourceNovel::path]() should be the same value as [NovelItem::path]() provided as parameter!
|
||||
> [!CAUTION]
|
||||
> [SourceNovel::path](#sourcenovel) should be the same value as [NovelItem::path](#novelitempath) provided as parameter!
|
||||
|
||||
###### Example:
|
||||
###### Example
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: "test",
|
||||
artist: "none",
|
||||
author: "none",
|
||||
cover: defaultCover,
|
||||
genres: "Isekai, Neverland",
|
||||
status: NovelStatus.Completed,
|
||||
summary: ""
|
||||
};
|
||||
let chapters: Plugin.ChapterItem[] = [];
|
||||
const chapter: Plugin.ChapterItem = {
|
||||
name: "",
|
||||
path: "",
|
||||
releaseTime: "",
|
||||
chapterNumber: 0,
|
||||
};
|
||||
chapters.push(chapter);
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
...
|
||||
...
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: 'test',
|
||||
artist: 'none',
|
||||
author: 'none',
|
||||
cover: defaultCover,
|
||||
genres: 'Isekai, Neverland',
|
||||
status: NovelStatus.Completed,
|
||||
summary: '',
|
||||
};
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
const chapter: Plugin.ChapterItem = {
|
||||
name: '',
|
||||
path: '',
|
||||
releaseTime: '',
|
||||
chapterNumber: 0,
|
||||
};
|
||||
chapters.push(chapter);
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### PluginBase::parseChapter
|
||||
|
||||
Function that is used to get the information about particular novel and the list of it's chapters
|
||||
Function that is used to get the text content of a particular chapter
|
||||
|
||||
```ts
|
||||
async parseChapter(chapterPath: string): Promise<string>
|
||||
@@ -287,21 +338,21 @@ See [Using cheerio](#using-cheerio) for more information on how to parse HTML do
|
||||
|
||||
`string` HTML content of the chapter
|
||||
|
||||
###### Example:
|
||||
###### Example
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
async parseChapter(chapterPath: string): Promise<string>{
|
||||
return "<h1>No chapter here</h1>";
|
||||
}
|
||||
...
|
||||
...
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
return '<h1>No chapter here</h1>';
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### PluginBase::searchNovels
|
||||
|
||||
Function that is used to find Novels in the source
|
||||
Function that is used to find novels in the source
|
||||
|
||||
```ts
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]>
|
||||
@@ -312,7 +363,7 @@ See [Using cheerio](#using-cheerio) for more information on how to parse HTML do
|
||||
###### Parameters
|
||||
|
||||
- `searchTerm` the search term
|
||||
- `page` search page number
|
||||
- `pageNo` search page number
|
||||
|
||||
###### Returns
|
||||
|
||||
@@ -322,15 +373,35 @@ See [Using cheerio](#using-cheerio) for more information on how to parse HTML do
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
return novels;
|
||||
}
|
||||
...
|
||||
...
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
return novels;
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### PluginBase::resolveUrl
|
||||
|
||||
Optional helper that turns a novel or chapter `path` into a full, requestable URL. It isn't
|
||||
required by the interface, but most plugins define one to avoid repeating
|
||||
`this.site + '/...'` string concatenation in every function.
|
||||
|
||||
```ts
|
||||
resolveUrl?(path: string, isNovel?: boolean): string;
|
||||
```
|
||||
|
||||
###### Example
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + (isNovel ? '/novel/' : '/chapter/') + path;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -338,7 +409,7 @@ class ExamplePlugin implements Plugin.PluginBase {
|
||||
|
||||
### NovelItem
|
||||
|
||||
It is an object representing information how to store/access the novel
|
||||
It is an object representing information on how to store/access the novel
|
||||
|
||||
| Field | type | Required | Description |
|
||||
| -------------------------------- | -------- | -------- | ------------------------------------------ |
|
||||
@@ -358,30 +429,34 @@ import { defaultCover } from '@libs/defaultCover';
|
||||
|
||||
### SourceNovel
|
||||
|
||||
| Field | Type | Required | Desciption |
|
||||
| ------- | ------------------------- | -------- | ---------- |
|
||||
| path | string | yes | |
|
||||
| name | string | no | string |
|
||||
| cover | `string` | no | |
|
||||
| genres | `string` | no | |
|
||||
| summary | `string` | no | |
|
||||
| author | `string` | no | |
|
||||
| artist | `string` | no | |
|
||||
| status | [NovelStatus] or `string` | no | |
|
||||
`SourceNovel` extends [NovelItem](#novelitem), so `path`, `name`, and `cover` behave the same way
|
||||
here as they do there.
|
||||
|
||||
chapters?: ChapterItem[];
|
||||
| Field | Type | Required | Description |
|
||||
| -------- | ---------------------------------- | -------- | --------------------------------------------- |
|
||||
| path | `string` | yes | Must match the [NovelItem::path](#novelitempath) passed into `parseNovel` |
|
||||
| name | `string` | yes | The novel's title |
|
||||
| cover | `string` | no | URL to the novel's cover |
|
||||
| genres | `string` | no | Comma-separated genre list, e.g. `"Action,Fantasy,Romance"` |
|
||||
| summary | `string` | no | The novel's synopsis/description |
|
||||
| author | `string` | no | |
|
||||
| artist | `string` | no | |
|
||||
| status | [NovelStatus](#novelstatus) or `string` | no | See [NovelStatus](#novelstatus) for the standard values |
|
||||
| rating | `number` | no | Rating out of 5, as a float |
|
||||
| chapters | [ChapterItem](#chapteritem)`[]` | no | The novel's chapter list |
|
||||
|
||||
---
|
||||
|
||||
### ChapterItem
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------- | -------- | -------- | ------------------------------- |
|
||||
| name | string | yes | |
|
||||
| path | string | yes | |
|
||||
| releaseTime | string | no | release time in `YYYY-MM-DD` |
|
||||
| chapterNumber | number | no | |
|
||||
| page | string | no | for multi-page chapter lists |
|
||||
| Field | Type | Required | Description |
|
||||
| ------------- | ------------------------ | -------- | ----------------------------------------------------------------- |
|
||||
| name | `string` | yes | |
|
||||
| path | `string` | yes | |
|
||||
| releaseTime | `string` | no | `"YYYY-MM-DD"` or an ISO date string |
|
||||
| chapterNumber | `number` | no | |
|
||||
| page | `string` | no | Only used for novels without pages (see `SourcePage`/`PagePlugin`) |
|
||||
| scanlator | `string` or `string[]` | no | Name(s) of the scanlation/translation group(s) |
|
||||
|
||||
### Filters
|
||||
|
||||
@@ -403,10 +478,10 @@ Every property of this object is a different filter. The key of the object is th
|
||||
|
||||
```ts
|
||||
filters = {
|
||||
order: {<FilterProperties>}
|
||||
order: {<FilterProperties>},
|
||||
} satisfies Filters;
|
||||
// accessible in popularNovels as
|
||||
options.filters.order
|
||||
options.filters.order;
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
@@ -414,7 +489,7 @@ options.filters.order
|
||||
|
||||
##### FilterProperties
|
||||
|
||||
| Name | Type | Required | Desciption |
|
||||
| Name | Type | Required | Description |
|
||||
| ------- | ---------------------------- | ------------- | ------------------------------------------------------------------ |
|
||||
| label | `string` | yes | in-app label |
|
||||
| type | `FilterTypes` | yes | type of the filter |
|
||||
@@ -447,7 +522,7 @@ Types of filters supported
|
||||
| `TextInput` | A filter allowing a free text input | `string` written value | N/A |
|
||||
| `Switch` | A boolean switch | `boolean` state of the switch | N/A |
|
||||
| `CheckboxGroup` | A grouping of checkboxes | `string[]` array containing selected values | [CheckboxGroup](#checkboxgroup-options) options |
|
||||
| `ExcludableCheckboxGroup` | A filter allowing to pick one of the choices provided in `options` | [ExcludableCheckboxGroupValues](#excludablecheckboxgroupvalue-object) object | [CheckboxGroup](#checkboxgroup-options) options |
|
||||
| `ExcludableCheckboxGroup` | A grouping of checkboxes where each one can be marked as included or excluded (e.g. "must have this genre" vs. "must not have this genre") | [ExcludableCheckboxGroupValue](#excludablecheckboxgroupvalue-object) object | [CheckboxGroup](#checkboxgroup-options) options |
|
||||
|
||||
###### Picker options
|
||||
|
||||
@@ -481,7 +556,7 @@ options: [
|
||||
|
||||
#### FilterValues object
|
||||
|
||||
It is an object used inisde of `popularNovels` that contains selected values for all filters defined in the [Filter definition](#filter-definition-object) object.
|
||||
It is an object used inside of `popularNovels` that contains selected values for all filters defined in the [Filter definition](#filter-definition-object) object.
|
||||
The keys of the filter values correspond to Filter definition keys
|
||||
|
||||
```ts
|
||||
@@ -509,8 +584,8 @@ options.filters.abc.type; // type of the filter
|
||||
|
||||
```ts
|
||||
{
|
||||
included: string[], // options with selected selected
|
||||
excluded: string[] // options with excluded selected
|
||||
include?: string[]; // values of the checkboxes marked as included
|
||||
exclude?: string[]; // values of the checkboxes marked as excluded
|
||||
}
|
||||
```
|
||||
|
||||
@@ -526,11 +601,11 @@ A user-defined object that defines configurable settings for the plugin. Each pr
|
||||
|
||||
```ts
|
||||
pluginSettings = {
|
||||
settingKey: {
|
||||
value: '',
|
||||
label: 'Setting Label',
|
||||
type: 'Text', // optional, defaults to 'Text'
|
||||
},
|
||||
settingKey: {
|
||||
value: '',
|
||||
label: 'Setting Label',
|
||||
type: 'Text', // optional, defaults to 'Text'
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -574,25 +649,25 @@ storage.set('settingKey', 'newValue');
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
hideLocked = storage.get('hideLocked');
|
||||
|
||||
pluginSettings = {
|
||||
hideLocked: {
|
||||
value: '',
|
||||
label: 'Hide locked chapters',
|
||||
type: 'Switch',
|
||||
},
|
||||
};
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
// Use the setting value
|
||||
if (this.hideLocked) {
|
||||
// Filter out locked chapters
|
||||
}
|
||||
...
|
||||
...
|
||||
hideLocked = storage.get('hideLocked');
|
||||
|
||||
pluginSettings = {
|
||||
hideLocked: {
|
||||
value: '',
|
||||
label: 'Hide locked chapters',
|
||||
type: 'Switch',
|
||||
},
|
||||
};
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
// Use the setting value
|
||||
if (this.hideLocked) {
|
||||
// Filter out locked chapters
|
||||
}
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -600,43 +675,244 @@ class ExamplePlugin implements Plugin.PluginBase {
|
||||
|
||||
```ts
|
||||
class ExamplePlugin implements Plugin.PluginBase {
|
||||
...
|
||||
site = storage.get('url');
|
||||
email = storage.get('email');
|
||||
password = storage.get('password');
|
||||
|
||||
pluginSettings = {
|
||||
url: {
|
||||
value: '',
|
||||
label: 'URL',
|
||||
// type: 'Text' is optional
|
||||
},
|
||||
email: {
|
||||
value: '',
|
||||
label: 'Email',
|
||||
type: 'Text',
|
||||
},
|
||||
password: {
|
||||
value: '',
|
||||
label: 'Password',
|
||||
// type defaults to 'Text' if omitted
|
||||
},
|
||||
};
|
||||
|
||||
async makeRequest(url: string): Promise<string> {
|
||||
return await fetchApi(url, {
|
||||
headers: {
|
||||
'Authorization': `Basic ${this.btoa(this.email + ':' + this.password)}`,
|
||||
},
|
||||
Referer: this.site,
|
||||
}).then(res => res.text());
|
||||
}
|
||||
...
|
||||
...
|
||||
site = storage.get('url');
|
||||
email = storage.get('email');
|
||||
password = storage.get('password');
|
||||
|
||||
pluginSettings = {
|
||||
url: {
|
||||
value: '',
|
||||
label: 'URL',
|
||||
// type: 'Text' is optional
|
||||
},
|
||||
email: {
|
||||
value: '',
|
||||
label: 'Email',
|
||||
type: 'Text',
|
||||
},
|
||||
password: {
|
||||
value: '',
|
||||
label: 'Password',
|
||||
// type defaults to 'Text' if omitted
|
||||
},
|
||||
};
|
||||
|
||||
async makeRequest(url: string): Promise<string> {
|
||||
return await fetchApi(url, {
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(this.email + ':' + this.password)}`,
|
||||
Referer: this.site,
|
||||
},
|
||||
}).then(res => res.text());
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### NovelStatus
|
||||
|
||||
`NovelStatus` is an enum of the standard values used for [SourceNovel::status](#sourcenovel). Using
|
||||
it (instead of a raw string) is what lets the app group/filter novels by status consistently
|
||||
across plugins.
|
||||
|
||||
```ts
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
```
|
||||
|
||||
| Member | Value |
|
||||
| --------------------- | ---------------------- |
|
||||
| `Unknown` | `'Unknown'` |
|
||||
| `Ongoing` | `'Ongoing'` |
|
||||
| `Completed` | `'Completed'` |
|
||||
| `Licensed` | `'Licensed'` |
|
||||
| `PublishingFinished` | `'Publishing Finished'` |
|
||||
| `Cancelled` | `'Cancelled'` |
|
||||
| `OnHiatus` | `'On Hiatus'` |
|
||||
| `STUB` | `'STUB'` |
|
||||
| `Inactive` | `'Inactive'` |
|
||||
|
||||
`status` isn't restricted to these values (it accepts any `string`), but prefer a `NovelStatus`
|
||||
member whenever the source's status maps onto one — free-text values won't be recognized by the
|
||||
app's status filter.
|
||||
|
||||
---
|
||||
|
||||
### Using Cheerio
|
||||
|
||||
Most sites are scraped by fetching the page HTML and parsing it with [Cheerio](https://cheerio.js.org/),
|
||||
a jQuery-like API for traversing/selecting elements server-side.
|
||||
|
||||
```ts
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
```
|
||||
|
||||
A typical `popularNovels` implementation fetches a listing page, loads it into Cheerio, and maps
|
||||
each matching element to a [NovelItem](#novelitem):
|
||||
|
||||
```ts
|
||||
async popularNovels(page: number): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
const body = await fetchApi(`${this.site}/novels?page=${page}`).then(res =>
|
||||
res.text(),
|
||||
);
|
||||
const $ = parseHTML(body);
|
||||
|
||||
$('li.novel-item').each((i, el) => {
|
||||
const name = $(el).find('.title').text().trim();
|
||||
const path = $(el).find('a').attr('href')?.replace(this.site, '');
|
||||
const cover = $(el).find('img').attr('src');
|
||||
|
||||
if (!path) return;
|
||||
novels.push({ name, path, cover });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `$(el)` re-scopes a selector to a single element found by `.each()`; without it you'd search the
|
||||
whole document again for every item.
|
||||
- `path` should be relative (strip `this.site`/the domain) — see [NovelItem::path](#novelitempath).
|
||||
- Prefer `.attr('href')` / `.attr('src')` over `.text()` for links and images, and always guard for
|
||||
`undefined` since a selector can fail to match if the site changes its markup.
|
||||
|
||||
See the [Cheerio API docs](https://cheerio.js.org/docs/api) for the full set of selectors/methods
|
||||
(`.find()`, `.first()`, `.eq()`, `.attr()`, `.text()`, `.html()`, etc.), and look at existing
|
||||
plugins under `plugins/**` for real examples.
|
||||
|
||||
---
|
||||
|
||||
### Custom fetching functions
|
||||
|
||||
Plugins can't use the browser/Node `fetch` directly — use the wrappers from `@libs/fetch` instead,
|
||||
which handle plugin-specific request setup (proxying, headers, etc.):
|
||||
|
||||
```ts
|
||||
import { fetchApi, fetchText, fetchProto } from '@libs/fetch';
|
||||
```
|
||||
|
||||
#### fetchApi
|
||||
|
||||
```ts
|
||||
declare function fetchApi(url: string, init?: FetchInit): Promise<Response>;
|
||||
```
|
||||
|
||||
The general-purpose fetcher. Returns a standard `Response`, so use `.text()`, `.json()`, etc. on
|
||||
the result, the same way you would with the native `fetch`.
|
||||
|
||||
```ts
|
||||
const res = await fetchApi(this.resolveUrl(novelPath));
|
||||
const body = await res.text();
|
||||
```
|
||||
|
||||
#### fetchText
|
||||
|
||||
```ts
|
||||
declare function fetchText(
|
||||
url: string,
|
||||
init?: FetchInit,
|
||||
encoding?: string,
|
||||
): Promise<string>;
|
||||
```
|
||||
|
||||
A shortcut for `fetchApi(...).then(res => res.text())`, with an optional `encoding` for sites that
|
||||
don't serve UTF-8 (e.g. `fetchText(url, undefined, 'gbk')` for some Chinese-language sites).
|
||||
|
||||
#### fetchProto
|
||||
|
||||
```ts
|
||||
declare function fetchProto(
|
||||
protoInit: ProtoRequestInit,
|
||||
url: string,
|
||||
init?: FetchInit,
|
||||
): Promise<unknown>;
|
||||
```
|
||||
|
||||
For sites whose API responds with [Protocol Buffers](https://protobuf.dev/) instead of JSON/HTML.
|
||||
|
||||
```ts
|
||||
type ProtoRequestInit = {
|
||||
proto: string; // the .proto schema source
|
||||
requestType: string; // message type to encode the request as
|
||||
requestData?: any; // request payload, encoded as `requestType`
|
||||
responseType: string; // message type to decode the response as
|
||||
};
|
||||
```
|
||||
|
||||
This is an advanced/uncommon case — only reach for it if the site's API is proto-based, which you
|
||||
can usually tell from binary (non-JSON) response bodies on an `application/x-protobuf`-style
|
||||
content type.
|
||||
|
||||
#### FetchInit
|
||||
|
||||
The `init` object accepted by all three functions above:
|
||||
|
||||
```ts
|
||||
type FetchInit = {
|
||||
headers?: Record<string, string> | Headers;
|
||||
method?: string;
|
||||
body?: FormData | string;
|
||||
[key: string]: string | Record<string, string> | FormData | Headers | undefined;
|
||||
};
|
||||
```
|
||||
|
||||
It mirrors the standard [`fetch` init object](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)
|
||||
(`headers`, `method`, `body`) — set headers like `Referer`/`Authorization`/`Cookie` under
|
||||
`headers`, not as top-level keys.
|
||||
|
||||
---
|
||||
|
||||
### Other libraries
|
||||
|
||||
A few smaller helpers are available for less common cases. You generally won't need these unless
|
||||
your target site requires them.
|
||||
|
||||
#### isUrlAbsolute
|
||||
|
||||
```ts
|
||||
import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
|
||||
|
||||
declare function isUrlAbsolute(url: string): boolean;
|
||||
```
|
||||
|
||||
Useful when a site mixes absolute and relative URLs in the same listing (e.g. some cover images
|
||||
are full URLs, others are paths) and you need to normalize them before returning a
|
||||
[NovelItem](#novelitem)/[SourceNovel](#sourcenovel).
|
||||
|
||||
#### storage (localStorage / sessionStorage)
|
||||
|
||||
```ts
|
||||
import { storage, localStorage, sessionStorage } from '@libs/storage';
|
||||
```
|
||||
|
||||
`storage` is the same persistent key-value store used for [plugin settings](#pluginsettings) —
|
||||
you can also use it directly for things like caching a session cookie or an auth token between
|
||||
requests. `localStorage`/`sessionStorage` are separate, lower-level stores for plugins that need
|
||||
that exact browser-style API (for example, reusing scraping code shared with a web target). If
|
||||
your plugin uses either of them, set [`webStorageUtilized`](#pluginbasewebstorageutilized) to
|
||||
`true` on the plugin so the app knows to provide that access.
|
||||
|
||||
#### AES decryption
|
||||
|
||||
```ts
|
||||
import { gcm } from '@libs/aes';
|
||||
import { utf8ToBytes, bytesToUtf8 } from '@libs/utils';
|
||||
```
|
||||
|
||||
For sites that encrypt their API responses with AES-GCM (uncommon, but seen on a handful of
|
||||
sources). `gcm(key, nonce, AAD?)` returns a `Cipher` with `encrypt`/`decrypt` methods operating on
|
||||
`Uint8Array`; `utf8ToBytes`/`bytesToUtf8` convert between that and plain strings.
|
||||
|
||||
```ts
|
||||
const cipher = gcm(keyBytes, nonceBytes);
|
||||
const plaintext = bytesToUtf8(cipher.decrypt(ciphertextBytes));
|
||||
```
|
||||
|
||||
This is an advanced case — only needed if you've confirmed the site is actually encrypting its
|
||||
payloads, not just minifying/obfuscating them.
|
||||
|
||||
Reference in New Issue
Block a user