This commit is contained in:
2025-01-20 19:04:48 +08:00
committed by nyne
parent 811fbb04dc
commit 1edf284709
4 changed files with 1228 additions and 1 deletions

View File

@@ -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

655
doc/comic_source.md Normal file
View File

@@ -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<any>}
*/
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<boolean>}
*/
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<any>} - 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<any>} - return any value to indicate success
*/
addFolder: async (name) => {
},
/**
* delete a folder
* @param folderId {string}
* @returns {Promise<void>} - 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<ComicDetails>}
*/
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<any>} - 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<ImageLoadingConfig>}
*/
onImageLoad: (url, comicId, epId) => {
return {}
},
/**
* [Optional] provide configs for a thumbnail loading
* @param url {string}
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
*
* `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<void>}
*/
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<any>}
*/
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<void>}
*/
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<number>} - 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<any>}
*/
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.

59
doc/import_comic.md Normal file
View File

@@ -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`

513
doc/js_api.md Normal file
View File

@@ -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<void>, 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<string, string[]> | {} | null | undefined}
* @param chapters {Map<string, string> | {} | 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 = {}
}
```