From 1edf284709371f5813765c3a2f5844f743600d3b Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 19:04:48 +0800 Subject: [PATCH] Add doc --- README.md | 2 +- doc/comic_source.md | 655 ++++++++++++++++++++++++++++++++++++++++++++ doc/import_comic.md | 59 ++++ doc/js_api.md | 513 ++++++++++++++++++++++++++++++++++ 4 files changed, 1228 insertions(+), 1 deletion(-) create mode 100644 doc/comic_source.md create mode 100644 doc/import_comic.md create mode 100644 doc/js_api.md diff --git a/README.md b/README.md index 8ba4002..ae5246a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ A comic reader that support reading local and network comics. ## Create a new comic source -See [venera-configs](https://github.com/venera-app/venera-configs) +See [Comic Source](doc/comic_source.md) ## Thanks diff --git a/doc/comic_source.md b/doc/comic_source.md new file mode 100644 index 0000000..9d5d830 --- /dev/null +++ b/doc/comic_source.md @@ -0,0 +1,655 @@ +# Comic Source + +## Introduction + +Venera is a comic reader that can read comics from various sources. + +All comic sources are written in javascript. +Venera uses [flutter_qjs](https://github.com/wgh136/flutter_qjs) as js engine which is forked from [ekibun](https://github.com/ekibun/flutter_qjs). + +This document will describe how to write a comic source for Venera. + +## Preparation + +- Install Venera. Using flutter to run the project is recommended since it's easier to debug. +- An editor that supports javascript. +- Download template and venera javascript api from [here](https://github.com/venera-app/venera-configs). + +## Start Writing + +The template contains detailed comments and examples. You can refer to it when writing your own comic source. + +Here is a brief introduction to the template: + +> Note: Javascript api document is [here](js_api.md). + +### Write basic information + +```javascript +class NewComicSource extends ComicSource { + // Note: The fields which are marked as [Optional] should be removed if not used + + // name of the source + name = "" + + // unique id of the source + key = "" + + version = "1.0.0" + + minAppVersion = "1.0.0" + + // update url + url = "" +// ... +} +``` + +In this part, you need to do the following: +- Change the class name to your source name. +- Fill in the name, key, version, minAppVersion, and url fields. + +### init function + +```javascript + /** + * [Optional] init function + */ + init() { + + } +``` + +The function will be called when the source is initialized. You can do some initialization work here. + +Remove this function if not used. + +### Account + +```javascript +// [Optional] account related + account = { + /** + * [Optional] login with account and password, return any value to indicate success + * @param account {string} + * @param pwd {string} + * @returns {Promise} + */ + login: async (account, pwd) => { + + }, + + /** + * [Optional] login with webview + */ + loginWithWebview: { + url: "", + /** + * check login status + * @param url {string} - current url + * @param title {string} - current title + * @returns {boolean} - return true if login success + */ + checkStatus: (url, title) => { + + }, + /** + * [Optional] Callback when login success + */ + onLoginSuccess: () => { + + }, + }, + + /** + * [Optional] login with cookies + * Note: If `this.account.login` is implemented, this will be ignored + */ + loginWithCookies: { + fields: [ + "ipb_member_id", + "ipb_pass_hash", + "igneous", + "star", + ], + /** + * Validate cookies, return false if cookies are invalid. + * + * Use `Network.setCookies` to set cookies before validate. + * @param values {string[]} - same order as `fields` + * @returns {Promise} + */ + validate: async (values) => { + + }, + }, + + /** + * logout function, clear account related data + */ + logout: () => { + + }, + + // {string?} - register url + registerWebsite: null + } +``` + +In this part, you can implement login, logout, and register functions. + +Remove this part if not used. + +### Explore page + +```javascript + // explore page list + explore = [ + { + // title of the page. + // title is used to identify the page, it should be unique + title: "", + + /// multiPartPage or multiPageComicList or mixed + type: "multiPartPage", + + /** + * load function + * @param page {number | null} - page number, null for `singlePageWithMultiPart` type + * @returns {{}} + * - for `multiPartPage` type, return {title: string, comics: Comic[], viewMore: string?}[] + * - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number} + * - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?} + */ + load: async (page) => { + + }, + + /** + * Only use for `multiPageComicList` type. + * `loadNext` would be ignored if `load` function is implemented. + * @param next {string | null} - next page token, null if first page + * @returns {Promise<{comics: Comic[], next: string?}>} - next is null if no next page. + */ + loadNext(next) {}, + } + ] +``` + +In this part, you can implement the explore page. + +A comic source can have multiple explore pages. + +There are three types of explore pages: +- multiPartPage: An explore page contains multiple parts, each part contains multiple comics. +- multiPageComicList: An explore page contains multiple comics, the comics are loaded page by page. +- mixed: An explore page contains multiple parts, each part can be a list of comics or a block of comics which have a title and a view more button. + +### Category Page + +```javascript + // categories + category = { + /// title of the category page, used to identify the page, it should be unique + title: "", + parts: [ + { + // title of the part + name: "Theme", + + // fixed or random + // if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time + type: "fixed", + + // number of comics to display at the same time + // randomNumber: 5, + + categories: ["All", "Adventure", "School"], + + // category or search + // if `category`, use categoryComics.load to load comics + // if `search`, use search.load to load comics + itemType: "category", + + // [Optional] {string[]?} must have same length as categories, used to provide loading param for each category + categoryParams: ["all", "adventure", "school"], + + // [Optional] {string} cannot be used with `categoryParams`, set all category params to this value + groupParam: null, + } + ], + // enable ranking page + enableRankingPage: false, + } +``` + +Category page is a static page that contains multiple parts, each part contains multiple categories. + +A comic source can only have one category page. + +### Category Comics Page + +```javascript + /// category comic loading related + categoryComics = { + /** + * load comics of a category + * @param category {string} - category name + * @param param {string?} - category param + * @param options {string[]} - options from optionList + * @param page {number} - page number + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + load: async (category, param, options, page) => { + + }, + // provide options for category comic loading + optionList: [ + { + // For a single option, use `-` to separate the value and text, left for value, right for text + options: [ + "newToOld-New to Old", + "oldToNew-Old to New" + ], + // [Optional] {string[]} - show this option only when the value not in the list + notShowWhen: null, + // [Optional] {string[]} - show this option only when the value in the list + showWhen: null + } + ], + ranking: { + // For a single option, use `-` to separate the value and text, left for value, right for text + options: [ + "day-Day", + "week-Week" + ], + /** + * load ranking comics + * @param option {string} - option from optionList + * @param page {number} - page number + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + load: async (option, page) => { + + } + } + } +``` + +When user clicks on a category, the category comics page will be displayed. + +This part is used to load comics of a category. + +### Search + +```javascript + /// search related + search = { + /** + * load search result + * @param keyword {string} + * @param options {(string | null)[]} - options from optionList + * @param page {number} + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + load: async (keyword, options, page) => { + + }, + + /** + * load search result with next page token. + * The field will be ignored if `load` function is implemented. + * @param keyword {string} + * @param options {(string)[]} - options from optionList + * @param next {string | null} + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + loadNext: async (keyword, options, next) => { + + }, + + // provide options for search + optionList: [ + { + // [Optional] default is `select` + // type: select, multi-select, dropdown + // For select, there is only one selected value + // For multi-select, there are multiple selected values or none. The `load` function will receive a json string which is an array of selected values + // For dropdown, there is one selected value at most. If no selected value, the `load` function will receive a null + type: "select", + // For a single option, use `-` to separate the value and text, left for value, right for text + options: [ + "0-time", + "1-popular" + ], + // option label + label: "sort", + // default selected options + default: null, + } + ], + + // enable tags suggestions + enableTagsSuggestions: false, + } +``` + +This part is used to load search results. + +`load` and `loadNext` functions are used to load search results. +If `load` function is implemented, `loadNext` function will be ignored. + +### Favorites + +```javascript + // favorite related + favorites = { + // whether support multi folders + multiFolder: false, + /** + * add or delete favorite. + * throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite + * @param comicId {string} + * @param folderId {string} + * @param isAdding {boolean} - true for add, false for delete + * @param favoriteId {string?} - [Comic.favoriteId] + * @returns {Promise} - return any value to indicate success + */ + addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => { + + }, + /** + * load favorite folders. + * throw `Login expired` to indicate login expired, App will automatically re-login retry. + * if comicId is not null, return favorite folders which contains the comic. + * @param comicId {string?} + * @returns {Promise<{folders: {[p: string]: string}, favorited: string[]}>} - `folders` is a map of folder id to folder name, `favorited` is a list of folder id which contains the comic + */ + loadFolders: async (comicId) => { + + }, + /** + * add a folder + * @param name {string} + * @returns {Promise} - return any value to indicate success + */ + addFolder: async (name) => { + + }, + /** + * delete a folder + * @param folderId {string} + * @returns {Promise} - return any value to indicate success + */ + deleteFolder: async (folderId) => { + + }, + /** + * load comics in a folder + * throw `Login expired` to indicate login expired, App will automatically re-login retry. + * @param page {number} + * @param folder {string?} - folder id, null for non-multi-folder + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + loadComics: async (page, folder) => { + + }, + /** + * load comics with next page token + * @param next {string | null} - next page token, null for first page + * @param folder {string} + * @returns {Promise<{comics: Comic[], next: string?}>} + */ + loadNext: async (next, folder) => { + + }, + } +``` + +This part is used to manage network favorites of the source. + +`load` and `loadNext` functions are used to load search results. +If `load` function is implemented, `loadNext` function will be ignored. + +### Comic Details + +```javascript + /// single comic related + comic = { + /** + * load comic info + * @param id {string} + * @returns {Promise} + */ + loadInfo: async (id) => { + + }, + /** + * [Optional] load thumbnails of a comic + * + * To render a part of an image as thumbnail, return `${url}@x=${start}-${end}&y=${start}-${end}` + * - If width is not provided, use full width + * - If height is not provided, use full height + * @param id {string} + * @param next {string?} - next page token, null for first page + * @returns {Promise<{thumbnails: string[], next: string?}>} - `next` is next page token, null for no more + */ + loadThumbnails: async (id, next) => { + + }, + + /** + * rate a comic + * @param id + * @param rating {number} - [0-10] app use 5 stars, 1 rating = 0.5 stars, + * @returns {Promise} - return any value to indicate success + */ + starRating: async (id, rating) => { + + }, + + /** + * load images of a chapter + * @param comicId {string} + * @param epId {string?} + * @returns {Promise<{images: string[]}>} + */ + loadEp: async (comicId, epId) => { + + }, + /** + * [Optional] provide configs for an image loading + * @param url + * @param comicId + * @param epId + * @returns {ImageLoadingConfig | Promise} + */ + onImageLoad: (url, comicId, epId) => { + return {} + }, + /** + * [Optional] provide configs for a thumbnail loading + * @param url {string} + * @returns {ImageLoadingConfig | Promise} + * + * `ImageLoadingConfig.modifyImage` and `ImageLoadingConfig.onLoadFailed` will be ignored. + * They are not supported for thumbnails. + */ + onThumbnailLoad: (url) => { + return {} + }, + /** + * [Optional] like or unlike a comic + * @param id {string} + * @param isLike {boolean} - true for like, false for unlike + * @returns {Promise} + */ + likeComic: async (id, isLike) => { + + }, + /** + * [Optional] load comments + * + * Since app version 1.0.6, rich text is supported in comments. + * Following html tags are supported: ['a', 'b', 'i', 'u', 's', 'br', 'span', 'img']. + * span tag supports style attribute, but only support font-weight, font-style, text-decoration. + * All images will be placed at the end of the comment. + * Auto link detection is enabled, but only http/https links are supported. + * @param comicId {string} + * @param subId {string?} - ComicDetails.subId + * @param page {number} + * @param replyTo {string?} - commentId to reply, not null when reply to a comment + * @returns {Promise<{comments: Comment[], maxPage: number?}>} + */ + loadComments: async (comicId, subId, page, replyTo) => { + + }, + /** + * [Optional] send a comment, return any value to indicate success + * @param comicId {string} + * @param subId {string?} - ComicDetails.subId + * @param content {string} + * @param replyTo {string?} - commentId to reply, not null when reply to a comment + * @returns {Promise} + */ + sendComment: async (comicId, subId, content, replyTo) => { + + }, + /** + * [Optional] like or unlike a comment + * @param comicId {string} + * @param subId {string?} - ComicDetails.subId + * @param commentId {string} + * @param isLike {boolean} - true for like, false for unlike + * @returns {Promise} + */ + likeComment: async (comicId, subId, commentId, isLike) => { + + }, + /** + * [Optional] vote a comment + * @param id {string} - comicId + * @param subId {string?} - ComicDetails.subId + * @param commentId {string} - commentId + * @param isUp {boolean} - true for up, false for down + * @param isCancel {boolean} - true for cancel, false for vote + * @returns {Promise} - new score + */ + voteComment: async (id, subId, commentId, isUp, isCancel) => { + + }, + // {string?} - regex string, used to identify comic id from user input + idMatch: null, + /** + * [Optional] Handle tag click event + * @param namespace {string} + * @param tag {string} + * @returns {{action: string, keyword: string, param: string?}} + */ + onClickTag: (namespace, tag) => { + + }, + /** + * [Optional] Handle links + */ + link: { + /** + * set accepted domains + */ + domains: [ + 'example.com' + ], + /** + * parse url to comic id + * @param url {string} + * @returns {string | null} + */ + linkToId: (url) => { + + } + }, + // enable tags translate + enableTagsTranslate: false, + } + +``` + +This part is used to load comic details. + +### Settings + +```javascript + /* + [Optional] settings related + Use this.loadSetting to load setting + ``` + let setting1Value = this.loadSetting('setting1') + console.log(setting1Value) + ``` + */ + settings = { + setting1: { + // title + title: "Setting1", + // type: input, select, switch + type: "select", + // options + options: [ + { + // value + value: 'o1', + // [Optional] text, if not set, use value as text + text: 'Option 1', + }, + ], + default: 'o1', + }, + setting2: { + title: "Setting2", + type: "switch", + default: true, + }, + setting3: { + title: "Setting3", + type: "input", + validator: null, // string | null, regex string + default: '', + }, + setting4: { + title: "Setting4", + type: "callback", + buttonText: "Click me", + /** + * callback function + * + * If the callback function returns a Promise, the button will show a loading indicator until the promise is resolved. + * @returns {void | Promise} + */ + callback: () => { + // do something + } + } + } +``` + +This part is used to provide settings for the source. + + +### Translations + +```javascript + // [Optional] translations for the strings in this config + translation = { + 'zh_CN': { + 'Setting1': '设置1', + 'Setting2': '设置2', + 'Setting3': '设置3', + }, + 'zh_TW': {}, + 'en': {} + } +``` + +This part is used to provide translations for the source. + +> Note: strings in the UI api will not be translated automatically. You need to translate them manually. \ No newline at end of file diff --git a/doc/import_comic.md b/doc/import_comic.md new file mode 100644 index 0000000..5cd00d5 --- /dev/null +++ b/doc/import_comic.md @@ -0,0 +1,59 @@ +# Import Comic + +## Introduction + +Venera supports importing comics from local files. +However, the comic files must be in a specific format. + +## Comic Directory + +A directory considered as a comic directory only if it follows the following two types of structure: + +The file name can be anything, but the extension must be a valid image extension. + +The page order is determined by the file name. App will sort the files by name and display them in that order. + +Cover image is optional. +If there is a file named `cover.[ext]` in the directory, it will be considered as the cover image. +Otherwise, the first image will be considered as the cover image. + +### Without Chapter + +``` +comic_directory +├── cover.[ext] +├── img1.[ext] +├── img2.[ext] +├── img3.[ext] +├── ... +``` + +### With Chapter + +``` +comic_directory +├── cover.[ext] +├── chapter1 +│ ├── img1.[ext] +│ ├── img2.[ext] +│ ├── img3.[ext] +│ ├── ... +├── chapter2 +│ ├── img1.[ext] +│ ├── img2.[ext] +│ ├── img3.[ext] +│ ├── ... +├── ... +``` + +## Archive + +Venera supports importing comics from archive files. + +The archive file must follow [Comic Book Archive](https://en.wikipedia.org/wiki/Comic_book_archive_file) format. + +Currently, Venera supports the following archive formats: +- `.cbz` +- `.cb7` +- `.zip` +- `.7z` \ No newline at end of file diff --git a/doc/js_api.md b/doc/js_api.md new file mode 100644 index 0000000..09a4f35 --- /dev/null +++ b/doc/js_api.md @@ -0,0 +1,513 @@ +# Javascript API + +## Overview + +The Javascript API is a set of functions that used to interact application. + +There are following parts in the API: +- [Convert](#Convert) +- [Network](#Network) +- [Html](#Html) +- [UI](#UI) +- [Utils](#Utils) +- [Types](#Types) + + +## Convert + +Convert is a set of functions that used to convert data between different types. + +### `Convert.encodeUtf8(str: string): ArrayBuffer` + +Convert a string to an ArrayBuffer. + +### `Convert.decodeUtf8(value: ArrayBuffer): string` + +Convert an ArrayBuffer to a string. + +### `Convert.encodeBase64(value: ArrayBuffer): string` + +Convert an ArrayBuffer to a base64 string. + +### `Convert.decodeBase64(value: string): ArrayBuffer` + +Convert a base64 string to an ArrayBuffer. + +### `Convert.md5(value: ArrayBuffer): ArrayBuffer` + +Calculate the md5 hash of an ArrayBuffer. + +### `Convert.sha1(value: ArrayBuffer): ArrayBuffer` + +Calculate the sha1 hash of an ArrayBuffer. + +### `Convert.sha256(value: ArrayBuffer): ArrayBuffer` + +Calculate the sha256 hash of an ArrayBuffer. + +### `Convert.sha512(value: ArrayBuffer): ArrayBuffer` + +Calculate the sha512 hash of an ArrayBuffer. + +### `Convert.hmac(key: ArrayBuffer, value: ArrayBuffer, hash: string): ArrayBuffer` + +Calculate the hmac hash of an ArrayBuffer. + +### `Convert.hmacString(key: ArrayBuffer, value: ArrayBuffer, hash: string): string` + +Calculate the hmac hash of an ArrayBuffer and return a string. + +### `Convert.decryptAesEcb(value: ArrayBuffer, key: ArrayBuffer): ArrayBuffer` + +Decrypt an ArrayBuffer with AES ECB mode. + +### `Convert.decryptAesCbc(value: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer): ArrayBuffer` + +Decrypt an ArrayBuffer with AES CBC mode. + +### `Convert.decryptAesCfb(value: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer): ArrayBuffer` + +Decrypt an ArrayBuffer with AES CFB mode. + +### `Convert.decryptAesOfb(value: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer): ArrayBuffer` + +Decrypt an ArrayBuffer with AES OFB mode. + +### `Convert.decryptRsa(value: ArrayBuffer, key: ArrayBuffer): ArrayBuffer` + +Decrypt an ArrayBuffer with RSA. + +### `Convert.hexEncode(value: ArrayBuffer): string` + +Convert an ArrayBuffer to a hex string. + +## Network + +Network is a set of functions that used to send network requests and manage network resources. + +### `Network.fetchBytes(method: string, url: string, headers: object, data: ArrayBuffer): Promise<{status: number, headers: object, body: ArrayBuffer}>` + +Send a network request and return the response as an ArrayBuffer. + +### `Network.sendRequest(method: string, url: string, headers: object, data: ArrayBuffer): Promise<{status: number, headers: object, body: string}>` + +Send a network request and return the response as a string. + +### `Network.get(url: string, headers: object): Promise<{status: number, headers: object, body: string}>` + +Send a GET request and return the response as a string. + +### `Network.post(url: string, headers: object, data: ArrayBuffer): Promise<{status: number, headers: object, body: string}>` + +Send a POST request and return the response as a string. + +### `Network.put(url: string, headers: object, data: ArrayBuffer): Promise<{status: number, headers: object, body: string}>` + +Send a PUT request and return the response as a string. + +### `Network.delete(url: string, headers: object): Promise<{status: number, headers: object, body: string}>` + +Send a DELETE request and return the response as a string. + +### `Network.patch(url: string, headers: object, data: ArrayBuffer): Promise<{status: number, headers: object, body: string}>` + +Send a PATCH request and return the response as a string. + +### `Network.setCookies(url: string, cookies: Cookie[]): void` + +Set cookies for a specific url. + +### `Network.getCookies(url: string): Cookie[]` + +Get cookies for a specific url. + +### `Network.deleteCookies(url: string): void` + +Delete cookies for a specific url. + +### `fetch` + +The fetch function is a wrapper of the `Network.fetchBytes` function. Same as the `fetch` function in the browser. + +## Html + +Api for parsing HTML. + +### `new HtmlDocument(html: string): HtmlDocument` + +Create a HtmlDocument object from a html string. + +### `HtmlDocument.querySelector(selector: string): HtmlElement` + +Find the first element that matches the selector. + +### `HtmlDocument.querySelectorAll(selector: string): HtmlElement[]` + +Find all elements that match the selector. + +### `HtmlDocument.getElementById(id: string): HtmlElement` + +Find the element with the id. + +### `HtmlDocument.dispose(): void` + +Dispose the HtmlDocument object. + +### `HtmlElement.querySelector(selector: string): HtmlElement` + +Find the first element that matches the selector. + +### `HtmlElement.querySelectorAll(selector: string): HtmlElement[]` + +Find all elements that match the selector. + +### `HtmlElement.getElementById(id: string): HtmlElement` + +Find the element with the id. + +### `get HtmlElement.text(): string` + +Get the text content of the element. + +### `get HtmlElement.attributes(): object` + +Get the attributes of the element. + +### `get HtmlElement.children(): HtmlElement[]` + +Get the children + +### `get HtmlElement.nodes(): HtmlNode[]` + +Get the child nodes + +### `get HtmlElement.parent(): HtmlElement | null` + +Get the parent element + +### `get HtmlElement.innerHtml(): string` + +Get the inner html + +### `get HtmlElement.classNames(): string[]` + +Get the class names + +### `get HtmlElement.id(): string | null` + +Get the id + +### `get HtmlElement.localName(): string` + +Get the local name + +### `get HtmlElement.previousSibling(): HtmlElement | null` + +Get the previous sibling + +### `get HtmlElement.nextSibling(): HtmlElement | null` + +Get the next sibling + +### `get HtmlNode.type(): string` + +Get the node type ("text", "element", "comment", "document", "unknown") + +### `HtmlNode.toElement(): HtmlElement | null` + +Convert the node to an element + +### `get HtmlNode.text(): string` + +Get the text content of the node + +## UI + +### `UI.showMessage(message: string): void` + +Show a message. + +### `UI.showDialog(title: string, content: string, actions: {text: string, callback: () => void | Promise, style: "text"|"filled"|"danger"}[]): void` + +Show a dialog. Any action will close the dialog. + +### `UI.launchUrl(url: string): void` + +Open a url in external browser. + +### `UI.showLoading(onCancel: () => void | null | undefined): number` + +Show a loading dialog. + +### `UI.cancelLoading(id: number): void` + +Cancel a loading dialog. + +### `UI.showInputDialog(title: string, validator: (string) => string | null | undefined): string | null` + +Show an input dialog. + +### `UI.showSelectDialog(title: string, options: string[], initialIndex?: number): number | null` + +Show a select dialog. + +## Utils + +### `createUuid(): string` + +create a time-based uuid. + +### `randomInt(min: number, max: number): number` + +Generate a random integer between min and max. + +### `randomDouble(min: number, max: number): number` + +Generate a random double between min and max. + +### console + +Send log to application console. Same api as the browser console. + +## Types + +### `Cookie` + +```javascript +/** + * Create a cookie object. + * @param name {string} + * @param value {string} + * @param domain {string} + * @constructor + */ +function Cookie({name, value, domain}) { + this.name = name; + this.value = value; + this.domain = domain; +} +``` + +### `Comic` + +```javascript +/** + * Create a comic object + * @param id {string} + * @param title {string} + * @param subtitle {string} + * @param subTitle {string} - equal to subtitle + * @param cover {string} + * @param tags {string[]} + * @param description {string} + * @param maxPage {number?} + * @param language {string?} + * @param favoriteId {string?} - Only set this field if the comic is from favorites page + * @param stars {number?} - 0-5, double + * @constructor + */ +function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage, language, favoriteId, stars}) { + this.id = id; + this.title = title; + this.subtitle = subtitle; + this.subTitle = subTitle; + this.cover = cover; + this.tags = tags; + this.description = description; + this.maxPage = maxPage; + this.language = language; + this.favoriteId = favoriteId; + this.stars = stars; +} +``` + +### `ComicDetails` +```javascript +/** + * Create a comic details object + * @param title {string} + * @param subtitle {string} + * @param subTitle {string} - equal to subtitle + * @param cover {string} + * @param description {string?} + * @param tags {Map | {} | null | undefined} + * @param chapters {Map | {} | null | undefined} - key: chapter id, value: chapter title + * @param isFavorite {boolean | null | undefined} - favorite status. If the comic source supports multiple folders, this field should be null + * @param subId {string?} - a param which is passed to comments api + * @param thumbnails {string[]?} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails + * @param recommend {Comic[]?} - related comics + * @param commentCount {number?} + * @param likesCount {number?} + * @param isLiked {boolean?} + * @param uploader {string?} + * @param updateTime {string?} + * @param uploadTime {string?} + * @param url {string?} + * @param stars {number?} - 0-5, double + * @param maxPage {number?} + * @param comments {Comment[]?}- `since 1.0.7` App will display comments in the details page. + * @constructor + */ +function ComicDetails({title, subtitle, subTitle, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage, comments}) { + this.title = title; + this.subtitle = subtitle ?? subTitle; + this.cover = cover; + this.description = description; + this.tags = tags; + this.chapters = chapters; + this.isFavorite = isFavorite; + this.subId = subId; + this.thumbnails = thumbnails; + this.recommend = recommend; + this.commentCount = commentCount; + this.likesCount = likesCount; + this.isLiked = isLiked; + this.uploader = uploader; + this.updateTime = updateTime; + this.uploadTime = uploadTime; + this.url = url; + this.stars = stars; + this.maxPage = maxPage; + this.comments = comments; +} +``` + +### `Comment` +```javascript +/** + * Create a comment object + * @param userName {string} + * @param avatar {string?} + * @param content {string} + * @param time {string?} + * @param replyCount {number?} + * @param id {string?} + * @param isLiked {boolean?} + * @param score {number?} + * @param voteStatus {number?} - 1: upvote, -1: downvote, 0: none + * @constructor + */ +function Comment({userName, avatar, content, time, replyCount, id, isLiked, score, voteStatus}) { + this.userName = userName; + this.avatar = avatar; + this.content = content; + this.time = time; + this.replyCount = replyCount; + this.id = id; + this.isLiked = isLiked; + this.score = score; + this.voteStatus = voteStatus; +} +``` + +### `ImageLoadingConfig` +```javascript +/** + * Create image loading config + * @param url {string?} + * @param method {string?} - http method, uppercase + * @param data {any} - request data, may be null + * @param headers {Object?} - request headers + * @param onResponse {((ArrayBuffer) => ArrayBuffer)?} - modify response data + * @param modifyImage {string?} + * A js script string. + * The script will be executed in a new Isolate. + * A function named `modifyImage` should be defined in the script, which receives an [Image] as the only argument, and returns an [Image].. + * @param onLoadFailed {(() => ImageLoadingConfig)?} - called when the image loading failed + * @constructor + * @since 1.0.5 + * + * To keep the compatibility with the old version, do not use the constructor. Consider creating a new object with the properties directly. + */ +function ImageLoadingConfig({url, method, data, headers, onResponse, modifyImage, onLoadFailed}) { + this.url = url; + this.method = method; + this.data = data; + this.headers = headers; + this.onResponse = onResponse; + this.modifyImage = modifyImage; + this.onLoadFailed = onLoadFailed; +} +``` + +### `ComicSource` +```javascript +class ComicSource { + name = "" + + key = "" + + version = "" + + minAppVersion = "" + + url = "" + + /** + * load data with its key + * @param {string} dataKey + * @returns {any} + */ + loadData(dataKey) { + return sendMessage({ + method: 'load_data', + key: this.key, + data_key: dataKey + }) + } + + /** + * load a setting with its key + * @param key {string} + * @returns {any} + */ + loadSetting(key) { + return sendMessage({ + method: 'load_setting', + key: this.key, + setting_key: key + }) + } + + /** + * save data + * @param {string} dataKey + * @param data + */ + saveData(dataKey, data) { + return sendMessage({ + method: 'save_data', + key: this.key, + data_key: dataKey, + data: data + }) + } + + /** + * delete data + * @param {string} dataKey + */ + deleteData(dataKey) { + return sendMessage({ + method: 'delete_data', + key: this.key, + data_key: dataKey, + }) + } + + /** + * + * @returns {boolean} + */ + get isLogged() { + return sendMessage({ + method: 'isLogged', + key: this.key, + }); + } + + init() { } + + static sources = {} +} +``` \ No newline at end of file