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 = { /** * [Optional] login with account and password, return any value to indicate success * @param account {string} * @param pwd {string} * @returns {Promise} */ 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' ``` */ }, /** * [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: () => { /* ``` 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: "", /// 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) => { /* ``` 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 ``` */ }, /** * 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) {}, } ] // 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 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 | null)[]} - 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 } ``` */ }, /** * 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, } // 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) => { /* ``` 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 } ``` */ }, /** * add a folder * @param name {string} * @returns {Promise} - return any value to indicate success */ addFolder: async (name) => { /* ``` let res = await Network.post('...') if (res.status === 401) { throw `Login expired`; } return 'ok' ``` */ }, /** * delete a folder * @param folderId {string} * @returns {Promise} - return any value to indicate success */ deleteFolder: async (folderId) => { /* ``` let res = await Network.delete('...') if (res.status === 401) { throw `Login expired`; } return 'ok' ``` */ }, /** * 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 } ``` */ }, /** * 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) => { }, } /// 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) => { /* ``` let data = JSON.parse((await Network.get('...')).body) return { thumbnails: data.list, next: 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) => { /* ``` return { // string[] images: images } ``` */ }, /** * [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 * @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} */ 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) => { /* ``` return { // 'search' or 'category' action: 'search', keyword: tag, // {string?} only for category action param: null, } */ }, /** * [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, } /* [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': {} } }