diff --git a/.github/workflows/update_plugins_host.yml b/.github/workflows/update_plugins_host.yml
index 391c5d8..2a4721c 100644
--- a/.github/workflows/update_plugins_host.yml
+++ b/.github/workflows/update_plugins_host.yml
@@ -23,6 +23,5 @@ jobs:
run: |
git config user.name rajarsheechatterjee
git config user.email rajarshee.adm@gmail.com
- chmod +x ./host.sh
- ./host.sh
+ npm run host-linux
shell: bash
diff --git a/README.md b/README.md
index bbaaf63..c67a3da 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,42 @@
-
-
# LNReader Plugins
-
-
-
-
-
-
-
-
-
-
-
-
-
-Repository to host plugins and related issues, and requests for [LNReader](https://github.com/LNReader/lnreader).
+Repository to host plugins and related issues, and requests for
+[LNReader](https://github.com/LNReader/lnreader).
## Installing
-- Prerequisites: Nodejs >= 18
+- Prerequisites: Nodejs >= 18
+
1. `npm install`
+
## Contributing
-1. Choose your language in [plugins](./plugins)
-2. Write your scripts
-
-
-
-## Examples:
-+ [Hako](./plugins/vietnamese/LNHako.ts)
-+ Multisrc: [multisrc](./scripts/multisrc)
+- [Quick start](./docs/quickstart.md)
+- [Documentation](./docs/docs.md)
## Testing
-- If you are making a [multisrc](./scripts/multisrc): `npm run generate`
-- `npm start`
-- Open http://localhost:3000 and test
-### If you want to test in app side.
+#### via the testing website
-- `./host.sh`
+1. Run `npm start` and open `localhost:3000` to test!
-- Change these in [pluginManager.ts](https://github.com/LNReader/lnreader/blob/master/src/plugins/pluginManager.ts) (app repo) to yours
+[Detailed tutorial for testing website](./docs/website-tutorial.md)
-
+#### via an app
+
+1. Run `npm run host-linux` or `npm run host-windows` (depending on your operating system)
+2. Change the values in [pluginManager.ts](https://github.com/LNReader/lnreader/blob/master/src/plugins/pluginManager.ts) (in-app) to yours
```ts
-const githubUsername = 'LNReader';
-const githubRepository = 'lnreader-sources';
+const githubUsername = "LNReader";
+const githubRepository = "lnreader-sources";
```
-----------
-
+---
The developer of this application does not have any affiliation with the content providers available.
diff --git a/docs/docs.md b/docs/docs.md
new file mode 100644
index 0000000..fb5694b
--- /dev/null
+++ b/docs/docs.md
@@ -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. `..`
+
+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
+ ): Promise
+```
+
+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
+ ): Promise {
+ 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
+
+- `showLatestNovels: boolean` flag set when opened with `Latest` button
+
+- `filters: FilterValues` 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
+```
+
+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 {
+ 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
+```
+
+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{
+ return "No chapter here
";
+ }
+ ...
+}
+```
+
+#### PluginBase::searchNovels
+
+Function that is used to find Novels in the source
+
+```ts
+async searchNovels(searchTerm: string, pageNo: number): Promise
+```
+
+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 {
+ 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
+```
+
+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 {
+ 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 |
+| -------------------------------- | -------- | -------- | ------------------------------------------ |
+| url
| `string` | yes | The url to the site |
+| name
| `string` | yes | The name of the novel shown in the library |
+| cover
| `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: {}
+} 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
diff --git a/docs/plugin-template.ts b/docs/plugin-template.ts
new file mode 100644
index 0000000..5d61a22
--- /dev/null
+++ b/docs/plugin-template.ts
@@ -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
+ ): Promise {
+ 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 {
+ 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 {
+ // parse chapter text here
+ const chapterText = "";
+ return chapterText;
+ }
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number
+ ): Promise {
+ let novels: Plugin.NovelItem[] = [];
+
+ // get novels using the search term
+
+ return novels;
+ }
+ async fetchImage(url: string): Promise {
+ // if your plugin has images and they won't load
+ // this is the function to fiddle with
+ return fetchFile(url);
+ }
+}
+
+export default new TemplatePlugin();
diff --git a/docs/quickstart.md b/docs/quickstart.md
new file mode 100644
index 0000000..ea36a23
--- /dev/null
+++ b/docs/quickstart.md
@@ -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` [(learn more)](#creating-plugin-script)
+2. Copy code from [plugin-template.ts](./plugin-template.ts)
+3. Start coding [(documentation)](./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///icon.png`
+
+> [!WARNING]
+> Icon size should be 96x96px!
+
+### Creating multi-source plugins
diff --git a/docs/website-tutorial.md b/docs/website-tutorial.md
new file mode 100644
index 0000000..e69de29
diff --git a/host.ps1 b/host.ps1
new file mode 100644
index 0000000..98c81b7
--- /dev/null
+++ b/host.ps1
@@ -0,0 +1,32 @@
+$current=$(git rev-parse --abbrev-ref HEAD)
+$dist='dist'
+$exists=$(git show-ref refs/heads/$dist)
+
+echo $current
+echo $exists
+
+if ($exists){
+ git checkout $dist
+}else{
+ ## Make a new one
+ git checkout -b $dist
+}
+
+if(-Not $?){
+ # If checkout failed
+ echo "=========="
+ echo "Could not checkout branch dist! See the error above and fix it!"
+ exit 1
+}
+
+git merge $current --strategy-option theirs
+
+npm run generate
+npm run json
+git add .
+git add -f .dist .js/plugins
+git commit -m "Update plugins host"
+git push -f origin $dist
+
+git checkout $current
+
diff --git a/host.sh b/host.sh
index 4cf2d17..3118428 100755
--- a/host.sh
+++ b/host.sh
@@ -8,6 +8,13 @@ else
git checkout -b $dist
fi
+if [ $? -eq 1 ]; then
+ # If checkout failed
+ echo "=========="
+ echo "Could not checkout branch dist! See the error above and fix it!"
+ exit 1
+fi
+
git merge $current --strategy-option theirs
npm run generate
@@ -18,3 +25,4 @@ git commit -m "Update plugins host"
git push -f origin $dist
git checkout $current
+
diff --git a/package.json b/package.json
index 1991035..001b213 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
"generate": "ts-node ./scripts/multisrc/generate.ts",
"clearMulti": "ts-node ./scripts/clearMultisrc.ts",
"less": "npx less ./test_web/static/css/index.less ./test_web/static/css/index.css",
- "host": "host.sh"
+ "host-linux": "chmod +x ./host.sh && ./host.sh",
+ "host-windows": "powershell ./host.ps1"
},
"author": "LNReader",
"license": "MIT",
diff --git a/plugins/japanese/Syosetu.ts b/plugins/japanese/Syosetu.ts
index e9b2981..0ba973a 100644
--- a/plugins/japanese/Syosetu.ts
+++ b/plugins/japanese/Syosetu.ts
@@ -7,14 +7,6 @@ import { Filters } from "@libs/filterInputs";
// const isUrlAbsolute = require('@libs/isAbsoluteUrl');
// const parseDate = require('@libs/parseDate');
-const pluginId = "yomou.syosetu";
-
-export const id = pluginId;
-export const name = "Syosetu";
-export const icon = "src/jp/syosetu/icon.png";
-export const version = "1.0.0";
-export const site = "https://yomou.syosetu.com/";
-
class Syosetu implements Plugin.PluginBase {
id = "yomou.syosetu";
name = "Syosetu";
diff --git a/types/plugin.ts b/types/plugin.ts
index 1c0ac99..9bf118e 100644
--- a/types/plugin.ts
+++ b/types/plugin.ts
@@ -22,7 +22,9 @@ export namespace Plugin {
export interface SourceNovel {
url: string;
name?: string;
+ /** Novel cover absolute URL */
cover?: string;
+ /** Comma separated genre list */
genres?: string;
summary?: string;
author?: string;
@@ -68,7 +70,7 @@ export namespace Plugin {
/**
*
* @param url Image url
- * @returns {string} Base64 of image
+ * @returns {Promise} Base64 of image
* @example
* ```ts
* const headers = {