From 211f30d5b264d8f88f5dab981c4a5a4cb87305d1 Mon Sep 17 00:00:00 2001 From: Marcin Majewski Date: Fri, 2 Feb 2024 04:23:50 +0100 Subject: [PATCH] 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 --- .github/workflows/update_plugins_host.yml | 3 +- README.md | 53 +-- docs/docs.md | 527 ++++++++++++++++++++++ docs/plugin-template.ts | 89 ++++ docs/quickstart.md | 30 ++ docs/website-tutorial.md | 0 host.ps1 | 32 ++ host.sh | 8 + package.json | 3 +- plugins/japanese/Syosetu.ts | 8 - types/plugin.ts | 4 +- 11 files changed, 709 insertions(+), 48 deletions(-) create mode 100644 docs/docs.md create mode 100644 docs/plugin-template.ts create mode 100644 docs/quickstart.md create mode 100644 docs/website-tutorial.md create mode 100644 host.ps1 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 - -

- - - GitHub issues by-label - - - GitHub issues by-label - - -

- - -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 = {