From 2d1c696ab3462d494613716de837ba74da6db163 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 20 Oct 2024 11:12:53 +0800 Subject: [PATCH] =?UTF-8?q?add=20=E7=B4=B3=E5=A3=AB=E6=BC=AB=E7=95=AB;=20u?= =?UTF-8?q?pdate=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _template_.js | 35 ++- _venera_.js | 143 +++++++++--- index.json | 6 + wnacg.js | 585 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 743 insertions(+), 26 deletions(-) create mode 100644 wnacg.js diff --git a/_template_.js b/_template_.js index 06d30f4..35f4b61 100644 --- a/_template_.js +++ b/_template_.js @@ -324,9 +324,10 @@ class NewComicSource extends ComicSource { * @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) => { + addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => { /* ``` let res = await Network.post('...') @@ -362,6 +363,38 @@ class NewComicSource extends ComicSource { ``` */ }, + /** + * 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. diff --git a/_venera_.js b/_venera_.js index 4f26704..c485a50 100644 --- a/_venera_.js +++ b/_venera_.js @@ -242,9 +242,31 @@ function createUuid() { }); } +/** + * Generate a random integer between min and max + * @param min {number} + * @param max {number} + * @returns {number} + */ function randomInt(min, max) { return sendMessage({ method: 'random', + type: 'int', + min: min, + max: max + }); +} + +/** + * Generate a random double between min and max + * @param min {number} + * @param max {number} + * @returns {number} + */ +function randomDouble(min, max) { + return sendMessage({ + method: 'random', + type: 'double', min: min, max: max }); @@ -478,8 +500,8 @@ class HtmlDocument { key: this.key, query: query }) - if(!k) return null; - return new HtmlElement(k); + if(k == null) return null; + return new HtmlElement(k, this.key); } /** @@ -494,7 +516,19 @@ class HtmlDocument { key: this.key, query: query }) - return ks.map(k => new HtmlElement(k)); + return ks.map(k => new HtmlElement(k, this.key)); + } + + /** + * Dispose the HTML document. + * This should be called when the document is no longer needed. + */ + dispose() { + sendMessage({ + method: "html", + function: "dispose", + key: this.key + }) } } @@ -504,12 +538,16 @@ class HtmlDocument { class HtmlElement { key = 0; + doc = 0; + /** * Constructor for HtmlDom. * @param {number} k - The key of the element. + * @param {number} doc - The key of the document. */ - constructor(k) { + constructor(k, doc) { this.key = k; + this.doc = doc; } /** @@ -520,7 +558,8 @@ class HtmlElement { return sendMessage({ method: "html", function: "getText", - key: this.key + key: this.key, + doc: this.doc, }) } @@ -532,7 +571,8 @@ class HtmlElement { return sendMessage({ method: "html", function: "getAttributes", - key: this.key + key: this.key, + doc: this.doc, }) } @@ -546,10 +586,11 @@ class HtmlElement { method: "html", function: "dom_querySelector", key: this.key, - query: query + query: query, + doc: this.doc, }) - if(!k) return null; - return new HtmlElement(k); + if(k == null) return null; + return new HtmlElement(k, this.doc); } /** @@ -562,9 +603,10 @@ class HtmlElement { method: "html", function: "dom_querySelectorAll", key: this.key, - query: query + query: query, + doc: this.doc, }) - return ks.map(k => new HtmlElement(k)); + return ks.map(k => new HtmlElement(k, this.doc)); } /** @@ -575,9 +617,10 @@ class HtmlElement { let ks = sendMessage({ method: "html", function: "getChildren", - key: this.key + key: this.key, + doc: this.doc, }) - return ks.map(k => new HtmlElement(k)); + return ks.map(k => new HtmlElement(k, this.doc)); } /** @@ -588,9 +631,10 @@ class HtmlElement { let ks = sendMessage({ method: "html", function: "getNodes", - key: this.key + key: this.key, + doc: this.doc, }) - return ks.map(k => new HtmlNode(k)); + return ks.map(k => new HtmlNode(k, this.doc)); } /** @@ -601,7 +645,8 @@ class HtmlElement { return sendMessage({ method: "html", function: "getInnerHTML", - key: this.key + key: this.key, + doc: this.doc, }) } @@ -613,18 +658,61 @@ class HtmlElement { let k = sendMessage({ method: "html", function: "getParent", - key: this.key + key: this.key, + doc: this.doc, }) - if(!k) return null; + if(k == null) return null; return new HtmlElement(k); } + + /** + * Get class names of the element. + * @returns {string[]} An array of class names. + */ + get classNames() { + return sendMessage({ + method: "html", + function: "getClassNames", + key: this.key, + doc: this.doc, + }) + } + + /** + * Get id of the element. + * @returns {string | null} The id of the element. + */ + get id() { + return sendMessage({ + method: "html", + function: "getId", + key: this.key, + doc: this.doc, + }) + } + + /** + * Get local name of the element. + * @returns {string} The tag name of the element. + */ + get localName() { + return sendMessage({ + method: "html", + function: "getLocalName", + key: this.key, + doc: this.doc, + }) + } } class HtmlNode { key = 0; - constructor(k) { + doc = 0; + + constructor(k, doc) { this.key = k; + this.doc = doc; } /** @@ -635,7 +723,8 @@ class HtmlNode { return sendMessage({ method: "html", function: "node_text", - key: this.key + key: this.key, + doc: this.doc, }) } @@ -647,7 +736,8 @@ class HtmlNode { return sendMessage({ method: "html", function: "node_type", - key: this.key + key: this.key, + doc: this.doc, }) } @@ -659,10 +749,11 @@ class HtmlNode { let k = sendMessage({ method: "html", function: "node_toElement", - key: this.key + key: this.key, + doc: this.doc, }) - if(!k) return null; - return new HtmlElement(k); + if(k == null) return null; + return new HtmlElement(k, this.doc); } } @@ -697,9 +788,10 @@ let console = { * @param description {string} * @param maxPage {number?} * @param language {string?} + * @param favoriteId {string?} - Only set this field if the comic is from favorites page * @constructor */ -function Comic({id, title, subtitle, cover, tags, description, maxPage, language}) { +function Comic({id, title, subtitle, cover, tags, description, maxPage, language, favoriteId}) { this.id = id; this.title = title; this.subtitle = subtitle; @@ -708,6 +800,7 @@ function Comic({id, title, subtitle, cover, tags, description, maxPage, language this.description = description; this.maxPage = maxPage; this.language = language; + this.favoriteId = favoriteId; } /** diff --git a/index.json b/index.json index 7d28a76..9b6d6e4 100644 --- a/index.json +++ b/index.json @@ -34,5 +34,11 @@ "fileName": "nhentai.js", "key": "nhentai", "version": "1.0.0" + }, + { + "name": "紳士漫畫", + "fileName": "wnacg.js", + "key": "wnacg", + "version": "1.0.0" } ] diff --git a/wnacg.js b/wnacg.js new file mode 100644 index 0000000..6044f00 --- /dev/null +++ b/wnacg.js @@ -0,0 +1,585 @@ +class Wnacg 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 = "wnacg" + + version = "1.0.0" + + minAppVersion = "1.0.0" + + // update url + url = "https://raw.githubusercontent.com/venera-app/venera-configs/refs/heads/main/wnacg.js" + + get baseUrl() { + return `https://${this.loadSetting('domain')}` + } + + // [Optional] account related + account = { + /** + * login, return any value to indicate success + * @param account {string} + * @param pwd {string} + * @returns {Promise} + */ + login: async (account, pwd) => { + let res = await Network.post( + `${this.baseUrl}/users-check_login.html`, + { + 'content-type': 'application/x-www-form-urlencoded' + }, + `login_name=${encodeURIComponent(account)}&login_pass=${encodeURIComponent(pwd)}` + ) + if(res.status !== 200) { + throw 'Login failed' + } + let json = JSON.parse(res.body) + if(json['html'].includes('登錄成功')) { + return 'ok' + } + throw 'Login failed' + }, + + /** + * logout function, clear account related data + */ + logout: () => { + Network.deleteCookies(this.baseUrl) + }, + + // {string?} - register url + registerWebsite: null + } + + parseComic(c) { + let link = c.querySelector("div.pic_box > a").attributes["href"]; + let id = RegExp("(?<=-aid-)[0-9]+").exec(link)[0]; + let image = + c.querySelector("div.pic_box > a > img").attributes["src"]; + image = `https:${image}`; + let name = c.querySelector("div.info > div.title > a").text; + let info = c.querySelector("div.info > div.info_col").text.trim(); + info = info.replaceAll('\n', ''); + info = info.replaceAll('\t', ''); + return new Comic({ + id: id, + title: name, + cover: image, + description: info, + }) + } + + // 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(this.baseUrl, {}) + if(res.status !== 200) { + throw `Invalid Status Code ${res.status}` + } + let document = new HtmlDocument(res.body) + let titleBlocks = document.querySelectorAll("div.title_sort"); + let comicBlocks = document.querySelectorAll("div.bodywrap"); + if (titleBlocks.length !== comicBlocks.length) { + throw "Invalid Page" + } + let result = [] + for (let i = 0; i < titleBlocks.length; i++) { + let title = titleBlocks[i].querySelector("div.title_h2").text.replaceAll('\n', '').trim() + let link = titleBlocks[i].querySelector("div.r > a").attributes["href"] + let comics = [] + let comicBlock = comicBlocks[i] + let comicElements = comicBlock.querySelectorAll("div.gallary_wrap > ul.cc > li") + for (let comicElement of comicElements) { + comics.push(this.parseComic(comicElement)) + } + result.push({ + title: title, + comics: comics, + viewMore: `category:${link}` + }) + } + document.dispose() + return result + } + } + ] + + // categories + category = { + /// title of the category page, used to identify the page, it should be unique + title: "紳士漫畫", + parts: [ + { + // title of the part + name: "最新", + + // 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: ["最新"], + + // 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: ["/albums.html"], + + // [Optional] {string} cannot be used with `categoryParams`, set all category params to this value + groupParam: null, + }, + { + // title of the part + name: "同人誌", + + // 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: ["同人誌", "漢化", "日語", "English", "CG畫集", "3D漫畫", "寫真Cosplay"], + + // 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: [ + "/albums-index-cate-5.html", + "/albums-index-cate-1.html", + "/albums-index-cate-12.html", + "/albums-index-cate-16.html", + "/albums-index-cate-2.html", + "/albums-index-cate-22.html", + "/albums-index-cate-3.html", + ], + + // [Optional] {string} cannot be used with `categoryParams`, set all category params to this value + groupParam: null, + }, + { + // title of the part + name: "單行本", + + // 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: ["單行本", "漢化", "日語", "English",], + + // 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: [ + "/albums-index-cate-6.html", + "/albums-index-cate-9.html", + "/albums-index-cate-13.html", + "/albums-index-cate-17.html", + ], + + // [Optional] {string} cannot be used with `categoryParams`, set all category params to this value + groupParam: null, + }, + { + // title of the part + name: "雜誌短篇", + + // 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: ["雜誌短篇", "漢化", "日語", "English",], + + // 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: [ + "/albums-index-cate-7.html", + "/albums-index-cate-10.html", + "/albums-index-cate-14.html", + "/albums-index-cate-18.html", + ], + + // [Optional] {string} cannot be used with `categoryParams`, set all category params to this value + groupParam: null, + }, + { + // title of the part + name: "韓漫", + + // 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: ["韓漫", "漢化", "生肉",], + + // 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: [ + "/albums-index-cate-19.html", + "/albums-index-cate-20.html", + "/albums-index-cate-21.html", + ], + + // [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 url = this.baseUrl + param + if(page !== 0) { + if (!url.includes("-")) { + url = url.replaceAll(".html", "-.html"); + } + url = url.replaceAll("index", ""); + let lr = url.split("albums-"); + lr[1] = `index-page-${page}${lr[1]}`; + url = `${lr[0]}albums-${lr[1]}`; + } + + let res = await Network.get(url, {}) + if(res.status !== 200) { + throw `Invalid Status Code ${res.status}` + } + let document = new HtmlDocument(res.body) + let comicElements = document.querySelectorAll("div.grid div.gallary_wrap > ul.cc > li") + let comics = [] + for (let comicElement of comicElements) { + comics.push(this.parseComic(comicElement)) + } + let pagesLink = document.querySelectorAll("div.f_left.paginator > a"); + let pages = Number(pagesLink[pagesLink.length-1].text) + document.dispose() + return { + comics: comics, + maxPage: pages, + } + }, + } + + /// 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 url = `${this.baseUrl}/search/?q=${encodeURIComponent(keyword)}&f=_all&s=create_time_DESC&syn=yes` + if(page !== 0) { + url += `&p=${page}` + } + let res = await Network.get(url, {}) + if(res.status !== 200) { + throw `Invalid Status Code ${res.status}` + } + let document = new HtmlDocument(res.body) + let comicElements = document.querySelectorAll("div.grid div.gallary_wrap > ul.cc > li") + let comics = [] + for (let comicElement of comicElements) { + comics.push(this.parseComic(comicElement)) + } + let total = document.querySelectorAll("p.result > b")[0].text.match(/\d+/) + let pages = Math.ceil(Number(total) / 24) + document.dispose() + return { + comics: comics, + maxPage: pages, + } + }, + } + + // favorite related + favorites = { + // whether support multi folders + multiFolder: true, + /** + * 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) => { + if(!isAdding) { + let res = await Network.get(`${this.baseUrl}/users-fav_del-id-${favoriteId}.html?ajax=true&_t=${randomDouble(0, 1)}`, {}) + if(res.status !== 200) { + throw 'Delete failed' + } + } else { + let res = await Network.post(`${this.baseUrl}/users-save_fav-id-${comicId}.html`, { + 'content-type': 'application/x-www-form-urlencoded' + }, `favc_id=${folderId}`) + if(res.status !== 200) { + throw 'Delete failed' + } + } + 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 res = await Network.get(`${this.baseUrl}/users-addfav-id-210814.html`, {}) + if(res.status !== 200) { + throw 'Load failed' + } + let document = new HtmlDocument(res.body) + let data = {} + document.querySelectorAll("option").forEach((option => { + if (option.attributes["value"] === "") return + data[option.attributes["value"]] = option.text + })) + return { + folders: data, + favorited: [] + } + }, + /** + * add a folder + * @param name {string} + * @returns {Promise} - return any value to indicate success + */ + addFolder: async (name) => { + let res = await Network.post(`${this.baseUrl}/users-favc_save-id.html`, { + 'content-type': 'application/x-www-form-urlencoded' + }, `favc_name=${encodeURIComponent(name)}`) + if(res.status !== 200) { + throw 'Add failed' + } + return 'ok' + }, + /** + * delete a folder + * @param folderId {string} + * @returns {Promise} - return any value to indicate success + */ + deleteFolder: async (folderId) => { + let res = await Network.get(`${this.baseUrl}/users-favclass_del-id-${folderId}.html?ajax=true&_t=${randomDouble()}`, {}) + if(res.status !== 200) { + throw 'Delete failed' + } + 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 url = `${this.baseUrl}/users-users_fav-page-${page}-c-${folder}.html.html` + let res = await Network.get(url, {}) + if(res.status !== 200) { + throw `Invalid Status Code ${res.status}` + } + let document = new HtmlDocument(res.body) + let comicBlocks = document.querySelectorAll("div.asTB") + let comics = comicBlocks.map((comic) => { + let cover = comic.querySelector("div.asTBcell.thumb > div > img").attributes["src"] + cover = 'https:' + cover + let time = comic.querySelector("div.box_cel.u_listcon > p.l_catg > span").text.replaceAll("創建時間:", "") + let name = comic.querySelector("div.box_cel.u_listcon > p.l_title > a").text; + let link = comic.querySelector("div.box_cel.u_listcon > p.l_title > a").attributes["href"]; + let id = RegExp("(?<=-aid-)[0-9]+").exec(link)[0]; + let info = comic.querySelector("div.box_cel.u_listcon > p.l_detla").text; + let pages = Number(RegExp("(?<=頁數:)[0-9]+").exec(info)[0]) + let delUrl = comic.querySelector("div.box_cel.u_listcon > p.alopt > a").attributes["onclick"]; + let favoriteId = RegExp("(?<=del-id-)[0-9]+").exec(delUrl)[0]; + return new Comic({ + id: id, + title: name, + subtitle: time, + cover: cover, + pages: pages, + favoriteId: favoriteId, + }) + }) + let pages = 1 + let pagesLink = document.querySelectorAll("div.f_left.paginator > a") + if(pagesLink.length > 0) { + pages = Number(pagesLink[pagesLink.length-1].text) + } + document.dispose() + return { + comics: comics, + maxPage: pages, + } + } + } + + /// single comic related + comic = { + /** + * load comic info + * @param id {string} + * @returns {Promise} + */ + loadInfo: async (id) => { + let res = await Network.get(`${this.baseUrl}/photos-index-page-1-aid-${id}.html`, {}) + if(res.status !== 200) { + throw `Invalid Status Code ${res.status}` + } + let document = new HtmlDocument(res.body) + let title = document.querySelector("div.userwrap > h2").text + let cover = document.querySelector("div.userwrap > div.asTB > div.asTBcell.uwthumb > img").attributes["src"] + cover = 'https:' + cover + cover = cover.substring(0, 6) + cover.substring(8) + let labels = document.querySelectorAll("div.asTBcell.uwconn > label") + let category = labels[0].text.split(":")[1] + let pages = Number(RegExp("\\d+").exec(labels[1].text.split(":")[1])[0]); + let tagsDom = document.querySelectorAll("a.tagshow"); + let tags = new Map() + tags.set("分類", [category]) + if(tagsDom.length > 0) { + tags.set("標籤", tagsDom.map((e) => e.text)) + } + let description = document.querySelector("div.asTBcell.uwconn > p").text; + let uploader = document.querySelector("div.asTBcell.uwuinfo > a > p").text; + + return new ComicDetails({ + id: id, + title: title, + cover: cover, + pages: pages, + tags: tags, + description: description, + uploader: uploader, + }) + }, + /** + * [Optional] load thumbnails of a comic + * @param id {string} + * @param next {string | null | undefined} - 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) => { + next = next || '1' + let res = await Network.get(`${this.baseUrl}/photos-index-page-${next}-aid-${id}.html`, {}); + if(res.status !== 200) { + throw `Invalid Status Code ${res.status}` + } + let document = new HtmlDocument(res.body) + let thumbnails = document.querySelectorAll("div.pic_box.tb > a > img").map((e) => { + return 'https:' + e.attributes["src"] + }) + next = (Number(next)+1).toString() + let pagesLink = document.querySelector("div.f_left.paginator").children + if(pagesLink[pagesLink.length-1].classNames.includes("thispage")) { + next = null + } + return { + thumbnails: thumbnails, + next: next + } + }, + /** + * load images of a chapter + * @param comicId {string} + * @param epId {string?} + * @returns {Promise<{images: string[]}>} + */ + loadEp: async (comicId, epId) => { + let res = await Network.get(`${this.baseUrl}/photos-gallery-aid-${comicId}.html`, {}) + if(res.status !== 200) { + throw `Invalid Status Code ${res.status}` + } + const regex = RegExp(String.raw`//[^"]+/[^"]+\.[^"]+`, 'g'); + const matches = Array.from(res.body.matchAll(regex)); + return { + images: matches.map((e) => 'https:' + e[0].substring(0, e[0].length-1)) + } + }, + /** + * [Optional] Handle tag click event + * @param namespace {string} + * @param tag {string} + * @returns {{action: string, keyword: string, param: string?}} + */ + onClickTag: (namespace, tag) => { + return { + action: 'search', + keyword: tag, + } + }, + } + + settings = { + domain: { + title: "Domain", + type: "input", + validator: '^(?!:\\/\\/)(?=.{1,253})([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$', + default: 'www.wnacg.com', + }, + } +} \ No newline at end of file