update template & add picacg

This commit is contained in:
nyne
2024-10-15 20:55:14 +08:00
parent 1eed103f27
commit f072b27d67
5 changed files with 2056 additions and 0 deletions

View File

@@ -1,3 +1,11 @@
# venera-configs # venera-configs
Configuration file repository for venera Configuration file repository for venera
## Create a new configuration
1. Download `_template_.js`, `_venera_.js`, put them in the same directory
2. Rename `_template_.js` to `your_config_name.js`
3. Edit `your_config_name.js` to your needs.
- The `_template_.js` file contains comments to help you with that.
- The `_venera_.js` is used for code completion in your IDE.

626
_template_.js Normal file
View File

@@ -0,0 +1,626 @@
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 = ""
/**
* [Optional] init function
*/
init() {
}
// [Optional] account related
account = {
/**
* login, return any value to indicate success
* @param account {string}
* @param pwd {string}
* @returns {Promise<any>}
*/
login: async (account, pwd) => {
/*
Use Network to send request
Use this.saveData to save data
`account` and `pwd` will be saved to local storage automatically if login success
```
let res = await Network.post('https://example.com/login', {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
}, `account=${account}&password=${pwd}`)
if(res.status == 200) {
let json = JSON.parse(res.body)
this.saveData('token', json.token)
return 'ok'
}
throw 'Failed to login'
```
*/
},
/**
* logout function, clear account related data
*/
logout: () => {
/*
```
this.deleteData('token')
Network.deleteCookies('https://example.com')
```
*/
},
// {string?} - register url
registerWebsite: null
}
// explore page list
explore = [
{
// title of the page.
// title is used to identify the page, it should be unique
title: "",
/// singlePageWithMultiPart or multiPageComicList
type: "singlePageWithMultiPart",
/**
* load function
* @param page {number | null} - page number, null for `singlePageWithMultiPart` type
* @returns {{}} - for `singlePageWithMultiPart` type, return {[string]: Comic[]}; for `multiPageComicList` type, return {comics: Comic[], maxPage: number}
*/
load: async (page) => {
/*
```
let res = await Network.get("https://example.com")
if (res.status !== 200) {
throw `Invalid status code: ${res.status}`
}
let data = JSON.parse(res.body)
function parseComic(comic) {
// ...
return new Comic({
id: id,
title: title,
subTitle: author,
cover: cover,
tags: tags,
description: description
})
}
let comics = {}
comics["hot"] = data["results"]["recComics"].map(parseComic)
comics["latest"] = data["results"]["newComics"].map(parseComic)
return comics
```
*/
}
}
]
// 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] must have same length as categories, used to provide loading param for each category
categoryParams: ["all", "adventure", "school"]
}
],
// enable ranking page
enableRankingPage: false,
}
/// 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) => {
/*
```
let data = JSON.parse((await Network.get('...')).body)
let maxPage = data.maxPage
function parseComic(comic) {
// ...
return new Comic({
id: id,
title: title,
subTitle: author,
cover: cover,
tags: tags,
description: description
})
}
return {
comics: data.list.map(parseComic),
maxPage: maxPage
}
```
*/
},
// 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) => {
/*
```
let data = JSON.parse((await Network.get('...')).body)
let maxPage = data.maxPage
function parseComic(comic) {
// ...
return new Comic({
id: id,
title: title,
subTitle: author,
cover: cover,
tags: tags,
description: description
})
}
return {
comics: data.list.map(parseComic),
maxPage: maxPage
}
```
*/
}
}
}
/// search related
search = {
/**
* load search result
* @param keyword {string}
* @param options {string[]} - options from optionList
* @param page {number}
* @returns {Promise<{comics: Comic[], maxPage: number}>}
*/
load: async (keyword, options, page) => {
/*
```
let data = JSON.parse((await Network.get('...')).body)
let maxPage = data.maxPage
function parseComic(comic) {
// ...
return new Comic({
id: id,
title: title,
subTitle: author,
cover: cover,
tags: tags,
description: description
})
}
return {
comics: data.list.map(parseComic),
maxPage: maxPage
}
```
*/
},
// provide options for search
optionList: [
{
// 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"
}
]
}
// 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
* @returns {Promise<any>} - return any value to indicate success
*/
addOrDelFavorite: async (comicId, folderId, isAdding) => {
/*
```
let res = await Network.post('...')
if (res.status === 401) {
throw `Login expired`;
}
return 'ok'
```
*/
},
/**
* 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) => {
/*
```
let data = JSON.parse((await Network.get('...')).body)
let folders = {}
data.folders.forEach((f) => {
folders[f.id] = f.name
})
return {
folders: folders,
favorited: data.favorited
}
```
*/
},
/**
* 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) => {
/*
```
let data = JSON.parse((await Network.get('...')).body)
let maxPage = data.maxPage
function parseComic(comic) {
// ...
return new Comic{
id: id,
title: title,
subTitle: author,
cover: cover,
tags: tags,
description: description
}
}
return {
comics: data.list.map(parseComic),
maxPage: maxPage
}
```
*/
}
}
/// single comic related
comic = {
/**
* load comic info
* @param id {string}
* @returns {Promise<ComicDetails>}
*/
loadInfo: async (id) => {
},
/**
* [Optional] load thumbnails of a comic
* @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) => {
/*
```
let data = JSON.parse((await Network.get('...')).body)
return {
thumbnails: data.list,
next: next,
}
```
*/
},
/**
* load images of a chapter
* @param comicId {string}
* @param epId {string?}
* @returns {Promise<{images: string[]}>}
*/
loadEp: async (comicId, epId) => {
/*
```
return {
// string[]
images: images
}
```
*/
},
/**
* [Optional] provide configs for an image loading
* @param url
* @param comicId
* @param epId
* @returns {{}}
*/
onImageLoad: (url, comicId, epId) => {
/*
```
return {
url: `${url}?id=comicId`,
// http method
method: 'GET',
// any
data: null,
headers: {
'user-agent': 'pica_comic/v3.1.0',
},
// * modify response data
// * @param data {ArrayBuffer}
// * @returns {ArrayBuffer}
onResponse: (data) => {
return data
}
}
```
*/
return {}
},
/**
* [Optional] provide configs for a thumbnail loading
* @param url {string}
* @returns {{}}
*/
onThumbnailLoad: (url) => {
/*
```
return {
url: `${url}?id=comicId`,
// http method
method: 'GET',
// {any}
data: null,
headers: {
'user-agent': 'pica_comic/v3.1.0',
},
// modify response data
onResponse: (data) => {
return data
}
}
```
*/
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
* @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) => {
/*
```
// ...
return {
comments: data.results.list.map(e => {
return new Comment({
// string
userName: e.user_name,
// string
avatar: e.user_avatar,
// string
content: e.comment,
// string?
time: e.create_at,
// number?
replyCount: e.count,
// string
id: e.id,
})
}),
// number
maxPage: data.results.maxPage,
}
```
*/
},
/**
* [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) => {
/*
```
return {
// 'search' or 'category'
action: 'search',
keyword: tag,
// {string?} only for category action
param: null,
}
*/
},
}
/*
[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: '',
}
}
// [Optional] translations for the strings in this config
translation = {
'zh_CN': {
'Setting1': '设置1',
'Setting2': '设置2',
'Setting3': '设置3',
},
'zh_TW': {},
'en': {}
}
}

767
_venera_.js Normal file
View File

@@ -0,0 +1,767 @@
/*
Venera JavaScript Library
This library provides a set of APIs for interacting with the Venera app.
*/
/// encode, decode, hash, decrypt
let Convert = {
/**
* @param str {string}
* @returns {ArrayBuffer}
*/
encodeUtf8: (str) => {
return sendMessage({
method: "convert",
type: "utf8",
value: str,
isEncode: true
});
},
/**
* @param value {ArrayBuffer}
* @returns {string}
*/
decodeUtf8: (value) => {
return sendMessage({
method: "convert",
type: "utf8",
value: value,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @returns {string}
*/
encodeBase64: (value) => {
return sendMessage({
method: "convert",
type: "base64",
value: value,
isEncode: true
});
},
/**
* @param {string} value
* @returns {ArrayBuffer}
*/
decodeBase64: (value) => {
return sendMessage({
method: "convert",
type: "base64",
value: value,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @returns {ArrayBuffer}
*/
md5: (value) => {
return sendMessage({
method: "convert",
type: "md5",
value: value,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @returns {ArrayBuffer}
*/
sha1: (value) => {
return sendMessage({
method: "convert",
type: "sha1",
value: value,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @returns {ArrayBuffer}
*/
sha256: (value) => {
return sendMessage({
method: "convert",
type: "sha256",
value: value,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @returns {ArrayBuffer}
*/
sha512: (value) => {
return sendMessage({
method: "convert",
type: "sha512",
value: value,
isEncode: true
});
},
/**
* @param key {ArrayBuffer}
* @param value {ArrayBuffer}
* @param hash {string} - md5, sha1, sha256, sha512
* @returns {ArrayBuffer}
*/
hmac: (key, value, hash) => {
return sendMessage({
method: "convert",
type: "hmac",
value: value,
key: key,
hash: hash,
isEncode: true
});
},
/**
* @param key {ArrayBuffer}
* @param value {ArrayBuffer}
* @param hash {string} - md5, sha1, sha256, sha512
* @returns {string} - hex string
*/
hmacString: (key, value, hash) => {
return sendMessage({
method: "convert",
type: "hmac",
value: value,
key: key,
hash: hash,
isEncode: true,
isString: true
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @returns {ArrayBuffer}
*/
decryptAesEcb: (value, key) => {
return sendMessage({
method: "convert",
type: "aes-ecb",
value: value,
key: key,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {ArrayBuffer} iv
* @returns {ArrayBuffer}
*/
decryptAesCbc: (value, key, iv) => {
return sendMessage({
method: "convert",
type: "aes-ecb",
value: value,
key: key,
iv: iv,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {number} blockSize
* @returns {ArrayBuffer}
*/
decryptAesCfb: (value, key, blockSize) => {
return sendMessage({
method: "convert",
type: "aes-cfb",
value: value,
key: key,
blockSize: blockSize,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {number} blockSize
* @returns {ArrayBuffer}
*/
decryptAesOfb: (value, key, blockSize) => {
return sendMessage({
method: "convert",
type: "aes-ofb",
value: value,
key: key,
blockSize: blockSize,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @returns {ArrayBuffer}
*/
decryptRsa: (value, key) => {
return sendMessage({
method: "convert",
type: "rsa",
value: value,
key: key,
isEncode: false
});
}
}
/**
* create a time-based uuid
*
* Note: the engine will generate a new uuid every time it is called
*
* To get the same uuid, please save it to the local storage
*
* @returns {string}
*/
function createUuid() {
return sendMessage({
method: "uuid"
});
}
function randomInt(min, max) {
return sendMessage({
method: 'random',
min: min,
max: max
});
}
class _Timer {
delay = 0;
callback = () => { };
status = false;
constructor(delay, callback) {
this.delay = delay;
this.callback = callback;
}
run() {
this.status = true;
this._interval();
}
_interval() {
if (!this.status) {
return;
}
this.callback();
setTimeout(this._interval.bind(this), this.delay);
}
cancel() {
this.status = false;
}
}
function setInterval(callback, delay) {
let timer = new _Timer(delay, callback);
timer.run();
return timer;
}
function Cookie(name, value, domain = null) {
let obj = {};
obj.name = name;
obj.value = value;
if (domain) {
obj.domain = domain;
}
return obj;
}
/**
* Network object for sending HTTP requests and managing cookies.
* @namespace Network
*/
let Network = {
/**
* Sends an HTTP request.
* @param {string} method - The HTTP method (e.g., GET, POST, PUT, PATCH, DELETE).
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<ArrayBuffer>} The response from the request.
*/
async fetchBytes(method, url, headers, data) {
let result = await sendMessage({
method: 'http',
http_method: method,
bytes: true,
url: url,
headers: headers,
data: data,
});
if (result.error) {
throw result.error;
}
return result;
},
/**
* Sends an HTTP request.
* @param {string} method - The HTTP method (e.g., GET, POST, PUT, PATCH, DELETE).
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<Object>} The response from the request.
*/
async sendRequest(method, url, headers, data) {
let result = await sendMessage({
method: 'http',
http_method: method,
url: url,
headers: headers,
data: data,
});
if (result.error) {
throw result.error;
}
return result;
},
/**
* Sends an HTTP GET request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @returns {Promise<Object>} The response from the request.
*/
async get(url, headers) {
return this.sendRequest('GET', url, headers);
},
/**
* Sends an HTTP POST request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<Object>} The response from the request.
*/
async post(url, headers, data) {
return this.sendRequest('POST', url, headers, data);
},
/**
* Sends an HTTP PUT request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<Object>} The response from the request.
*/
async put(url, headers, data) {
return this.sendRequest('PUT', url, headers, data);
},
/**
* Sends an HTTP PATCH request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<Object>} The response from the request.
*/
async patch(url, headers, data) {
return this.sendRequest('PATCH', url, headers, data);
},
/**
* Sends an HTTP DELETE request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @returns {Promise<Object>} The response from the request.
*/
async delete(url, headers) {
return this.sendRequest('DELETE', url, headers);
},
/**
* Sets cookies for a specific URL.
* @param {string} url - The URL to set the cookies for.
* @param {Cookie[]} cookies - The cookies to set.
*/
setCookies(url, cookies) {
sendMessage({
method: 'cookie',
function: 'set',
url: url,
cookies: cookies,
});
},
/**
* Retrieves cookies for a specific URL.
* @param {string} url - The URL to get the cookies from.
* @returns {Promise<Cookie[]>} The cookies for the given URL.
*/
getCookies(url) {
return sendMessage({
method: 'cookie',
function: 'get',
url: url,
});
},
/**
* Deletes cookies for a specific URL.
* @param {string} url - The URL to delete the cookies from.
*/
deleteCookies(url) {
sendMessage({
method: 'cookie',
function: 'delete',
url: url,
});
},
};
/**
* HtmlDocument class for parsing HTML and querying elements.
*/
class HtmlDocument {
static _key = 0;
key = 0;
/**
* Constructor for HtmlDocument.
* @param {string} html - The HTML string to parse.
*/
constructor(html) {
this.key = HtmlDocument._key;
HtmlDocument._key++;
sendMessage({
method: "html",
function: "parse",
key: this.key,
data: html
})
}
/**
* Query a single element from the HTML document.
* @param {string} query - The query string.
* @returns {HtmlElement} The first matching element.
*/
querySelector(query) {
let k = sendMessage({
method: "html",
function: "querySelector",
key: this.key,
query: query
})
if(!k) return null;
return new HtmlElement(k);
}
/**
* Query all matching elements from the HTML document.
* @param {string} query - The query string.
* @returns {HtmlElement[]} An array of matching elements.
*/
querySelectorAll(query) {
let ks = sendMessage({
method: "html",
function: "querySelectorAll",
key: this.key,
query: query
})
return ks.map(k => new HtmlElement(k));
}
}
/**
* HtmlDom class for interacting with HTML elements.
*/
class HtmlElement {
key = 0;
/**
* Constructor for HtmlDom.
* @param {number} k - The key of the element.
*/
constructor(k) {
this.key = k;
}
/**
* Get the text content of the element.
* @returns {string} The text content.
*/
get text() {
return sendMessage({
method: "html",
function: "getText",
key: this.key
})
}
/**
* Get the attributes of the element.
* @returns {Object} The attributes.
*/
get attributes() {
return sendMessage({
method: "html",
function: "getAttributes",
key: this.key
})
}
/**
* Query a single element from the current element.
* @param {string} query - The query string.
* @returns {HtmlElement} The first matching element.
*/
querySelector(query) {
let k = sendMessage({
method: "html",
function: "dom_querySelector",
key: this.key,
query: query
})
if(!k) return null;
return new HtmlElement(k);
}
/**
* Query all matching elements from the current element.
* @param {string} query - The query string.
* @returns {HtmlElement[]} An array of matching elements.
*/
querySelectorAll(query) {
let ks = sendMessage({
method: "html",
function: "dom_querySelectorAll",
key: this.key,
query: query
})
return ks.map(k => new HtmlElement(k));
}
/**
* Get the children of the current element.
* @returns {HtmlElement[]} An array of child elements.
*/
get children() {
let ks = sendMessage({
method: "html",
function: "getChildren",
key: this.key
})
return ks.map(k => new HtmlElement(k));
}
}
function log(level, title, content) {
sendMessage({
method: 'log',
level: level,
title: title,
content: content,
})
}
let console = {
log: (content) => {
log('info', 'JS Console', content)
},
warn: (content) => {
log('warning', 'JS Console', content)
},
error: (content) => {
log('error', 'JS Console', content)
},
};
/**
* Create a comic object
* @param id {string}
* @param title {string}
* @param subtitle {string}
* @param cover {string}
* @param tags {string[]}
* @param description {string}
* @param maxPage {number | null}
* @constructor
*/
function Comic({id, title, subtitle, cover, tags, description, maxPage}) {
this.id = id;
this.title = title;
this.subtitle = subtitle;
this.cover = cover;
this.tags = tags;
this.description = description;
this.maxPage = maxPage;
}
/**
* Create a comic details object
* @param title {string}
* @param cover {string}
* @param description {string | null}
* @param tags {Map<string, string[]> | {} | null}
* @param chapters {Map<string, string> | {} | null} - key: chapter id, value: chapter title
* @param isFavorite {boolean | null} - favorite status. If the comic source supports multiple folders, this field should be null
* @param subId {string | null} - a param which is passed to comments api
* @param thumbnails {string[] | null} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails
* @param recommend {Comic[] | null} - related comics
* @param commentCount {number | null}
* @param likesCount {number | null}
* @param isLiked {boolean | null}
* @param uploader {string | null}
* @param updateTime {string | null}
* @param uploadTime {string | null}
* @param url {string | null}
* @constructor
*/
function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url}) {
this.title = title;
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;
}
/**
* 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;
}
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 = {}
}

View File

@@ -22,5 +22,11 @@
"fileName": "baozi.js", "fileName": "baozi.js",
"key": "baozi", "key": "baozi",
"version": "1.0.0" "version": "1.0.0"
},
{
"name": "Picacg",
"fileName": "picacg.js",
"key": "picacg",
"version": "1.0.0"
} }
] ]

649
picacg.js Normal file
View File

@@ -0,0 +1,649 @@
class Picacg extends ComicSource {
name = "Picacg"
key = "picacg"
version = "1.0.0"
minAppVersion = "1.0.0"
url = "https://raw.githubusercontent.com/venera-app/venera_configs/master/picacg.js"
api = "https://picaapi.picacomic.com"
apiKey = "C69BAF41DA5ABD1FFEDC6D2FEA56B";
createSignature(path, nonce, time, method) {
let data = path + time + nonce + method + this.apiKey
let key = '~d}$Q7$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn'
let s = Convert.encodeUtf8(key)
let h = Convert.encodeUtf8(data.toLowerCase())
return Convert.hmacString(s, h, 'sha256')
}
buildHeaders(method, path, token) {
let uuid = createUuid()
let nonce = uuid.replace(/-/g, '')
let time = (new Date().getTime() / 1000).toFixed(0)
let signature = this.createSignature(path, nonce, time, method.toUpperCase())
return {
"api-key": "C69BAF41DA5ABD1FFEDC6D2FEA56B",
"accept": "application/vnd.picacomic.com.v1+json",
"app-channel": this.loadSetting('appChannel'),
"authorization": token ?? "",
"time": time,
"nonce": nonce,
"app-version": "2.2.1.3.3.4",
"app-uuid": "defaultUuid",
"image-quality": this.loadSetting('imageQuality'),
"app-platform": "android",
"app-build-version": "45",
"Content-Type": "application/json; charset=UTF-8",
"user-agent": "okhttp/3.8.1",
"version": "v1.4.1",
"Host": "picaapi.picacomic.com",
"signature": signature,
}
}
account = {
login: async (account, pwd) => {
let res = await Network.post(
`${this.api}/auth/sign-in`,
this.buildHeaders('POST', 'auth/sign-in'),
{
email: account,
password: pwd
})
if (res.status === 200) {
let json = JSON.parse(res.body)
if (!json.data?.token) {
throw 'Failed to get token\nResponse: ' + res.body
}
this.saveData('token', json.data.token)
return 'ok'
}
throw 'Failed to login'
},
logout: () => {
this.deleteData('token')
},
registerWebsite: "https://manhuabika.com/pregister/?"
}
parseComic(comic) {
let tags = []
tags.push(...(comic.tags ?? []))
tags.push(...(comic.categories ?? []))
return new Comic({
id: comic._id,
title: comic.title,
subTitle: comic.author,
cover: comic.thumb.fileServer + '/static/' + comic.thumb.path,
tags: tags,
description: `${comic.totalLikes} likes`,
maxPage: comic.pagesCount,
})
}
explore = [
{
title: "Picacg Random",
type: "multiPageComicList",
load: async (page) => {
if (!this.isLogged) {
throw 'Not logged in'
}
let res = await Network.get(
`${this.api}/comics/random`,
this.buildHeaders('GET', 'comics/random', this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
let comics = []
data.data.comics.forEach(c => {
comics.push(this.parseComic(c))
})
return {
comics: comics
}
}
},
{
title: "Picacg Latest",
type: "multiPageComicList",
load: async (page) => {
if (!this.isLogged) {
throw 'Not logged in'
}
let res = await Network.get(
`${this.api}/comics?page=${page}&s=dd`,
this.buildHeaders('GET', `comics?page=${page}&s=dd`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
let comics = []
data.data.comics.docs.forEach(c => {
comics.push(this.parseComic(c))
})
return {
comics: comics
}
}
}
]
/// 分类页面
/// 一个漫画源只能有一个分类页面, 也可以没有, 设置为null禁用分类页面
category = {
/// 标题, 同时为标识符, 不能与其他漫画源的分类页面重复
title: "Picacg",
parts: [
{
name: "主题",
type: "fixed",
categories: [
"大家都在看",
"大濕推薦",
"那年今天",
"官方都在看",
"嗶咔漢化",
"全彩",
"長篇",
"同人",
"短篇",
"圓神領域",
"碧藍幻想",
"CG雜圖",
"英語 ENG",
"生肉",
"純愛",
"百合花園",
"耽美花園",
"偽娘哲學",
"後宮閃光",
"扶他樂園",
"單行本",
"姐姐系",
"妹妹系",
"SM",
"性轉換",
"足の恋",
"人妻",
"NTR",
"強暴",
"非人類",
"艦隊收藏",
"Love Live",
"SAO 刀劍神域",
"Fate",
"東方",
"WEBTOON",
"禁書目錄",
"歐美",
"Cosplay",
"重口地帶"
],
itemType: "category",
}
],
enableRankingPage: true,
}
/// 分类漫画页面, 即点击分类标签后进入的页面
categoryComics = {
load: async (category, param, options, page) => {
let type = param ?? 'c'
let res = await Network.get(
`${this.api}/comics?page=${page}&${type}=${encodeURIComponent(category)}&s=${options[0]}`,
this.buildHeaders('GET', `comics?page=${page}&${type}=${encodeURIComponent(category)}&s=${options[0]}`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
let comics = []
data.data.comics.docs.forEach(c => {
comics.push(this.parseComic(c))
})
return {
comics: comics,
maxPage: data.data.comics.pages
}
},
// 提供选项
optionList: [
{
options: [
"dd-New to old",
"da-Old to new",
"ld-Most likes",
"vd-Most nominated",
],
}
],
ranking: {
options: [
"H24-Day",
"D7-Week",
"D30-Month",
],
load: async (option, page) => {
let res = await Network.get(
`${this.api}/comics/leaderboard?tt=${option}&ct=VC`,
this.buildHeaders('GET', `comics/leaderboard?tt=${option}&ct=VC`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
let comics = []
data.data.comics.forEach(c => {
comics.push(this.parseComic(c))
})
return {
comics: comics,
maxPage: 1
}
}
}
}
/// 搜索
search = {
load: async (keyword, options, page) => {
let res = await Network.post(
`${this.api}/comics/advanced-search?page=${page}`,
this.buildHeaders('POST', `comics/advanced-search?page=${page}`, this.loadData('token')),
JSON.stringify({
keyword: keyword,
sort: options[0],
})
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
let comics = []
data.data.comics.docs.forEach(c => {
comics.push(this.parseComic(c))
})
return {
comics: comics,
maxPage: data.data.comics.pages
}
},
optionList: [
{
options: [
"dd-New to old",
"da-Old to new",
"ld-Most likes",
"vd-Most nominated",
],
label: "Sort"
}
]
}
/// 收藏
favorites = {
multiFolder: false,
/// 添加或者删除收藏
addOrDelFavorite: async (comicId, folderId, isAdding) => {
let res = await Network.post(
`${this.api}/comics/${comicId}/favourite`,
this.buildHeaders('POST', `comics/${comicId}/favourite`, this.loadData('token')),
'{}'
)
if(res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
return 'ok'
},
/// 加载漫画
loadComics: async (page, folder) => {
let sort = this.loadSetting('favoriteSort')
let res = await Network.get(
`${this.api}/users/favourite?page=${page}&s=${sort}`,
this.buildHeaders('GET', `users/favourite?page=${page}&s=${sort}`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
let comics = []
data.data.comics.docs.forEach(c => {
comics.push(this.parseComic(c))
})
return {
comics: comics,
maxPage: data.data.comics.pages
}
}
}
/// 单个漫画相关
comic = {
// 加载漫画信息
loadInfo: async (id) => {
let infoLoader = async () => {
let res = await Network.get(
`${this.api}/comics/${id}`,
this.buildHeaders('GET', `comics/${id}`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
return data.data.comic
}
let epsLoader = async () => {
let eps = new Map()
let i = 1;
let j = 1;
while(true) {
let res = await Network.get(
`${this.api}/comics/${id}/eps?page=${i}`,
this.buildHeaders('GET', `comics/${id}/eps?page=${i}`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
data.data.eps.docs.forEach(e => {
eps.set(j.toString(), e.title)
j++
})
if(data.data.eps.pages === i) {
break
}
i++
}
return eps
}
let relatedLoader = async () => {
let res = await Network.get(
`${this.api}/comics/${id}/recommendation`,
this.buildHeaders('GET', `comics/${id}/recommendation`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
let comics = []
data.data.comics.forEach(c => {
comics.push(this.parseComic(c))
})
return comics
}
let [info, eps, related] = await Promise.all([infoLoader(), epsLoader(), relatedLoader()])
let tags = {}
if(info.author) {
tags['Author'] = [info.author];
}
if(info.chineseTeam) {
tags['Chinese Team'] = [info.chineseTeam];
}
return new ComicDetails({
title: info.title,
cover: info.thumb.fileServer + '/static/' + info.thumb.path,
description: info.description,
tags: {
...tags,
'Categories': info.categories,
'Tags': info.tags,
},
chapters: eps,
isFavorite: info.isFavourite ?? false,
isLiked: info.isLiked ?? false,
recommend: related,
commentCount: info.commentsCount,
likesCount: info.likesCount,
uploader: info._creator.name,
updateTime: info.updated_at,
})
},
// 获取章节图片
loadEp: async (comicId, epId) => {
let images = []
let i = 1
while(true) {
let res = await Network.get(
`${this.api}/comics/${comicId}/order/${epId}/pages?page=${i}`,
this.buildHeaders('GET', `comics/${comicId}/order/${epId}/pages?page=${i}`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
data.data.pages.docs.forEach(p => {
images.push(p.media.fileServer + '/static/' + p.media.path)
})
if(data.data.pages.pages === i) {
break
}
i++
}
return {
images: images
}
},
likeComic: async (id, isLike) => {
var res = await Network.post(
`${this.api}/comics/${id}/like`,
this.buildHeaders('POST', `comics/${id}/like`, this.loadData('token')),
{}
);
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
return 'ok'
},
// 加载评论
loadComments: async (comicId, subId, page, replyTo) => {
function parseComment(c) {
return new Comment({
userName: c._user.name,
avatar: c._user.avatar ? c._user.avatar.fileServer + '/static/' + c._user.avatar.path : undefined,
id: c._id,
content: c.content,
isLiked: c.isLiked,
score: c.likesCount ?? 0,
replyCount: c.commentsCount,
time: c.created_at,
})
}
let comments = []
let maxPage = 1
if(replyTo) {
let res = await Network.get(
`${this.api}/comments/${replyTo}/childrens?page=${page}`,
this.buildHeaders('GET', `comments/${replyTo}/childrens?page=${page}`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
data.data.comments.docs.forEach(c => {
comments.push(parseComment(c))
})
maxPage = data.data.comments.pages
} else {
let res = await Network.get(
`${this.api}/comics/${comicId}/comments?page=${page}`,
this.buildHeaders('GET', `comics/${comicId}/comments?page=${page}`, this.loadData('token'))
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
let data = JSON.parse(res.body)
data.data.comments.docs.forEach(c => {
comments.push(parseComment(c))
})
maxPage = data.data.comments.pages
}
return {
comments: comments,
maxPage: maxPage
}
},
// 发送评论, 返回任意值表示成功
sendComment: async (comicId, subId, content, replyTo) => {
if(replyTo) {
let res = await Network.post(
`${this.api}/comments/${replyTo}`,
this.buildHeaders('POST', `/comments/${replyTo}`, this.loadData('token')),
JSON.stringify({
content: content
})
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
} else {
let res = await Network.post(
`${this.api}/comics/${comicId}/comments`,
this.buildHeaders('POST', `/comics/${comicId}/comments`, this.loadData('token')),
JSON.stringify({
content: content
})
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
}
return 'ok'
},
likeComment: async (comicId, subId, commentId, isLike) => {
let res = await Network.post(
`${this.api}/comments/${commentId}/like`,
this.buildHeaders('POST', `/comments/${commentId}/like`, this.loadData('token')),
'{}'
)
if (res.status !== 200) {
throw 'Invalid status code: ' + res.status
}
return 'ok'
},
onClickTag: (namespace, tag) => {
if(namespace === 'Author') {
return {
action: 'category',
keyword: tag,
param: 'a',
}
} else if (namespace === 'Categories') {
return {
action: 'category',
keyword: tag,
param: 'c',
}
} else {
return {
action: 'search',
keyword: tag,
}
}
}
}
settings = {
'imageQuality': {
type: 'select',
title: 'Image quality',
options: [
{
value: 'original',
},
{
value: 'medium'
},
{
value: 'low'
}
],
default: 'original',
},
'appChannel': {
type: 'select',
title: 'App channel',
options: [
{
value: '1',
},
{
value: '2'
},
{
value: '3'
}
],
default: '3',
},
'favoriteSort': {
type: 'select',
title: 'Favorite sort',
options: [
{
value: 'dd',
text: 'New to old'
},
{
value: 'da',
text: 'Old to new'
},
],
default: 'dd',
}
}
translation = {
'zh_CN': {
'Picacg Random': "哔咔随机",
'Picacg Latest': "哔咔最新",
'New to old': "新到旧",
'Old to new': "旧到新",
'Most likes': "最多喜欢",
'Most nominated': "最多指名",
'Day': "日",
'Week': "周",
'Month': "月",
'Author': "作者",
'Chinese Team': "汉化组",
'Categories': "分类",
'Tags': "标签",
'Image quality': "图片质量",
'App channel': "分流",
'Favorite sort': "收藏排序",
},
'zh_TW': {
'Picacg Random': "哔咔隨機",
'Picacg Latest': "哔咔最新",
'New to old': "新到舊",
'Old to new': "舊到新",
'Most likes': "最多喜歡",
'Most nominated': "最多指名",
'Day': "日",
'Week': "周",
'Month': "月",
'Author': "作者",
'Chinese Team': "漢化組",
'Categories': "分類",
'Tags': "標籤",
'Image quality': "圖片質量",
'App channel': "分流",
'Favorite sort': "收藏排序",
},
}
}