Start adding documation and add host.ps1 for windows users (#725)

* Add docs v0.0.1

* Add icon size warning

* Fix sourcenover caution

* Fix semver descriptions

* Filters fix

* Quickfix quickstart

* Add icon warning v2

* Fix typos

* Add NovelItem

* Update plugins host

* Update plugins host

* Remove unnecesarry .js and .dist

* Update plugins host

* Add host.ps1

* Fix readme.txt

* Fix READNE.txt, again

* Remove .js and .dist

* readme update with ost

* Add linux/windows hosting functions

* Fix workflows
This commit is contained in:
Marcin Majewski
2024-02-02 04:23:50 +01:00
committed by GitHub
parent 101f082a52
commit 211f30d5b2
11 changed files with 709 additions and 48 deletions
+527
View File
@@ -0,0 +1,527 @@
## Documentation for LNReader plugins
- [PluginBase](#pluginbase)
- [NovelItem](#novelitem)
- [SourceNovel](#sourcenovel)
- [ChapterItem](#chapteritem)
- [Filters](#filters)
- [Using Cheerio](#using-cheerio)
- [Custom fetching functions](#custom-fetching-functions)
Most of the Plugin/Novel type definitions accessed using the `Plugin` namespace imported via
```ts
import { Plugin } from "@typings/plugin";
```
### PluginBase
PluginBase is a base class for all plugins.
```ts
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 |
| [filters](#pluginbasefilters) | no | [Filter definition](#filter-definition-object) object |
| [popularNovels(page, options)](#pluginbasepopularnovels) | yes | Novel list getter |
| [parseNovelAndChapters(url)](#pluginbaseparsenovelandchapters) | yes | Novel info and chapter list getter |
| [parseChapter(url)](#pluginbaseparsechapter) | yes | Chapter text getter |
| [searchNovels(searchTerm, page)](#pluginbasesearchnovels) | yes | Novel searching getter |
| [fetchImage(url)](#pluginbasefetchimage) | yes | Customizable function for fetching images |
#### PluginBase::id
Unique ID of your plugin
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
id = "templateID";
...
}
```
#### PluginBase::name
The name of your plugin that is shown in-app
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
name = "template Plugin";
...
}
```
#### PluginBase::icon
The path to your plugin's icon inside of `icon` folder
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
icon = "src/eng/templateplugin/icon.png";
...
}
```
> [!WARNING]
> Icons should be 96x96px
#### PluginBase::site
The url to the plugin's site
###### Example
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
site = "https://example.com";
...
}
```
#### PluginBase::version
Version of your plugin formatted according to [semver2.0 spec](https://semver.org/) i.e. `<major>.<minor>.<patch>`
Where
- `patch` increments on small fixes that fix the plugin (like site changed a selector, filter had a typo etc.)
- `minor` increments on fixes that improve the plugin (like adding/removing filters, adding search options etc.)
- `major` increments on fixes that fix the major issues with the plugin (like changing site link)
###### Example
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
version = "1.0.0";
...
}
```
#### PluginBase::filters
A [Filter definition]() object that holds filters used in [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: "",
}
}
...
}
```
#### PluginBase::popularNovels
Function that is used to get the (filtered) list of novels from the front page of the site
```ts
async popularNovels(
page: number,
options: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]>
```
See [Using cheerio](#using-cheerio) for more information on how to parse HTML documents
###### Parameters
- `page` current page to fetch
- `options` [PopularNovelsOptions](#pluginbasepopularnovelsoptions)
###### Returns
`NovelItem[]` An array of filtered main-page [NovelItems](#novelitem)
###### 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",
url: "https://example.com/novel1",
cover:defaultCover
})
}
return novels;
}
}
```
##### PluginBase::PopularNovelsOptions
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>`filters: FilterValues<typeof filters>` object containing all selected filter values. [More about Filters](#filters)
#### PluginBase::parseNovelAndChapters
Function that is used to get the information about particular novel and the list of it's chapters
```ts
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel>
```
See [Using cheerio](#using-cheerio) for more information on how to parse HTML documents
###### Parameters
- `novelUrl` value from [NovelItem::url](#novelitemurl)
###### Returns
`SourceNovel` Novel information and chapter list as [SourceNovel](#sourcenovel) object
> [!CAUTION] > [SourceNovel::url]() should be the same value as [NovelItem::url]() provided as parameter!
###### Example:
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const novel: Plugin.SourceNovel = {
url: novelUrl,
name: "test",
artist: "none",
author: "none",
cover: defaultCover,
genres: "Isekai, Neverland",
status: NovelStatus.Completed,
summary: ""
};
let chapters: Plugin.ChapterItem[] = [];
const chapter: Plugin.ChapterItem = {
name: "",
url: "",
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
```ts
async parseChapter(chapterUrl: string): Promise<string>
```
See [Using cheerio](#using-cheerio) for more information on how to parse HTML documents
###### Parameters
- `chapterUrl` value from [ChapterItem::url](#chapteritemurl)
###### Returns
`string` HTML content of the chapter
###### Example:
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
async parseChapter(chapterUrl: string): Promise<string>{
return "<h1>No chapter here</h1>";
}
...
}
```
#### PluginBase::searchNovels
Function that is used to find Novels in the source
```ts
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]>
```
See [Using cheerio](#using-cheerio) for more information on how to parse HTML documents
###### Parameters
- `searchTerm` the search term
- `page` search page number
###### Returns
`NovelItem[]` An array of found [NovelItems](#novelitem)
###### Example
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
async searchNovels(
searchTerm: string,
pageNo: number
): Promise<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
return novels;
}
...
}
```
#### PluginBase::fetchImage
Function used if images failed to load due to site's protection
```ts
async fetchImage(url: string): Promise<string | undefined>
```
See [Fetch functions]() for detailed list of fetch functions provided by us to help with fetching data
###### Parameter
- `url` Image's url to fetch
###### Returns
- `string` base64 representation of the image
or
- `undefined` on error
###### Example
```ts
class ExamplePlugin implements Plugin.PluginBase {
...
async fetchImage(url: string): Promise<string | undefined> {
const headers = {
Referer: "https://ln.hako.vn",
};
return await fetchFile(url, { headers: headers });
}
...
}
```
---
### NovelItem
It is an object representing information how to store/access the novel
| Field | type | Required | Description |
| -------------------------------- | -------- | -------- | ------------------------------------------ |
| <p id="novelitemurl">url</p> | `string` | yes | The url to the site |
| <p id="novelitemname">name</p> | `string` | yes | The name of the novel shown in the library |
| <p id="novelitemcover">cover</p> | `string` | no | URL to novel's cover |
#### Default cover
You can use the default `Cover not available` cover by importing
```ts
import { defaultCover } from "@libs/defaultCover";
```
---
### SourceNovel
| Field | Type | Required | Desciption |
| ----- | ------ | -------- | ---------- |
| url | 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||
chapters?: ChapterItem[];
---
### ChapterItem
---
### Filters
`Filters` and `FilterTypes` are not in the `Plugin` namespace and are from `@libs/filterInputs` file:
```ts
import { FilterTypes, Filters } from "@libs/filterInputs";
```
There are 2 main objects when using filters:
- [Filter definition](#filter-definition-object) object
- [FilterValues](#filterValue) object
#### Filter definition object
This is the user-defined object that defines strictly what filters are available in the "filter" menu in app.
Every property of this object is a different filter. The key of the object is the name that will be used to reference this filter's value in the [FilterValues](#filtervalues-object) object
```ts
filters = {
order: {<FilterProperties>}
} satisfies Filters;
// accessible in popularNovels as
options.filters.order
```
> [!CAUTION]
> Do not forget to add `satisfies Filters` after the Filter definition object!
##### FilterProperties
| Name | Type | Required | Desciption |
| ------- | ---------------------------- | ------------- | ------------------------------------------------------------------ |
| label | `string` | yes | in-app label |
| type | `FilterTypes` | yes | type of the filter |
| value | [check types](#filter-types) | yes | Default value for this filter and the starting filter state in-app |
| options | [check types](#filter-types) | in some types | The options available in the given type |
###### Example
```ts
filters = {
genre: {
type: FilterTypes.CheckboxGroup,
label: "Genres",
value: [],
options: [
{ label: "Isekai", value: "isekai" },
{ label: "Romance", value: "romans" },
],
},
} satisfies Filters;
```
##### Filter types
Types of filters supported
| FilterType | Description | `value` | `options` |
| ------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------- | ----------------------------------------------- |
| `Picker` | A spinner for choosing one of the choices provided in `options` | `string` the picked value | [Picker](#picker-options) options |
| `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 |
###### Picker options
```ts
options: [
{
label: "default", // in-app label
value: "", // in-code value
},
{
label: "Value ABC",
value: "abc",
},
];
```
###### CheckboxGroup options
```ts
options: [
{
label: "Value ABC", // in-app label
value: "abc", // in-code value
},
{
label: "Value DEF",
value: "def",
},
];
```
#### 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.
The keys of the filter values correspond to Filter definition keys
```ts
// Filter definition object
filters = { abc: {} } satisfies Filters;
// then
options.filters; // FilterValues
options.filters.abc; // FilterValue for abc filter
```
##### FilterValue
Properties of FilterValue:
- `type: FilterType` type of the filter
- `value` value dependent on [FilterTypes](#filter-types)
```ts
options.filters.abc.value; // value of the filter
options.filters.abc.type; // type of the filter
```
###### ExcludableCheckboxGroupValue object
```ts
{
included: string[], // options with selected selected
excluded: string[] // options with excluded selected
}
```
### Using Cheerio
### Custom fetching functions
+89
View File
@@ -0,0 +1,89 @@
import { fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { load as loadCheerio } from "cheerio";
import { defaultCover } from "@libs/defaultCover";
import { NovelStatus } from "@libs/novelStatus";
// import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
class TemplatePlugin implements Plugin.PluginBase {
id = "";
name = "";
icon = "";
site = "";
version = "1.0.0";
filters: Filters | undefined = undefined;
async popularNovels(
pageNo: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
/** Add your fetching code here */
novels.push({
name: "Novel1",
url: "example.com/novel1",
cover: defaultCover,
});
return novels;
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const novel: Plugin.SourceNovel = {
url: novelUrl,
};
// TODO: get here data from the site and
// un-comment and fill-in the relevant fields
// novel.name = "";
// novel.artist = "";
// novel.author = "";
novel.cover = defaultCover;
// novel.genres = "";
// novel.status = NovelStatus.Completed;
// novel.summary = "";
let chapters: Plugin.ChapterItem[] = [];
// TODO: here parse the chapter list
// TODO: add each chapter to the list using
const chapter: Plugin.ChapterItem = {
name: "",
url: "",
releaseTime: "",
chapterNumber: 0,
};
chapters.push(chapter);
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
// parse chapter text here
const chapterText = "";
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number
): Promise<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
// get novels using the search term
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
// if your plugin has images and they won't load
// this is the function to fiddle with
return fetchFile(url);
}
}
export default new TemplatePlugin();
+30
View File
@@ -0,0 +1,30 @@
# Quick start
1. [Requirements](#requirements)
2. [Single plugin guide](#quick-guide)
3. [Multi-src guide](#creating-multi-src-plugins)
### Requirements
- [git](https://git-scm.com/doc/ext) basics
- Typescript or Javascript basics
- Node >=18
- Installing the dependencies with `npm i`
### Guide
1. Create plugin script in `/plugins` [<span style="font-size: 0.8rem;">(learn more)</span>](#creating-plugin-script)
2. Copy code from [plugin-template.ts](./plugin-template.ts)
3. Start coding [<span style="font-size:0.8rem">(documentation)</span>](./docs.md)
#### Creating plugin script
1. Remember to create your plugin inside the language folder corresponding to the language of the novels
2. File should have the `.ts` extension
Example `plugins/english/nobleMTL.ts`
3. Add an icon to `icons/src/<lang>/<plugin-name>/icon.png`
> [!WARNING]
> Icon size should be 96x96px!
### Creating multi-source plugins
View File