├── LICENSE ├── README.md └── examples ├── dmzj ├── README.md └── index.js ├── ehentai ├── README.md └── index.js ├── nhentai ├── README.md └── index.js ├── readme.md └── zh_TW ├── index.js └── language.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 typehm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kola Manga Reader插件文档 2 | 3 | Kola Manga Reader支持通过编写插件来扩展系统功能。目前支持对以下的功能进行扩展 4 | - 界面语言 5 | - 远程书籍源 6 | - 本地文件源 7 | 8 | 9 | # 警告 10 | 使用自行或他人编写的插件产生各种后果(包括但不限于用户系统或数据被侵害,产生某些费用,各种权利侵害等)都由使用者自行负担。 11 | 请在明确的知晓自己的行为后再开始插件的使用和开发。 12 | 13 | 14 | 15 | 16 | 插件结构 17 | ---- 18 | 19 | ```js 20 | + 插件目录 21 | |--- index.js 22 | |--- readme.md 23 | |--- 其他依赖文件 24 | ``` 25 | 一个插件将一个文件夹以zip的形式发布。其中将包含: 26 | 27 | - index.js 插件入口,需要按要求导出一个class 28 | - readme.md 插件的readme(可选)。用于在选项页面时显示插件信息。当文件不存在时,使用导出的class的description字段 29 | - 插件依赖的资源 30 | 31 | ### index.js 32 | 插件入口,需要按要求导出一个ProtocolBase的派生类 33 | ```js 34 | //基本的index.js例子 35 | //__common__是针对特定路径的require的封装方法,注意模块名的大小写问题,否则会导致模块重复加载 36 | const {PluginBase} = __common__("Plugin"); 37 | //定义一个ProtocolBase的派生类 38 | class dummy_Protocol extends PluginBase { 39 | name = "dummy"; 40 | description = "dummy"; 41 | } 42 | //导出上面定义的派生类 43 | module.exports = { 44 | default : dummy_Protocol, 45 | } 46 | ``` 47 | 上面的例子定义了一个没有任何功能的插件,仅仅是能做为一个合法的插件导入而已。 48 | 49 | #### 下面简单几类基本插件的编写的例子。 50 | # 语言扩展 51 | 语言扩展能为系统增加新的界面显示语言的支持 52 | 原理是在index.js中注册新的语言类型,并为其加载相应的json文件 53 | 一个语言扩展插件包括以下文件 54 | ```js 55 | + zh_TW 56 | |--- index.js 57 | |--- readme.md 58 | |--- language.json 59 | ``` 60 | #### index.js 61 | ```js 62 | const {LanguagePack} = __common__("language"); 63 | //派生一个语言包子类,并设置相应信息,构造时会自动添加入语言模块中 64 | class zh_TW_Protocol extends LanguagePack { 65 | name = "繁體中文語言包"; 66 | description = "繁體中文的語言界面支援包"; 67 | languageCodeName = "zh-TW"; 68 | languageName = "繁體中文"; 69 | languageJsonPath = __dirname + "/language.json"; 70 | } 71 | 72 | //导出声明的语言包类 73 | module.exports = { 74 | zh_TW_Protocol, 75 | default : zh_TW_Protocol, 76 | } 77 | ``` 78 | 79 | ### language.json 80 | ```json 81 | { 82 | "zh-TW": [ 83 | {"key": "common.name", "value": "通用"}, 84 | {"key": "common.language", "value": "當前語言"}, 85 | {"key": "common.devMode", "value": "開發者模式"}, 86 | {"key": "common.devmode.nodrag", "value": "打開開發者工具時視窗將無法被拖移"}, 87 | ... 88 | ... 89 | {"key": "config.uninstall", "value": "卸載"}, 90 | {"key": "config.filterplaceholder", "value": "查找插件"}, 91 | {"key": "form.path.select", "value": "選擇路徑"}, 92 | {"key": "form.path.open", "value": "打開路徑"}, 93 | {"key": "plugins.name", "value": "插件"} 94 | ] 95 | } 96 | ``` 97 | 98 | # 远程书籍源 99 | >远程书籍源能为系统增加新的在线书籍源 100 | 原理是通过插件请求远程的http源,获得书籍信息后,封装成系统能识别的书籍数据。 101 | 一个书籍源扩展插件包括以下文件 102 | ```js 103 | + dmzj 104 | |--- index.js 105 | |--- readme.md 106 | |--- 依赖文件 107 | ``` 108 | ### index.js 109 | >插件入口,需要按要求导出一个ProtocolBase的派生类 110 | 并重载ProtocolBase中的方法,实现对远程数据的请求/解析/封装 111 | 插件的编写将会使用到以下的类 112 | ```js 113 | //书籍源的基类 114 | class ProtocolBase extends PluginBase { 115 | name = ""; 116 | description = ""; 117 | protocols = "protocols://;"; 118 | isSource = false; 119 | icon = ""; 120 | reset() {} 121 | get(filter, isContinue) {} 122 | open(uri) {} 123 | close(book) {} 124 | } 125 | ``` 126 | 127 | #### ProtocolBase.name : string 128 | 插件的名称,用于显示在选项页中 129 | #### ProtocolBase.description : string 130 | 插件的描述,当插件没有提供readme.md时,将这个字段显示为插件的描述 131 | #### ProtocolBase.isSource : boolean 132 | 是否为一个书籍源,当只有isSource为true时,才会被当做一个源来注册入系统 133 | #### ProtocolBase.icon : string 134 | 源的图标的url。用于显示在书库界面中 135 | #### ProtocolBase.reset() 136 | 重置状态(目前没有使用) 137 | #### ProtocolBase.get( filter : string, isContinue : boolean ) : Promise({}) 138 | 从远程服务器获得书籍列表 139 | 此方法应该返回一个Promise,成功时返回一个如下结构的dict,失败时可以简单的返回错误信息字符串或是异常对象 140 | ```js 141 | { 142 | filter: filter, //当前请求的filter参数 143 | pageNo: pageNo, //当前请求的页号 144 | books : [], //当前请求得到的书籍信息的array 145 | hasMore : false, //当前请求之后,是否还有更多的结果可以请求 146 | } 147 | ``` 148 | books中的数据结构 149 | ```js 150 | { 151 | name: "", //书籍的显示用的名字 152 | path: "", //书籍的path,为protocols中的某一个值+书籍的标识符组成的一串唯一的路径,由插件产生并解析 153 | thumbnail: "" //书籍的封面缩略图的url 154 | } 155 | ``` 156 | 157 | filter参数是由空格分隔开的关键词,如果关键词以$开始,则是一个tag,远程源可以简单的将$移除,或是将其接入对应的远端的搜索系统中 158 | isCountinue参数表示是否继续上一次的搜索。即如果上一次的搜索返回了pageNo为0并且hasMore为true的结果,则这次应该开始pageNo为1的结果集的获取 159 | 160 | #### ProtocolBase.open( uri : string) : Promise(BookDetail) 161 | 从远程服务器获得uri对应的书籍的详细信息并组织成一个BookDetail返回。 162 | 此方法应该返回一个Promise,成功时返回一个BookDetail,失败时可以简单的返回错误信息字符串或是异常对象 163 | uri即get方法中返回的book的path 164 | 165 | ```js 166 | //书籍详细信息的基类 167 | //仅展示需要由插件填入的成员及需要重载的方法 168 | class BookDetail { 169 | id = 0; //书籍的id 170 | name = ""; //书籍的名称 171 | path = ""; //书籍的path,应该与get方法中返回的一致 172 | thumbnail = "name"; //书籍的封面url 173 | category = [BookChapter] ; //书籍的分类 174 | tags = []; //标签列表 175 | vertical = false; //是否是纵向阅读 176 | LoadChapter( categoryIdx : int, chapterIdx : int ) : Promise(BookChapter) //读取一个指定的category中的chapter 177 | LoadImage( page : BookPageInfo ) : Promise(Blob) //读取并返回page指定的页的图像blob对象 178 | } 179 | 180 | //表示一个具体的章节 181 | class BookChapter 182 | { 183 | name = ""; //章节名字,显示用 184 | id = 0; //保留,未使用 185 | order = 0; //章节顺序,对章节排序用 186 | pages = []; //章节之下所有的页的array,具体定义由插件自行解释 187 | constructor( id, name, order ) 188 | } 189 | 190 | //表示一个书籍中的一个具体的分类 191 | //用在动漫之家这类会在一个书籍下有多个版本的情况,比如不同汉化组或是连载与单行本的分别 192 | class BookCategory 193 | { 194 | id = 0; //保留,未使用 195 | name = "name"; //分类名字 196 | chapters = []; //分类下的章节 197 | constructor(name, id); 198 | } 199 | 200 | //表示一个书籍中的一个具体的页的位置 201 | class BookPageInfo { 202 | category = 0; //分类索引 203 | chapter = 0; //章节索引 204 | page = 0; //页面索引 205 | constructor( category, chapter, page); 206 | } 207 | ``` 208 | 209 | ###### BookDetail.LoadChapter( categoryIdx : int, chapterIdx : int ) : Promise(BookChapter) 210 | 读取一个指定的category中的chapter。用于无法在ProtocolBase.open方法中得到具体的章节细节数据时使用。 211 | 有些情况下,你可能只能在open的请求中得到章节的数目,而不能得到其他的详细信息。当开始阅读时,会先调用LoadChapter来准备好chapter中的数据,然后返回给系统。 212 | 213 | ###### BookDetail.LoadImage( page : BookPageInfo ) : Promise(Blob) 214 | 读取并返回page指定的页的图的Blob对象。 215 | 216 | 217 | 218 | #### ProtocolBase.close( book : BookDetail) : Promise 219 | 关闭打开的书籍源,以释放相应的资源。 220 | 对于基于http的源而言,基本不需要关注此方法。 221 | 主要用于支持本地文件做为源时,关闭打开的文件之类的资源。 222 | 223 | 224 | 225 | [参考插件例子](https://github.com/typehm/KolaMangaReader-plugins/tree/main/examples) 226 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /examples/dmzj/README.md: -------------------------------------------------------------------------------- 1 | # 动漫之家 2 | 3 | 使用动漫之家做为在线书籍源 4 | 5 | License 6 | ---- 7 | **Free Software** 8 | -------------------------------------------------------------------------------- /examples/dmzj/index.js: -------------------------------------------------------------------------------- 1 | const {Util} = __common__("util"); 2 | const {ProtocolBase} = __common__("protocol"); 3 | const {BookDetail, BookChapter, BookCategory} = __common__("book"); 4 | 5 | class DmzjBookDetail extends BookDetail { 6 | 7 | LoadImage(page) { 8 | //先加载章节数据,章节数据加载成功时,开始加载图像数据 9 | return this.LoadChapter(page.category, page.chapter).then(chapter => { 10 | var url = chapter.pages[page.page]; 11 | return Util.downloadImageToBlob(url, "http://images.dmzj.com/"); 12 | }); 13 | } 14 | 15 | loadChapterPromise = null; 16 | 17 | LoadChapter(categoryIdx, chapterIdx) { 18 | return new Promise((resolve, reject) => { 19 | var chapter = this.category[categoryIdx].chapters[chapterIdx]; 20 | 21 | //判断章节是否己经在加载中,是的话等待加载完成后再返回,否则会出现重复加载章节数据 22 | if ( chapter.loadChapterPromise ) 23 | resolve( Promise.all( [chapter.loadChapterPromise] ) ).then( ()=> chapter ); 24 | 25 | //判断是否已经是取得过pages数据的章节,如果是的话,直接返回 26 | if (chapter.pages.length != 0) 27 | resolve(chapter); 28 | else { 29 | //如果pages的length为0,那么通过api出取得章节中的详细信息并填入chapter中,然后返回 30 | var url = "http://v2.api.dmzj.com/chapter/" + this.id + "/" + chapter.id + ".json"; 31 | chapter.loadChapterPromise = fetch(url).then(resp => resp.json()) 32 | .then(json => { 33 | json.page_url.forEach(pUrl => { 34 | chapter.pages.push(pUrl); 35 | }); 36 | chapter.loadChapterPromise = null; 37 | resolve(chapter); 38 | }); 39 | } 40 | }); 41 | } 42 | 43 | 44 | Close() { 45 | } 46 | } 47 | 48 | 49 | class DMZJProtocol extends ProtocolBase { 50 | name = "动漫之家"; 51 | description = "**动漫之家**\n========="; 52 | protocols = "dmzj://"; 53 | isSource = true; 54 | referer = 'https://manhua.dmzj.com'; 55 | icon = "https://www.dmzj.com/favicon.ico"; 56 | 57 | //最后一次的搜索结果 58 | lastResult = null; 59 | 60 | get(filter, isContinue) { 61 | //将tag替换为普通的关键词 62 | filter = filter.replace("$", ""); 63 | return new Promise((resolve, reject) => { 64 | //如果继续上一次的搜索并且有最后一次的搜索结果,那么pageNo从上一次的结果增1,否则从0开始 65 | var pageNo = isContinue && this.lastResult ? this.lastResult.pageNo + 1 : 0; 66 | 67 | //请求dmzj书籍列表的url 68 | var url = 'http://v2.api.dmzj.com/classify/0/0/' + pageNo + '.json'; 69 | //如果带有filter,则url替换为搜索用的url 70 | if (filter) 71 | url = "http://s.acg.dmzj.com/comicsum/search.php?s=" + filter; 72 | 73 | //访问url 74 | fetch(url).then(resp => { 75 | //如果是搜索的结果,那么将它转换为与普通的情况一致的格式 76 | if (filter) { 77 | return resp.text().then(text => { 78 | if (!text) 79 | return []; 80 | text = text.replace("];", "]").replace("var g_search_data = ", ""); 81 | var ret = JSON.parse(text); 82 | ret.forEach(item => { 83 | item.title = item.comic_name; 84 | }); 85 | return ret; 86 | }); 87 | } else 88 | return resp.json(); 89 | }).then(json => { 90 | var result = { 91 | filter: filter, 92 | pageNo: pageNo, 93 | }; 94 | //把从上一个阶段中的json数据转换为book数据 95 | var books = []; 96 | json.forEach(item => { 97 | var book = { 98 | name: item.title, 99 | path: "dmzj://" + item.id, 100 | thumbnail: item.cover, 101 | }; 102 | books.push(book); 103 | }); 104 | result.books = books; 105 | result.hasMore = !filter ? books.length > 0 : false; 106 | this.lastResult = result; 107 | resolve(result); 108 | }).catch( resolve ); 109 | }); 110 | } 111 | 112 | open(uri) { 113 | //uri是上面get方法中已经去掉了dmzj://的path数据 114 | var self = this; 115 | return new Promise((resolve, reject) => { 116 | var id = uri; 117 | //从uri构造出这个书籍的实际url 118 | var url = "http://v2.api.dmzj.com/comic/" + id + ".json"; 119 | console.log(url); 120 | fetch(url).then(resp => { 121 | return resp.json(); 122 | }).then(json => { 123 | console.log(json); 124 | //构造BookDetail 125 | var book = new DmzjBookDetail(); 126 | book.id = id; 127 | book.name = json.title; 128 | book.referer = this.referer; 129 | book.thumbnail = json.cover; 130 | book.tags = json.types.map( i => { 131 | return {name: i.tag_name}; 132 | }); 133 | book.vertical = json.islong == 1; 134 | book.lastUpdate = Util.timestampToDate( json.last_updatetime); 135 | 136 | //为detail填充章节数据 137 | json.chapters.map(chapter => { 138 | var category = new BookCategory(chapter.title); 139 | book.category.push(category); 140 | chapter.data.forEach(c => { 141 | var _c = new BookChapter(c.chapter_id, c.chapter_title, c.chapter_order); 142 | category.chapters.push(_c); 143 | }); 144 | category.chapters.sort((a, b) => a.order - b.order); 145 | }); 146 | resolve(book); 147 | }).catch(e=>{ 148 | console.log(e); 149 | //某些情况下会出错,如被隐藏的漫画之类的,可以列出但是无法用api打开 150 | //由于无法知道具体原理,简单返回不存在 151 | reject( "漫画不存在!" ); 152 | }) 153 | }); 154 | } 155 | 156 | }; 157 | 158 | 159 | 160 | module.exports = { 161 | DMZJProtocol, 162 | default : DMZJProtocol, 163 | } 164 | -------------------------------------------------------------------------------- /examples/ehentai/README.md: -------------------------------------------------------------------------------- 1 | # e-hentai 2 | 3 | 使用e-hentai做为在线书籍源 4 | 5 | License 6 | ---- 7 | **Free Software** 8 | -------------------------------------------------------------------------------- /examples/ehentai/index.js: -------------------------------------------------------------------------------- 1 | const {Util} = __common__("util"); 2 | const {ProtocolBase} = __common__("protocol"); 3 | const {BookDetail, BookCategory, BookChapter} = __common__("book"); 4 | 5 | class EHentaiBookDetail extends BookDetail { 6 | protocol = null; 7 | totalPages = 0; 8 | imagePerPage = 0; 9 | pagesURL = ""; 10 | 11 | LoadImage(page) { 12 | return this.LoadChapter(page.category, page.chapter).then(chapter => { 13 | var url = chapter.pages[page.page]; 14 | return new Promise((resolve, reject) => { 15 | //如果已经有url了,返回给下一个阶段 16 | if ( url ) 17 | resolve( url ); 18 | else 19 | //如果还没有url,则要先提取。因为eh的一个漫画里可能分很多页,而事先提供全部url会导致打开很慢,所以采取使用时再提取的方法 20 | //技术上可以采取更主动的预读策略来隐藏这个过程,这里只是演示则不深入 21 | { 22 | //从page推出在哪一个分页,然后构造分页url 23 | var p = Math.floor( page.page / this.imagePerPage ); 24 | var si = p * this.imagePerPage; 25 | var purl = this.pagesURL + "?p="+p; 26 | //请求url 27 | fetch(purl).then( resp => resp.text()) 28 | .then( html => { 29 | //解析html,得到这个分页上的图像的url,保存起来 30 | var parser = new DOMParser(); 31 | var doc = parser.parseFromString(html, "text/html"); 32 | var imgs = doc.querySelectorAll("div.gdtm a"); 33 | imgs.forEach( (v,i) => { 34 | chapter.pages[ si + i ] = v.href; 35 | }); 36 | //返回需要读取的页面url 37 | resolve( chapter.pages[page.page ] ); 38 | }); 39 | } 40 | }) 41 | .then(url => { 42 | return fetch(url) 43 | }) 44 | .then(resp => resp.text()).then(html => { 45 | //解析实际的图像页面 46 | var parser = new DOMParser(); 47 | var doc = parser.parseFromString(html, "text/html"); 48 | var img = doc.querySelectorAll("img#img"); 49 | var src = img[0].src; 50 | //加载图像并返回blob 51 | return Util.downloadImageToBlob( src ); 52 | }); 53 | }); 54 | } 55 | 56 | //chpater数据在open时已经完整的得到,所以直接返回相应的chapter 57 | LoadChapter(categoryIdx, chapterIdx) { 58 | return new Promise((resolve, reject) => { 59 | var chapter = this.category[categoryIdx].chapters[chapterIdx]; 60 | resolve(chapter); 61 | }); 62 | } 63 | 64 | 65 | Close() { 66 | } 67 | } 68 | 69 | 70 | class EHentaiProtocol extends ProtocolBase{ 71 | name = "E-Hentai"; 72 | description = "E-Hentai\n=========\n\n\nLicense\n-------"; 73 | 74 | protocols = "ehentai://"; 75 | isSource = true; 76 | enable = true; 77 | referer = ''; 78 | icon = "https://static.nhentai.net/img/logo.090da3be7b51.svg"; 79 | lastResult = null; 80 | 81 | get(filter, isContinue) { 82 | return new Promise((resolve, reject) => { 83 | //如果继续上一次的搜索并且有最后一次的搜索结果,那么pageNo从上一次的结果增1,否则从0开始 84 | var pageNo = isContinue && this.lastResult ? this.lastResult.pageNo + 1 : 0; 85 | 86 | //请求书籍列表的url 87 | var url = 'https://e-hentai.org/'; 88 | //如果带有filter,则url替换为搜索用的url 89 | if (filter) 90 | url += "?f_search=" + filter + "&page=" + pageNo; 91 | else 92 | url += "?&page=" + pageNo; 93 | 94 | console.log(url); 95 | fetch(url).then(resp => { 96 | return resp.text(); 97 | }).then(html => { 98 | var result = { 99 | filter: filter, 100 | pageNo: pageNo, 101 | }; 102 | 103 | //解析html页面,得到相关的信息 104 | var books = []; 105 | var parser = new DOMParser(); 106 | var doc = parser.parseFromString(html, "text/html"); 107 | var all = doc.querySelectorAll("tr"); 108 | var tmp = doc.querySelectorAll(".ptb td a"); 109 | var pages = parseInt( $( tmp[ tmp.length -2 ] ).text() ); 110 | 111 | //解析并构造books中的数据 112 | all.forEach( item => { 113 | var gl = $(".glname>a", item); 114 | if (gl.length == 0) return; 115 | 116 | var n = $(".glthumb div img", item); 117 | var cover = n.attr("data-src") || n.attr("src"); 118 | var url = gl.attr("href"); 119 | var name = $(".glink", gl).text(); 120 | var book = { 121 | name: name, 122 | path: "ehentai://" + url.replace("https://e-hentai.org/", ""), 123 | thumbnail: cover, 124 | }; 125 | books.push(book); 126 | }); 127 | 128 | result.books = books; 129 | result.hasMore = pageNo < pages; 130 | this.lastResult = result; 131 | 132 | resolve(result); 133 | }); 134 | }); 135 | } 136 | 137 | 138 | parseDetail(html) { 139 | var parser = new DOMParser(); 140 | var doc = parser.parseFromString(html, "text/html"); 141 | 142 | var pages = []; 143 | var totalPages = parseInt($(doc.querySelectorAll("#gd3 #gdd .gdt2")[5]).text().replace(" pages", "")); 144 | for (var i = 0; i < totalPages; i++) 145 | pages.push(""); 146 | 147 | var thumbnail = doc.querySelectorAll("#gleft #gd1 div")[0].style.backgroundImage.replace("url(\"", "").replace("\")", ""); 148 | 149 | var tags = $(doc.querySelectorAll("#taglist .gtl a")).map( (i,v) =>{ 150 | return { name : v.innerText } 151 | } ).get(); 152 | 153 | return { 154 | name: $(doc.querySelectorAll("h1#gn")).text(), 155 | thumbnail: thumbnail, 156 | totalPages: totalPages, 157 | imagePerPage: doc.querySelectorAll("div.gdtm a").length, 158 | pages: pages, 159 | pageUrl: $(doc.querySelectorAll(".ptds a")).attr("href"), 160 | tags : tags, 161 | } 162 | } 163 | 164 | 165 | open(uri) { 166 | return new Promise((resolve, reject) => { 167 | var url = "https://e-hentai.org/" + uri; 168 | var self = this; 169 | fetch(url).then(resp => { 170 | return resp.text(); 171 | }).then(html => { 172 | console.log(url); 173 | var book = new EHentaiBookDetail(); 174 | book.category = []; 175 | book.protocol = self; 176 | 177 | //因为此站书籍下没有子分类及章节,所以构造一个分类和一个章节 178 | var category = new BookCategory(); 179 | category.name = ""; 180 | book.category.push(category); 181 | 182 | var chapter = new BookChapter(); 183 | category.chapters.push(chapter); 184 | 185 | // chapter.html = html; 186 | 187 | var detail = this.parseDetail(html); 188 | console.log(detail); 189 | 190 | book.name = detail.name; 191 | book.thumbnail = detail.thumbnail; 192 | book.totalPages = detail.totalPages; 193 | book.imagePerPage = detail.imagePerPage; 194 | book.pagesURL = detail.pageUrl; 195 | book.tags = detail.tags; 196 | 197 | chapter.pages = detail.pages; 198 | 199 | resolve(book); 200 | }); 201 | }); 202 | } 203 | }; 204 | 205 | 206 | 207 | module.exports = { 208 | EHentaiProtocol, 209 | default : EHentaiProtocol, 210 | } 211 | -------------------------------------------------------------------------------- /examples/nhentai/README.md: -------------------------------------------------------------------------------- 1 | # nhentai 2 | 3 | 使用nhentai做为在线书籍源 4 | 5 | License 6 | ---- 7 | **Free Software** 8 | -------------------------------------------------------------------------------- /examples/nhentai/index.js: -------------------------------------------------------------------------------- 1 | const {Util} = __common__("util"); 2 | const {ProtocolBase} = __common__("protocol"); 3 | const {BookDetail, BookCategory, BookChapter} = __common__("book"); 4 | 5 | class NHentaiBookDetail extends BookDetail { 6 | 7 | //chpater数据在open时已经完整的得到,所以直接返回相应的chapter 8 | LoadChapter(categoryIdx, chapterIdx) { 9 | return new Promise((resolve, reject) => { 10 | var chapter = this.category[categoryIdx].chapters[chapterIdx]; 11 | resolve(chapter); 12 | }); 13 | } 14 | 15 | LoadImage(page) { 16 | return this.LoadChapter(page.category, page.chapter).then(chapter => { 17 | var url = 'https://nhentai.net' + chapter.pages[page.page]; 18 | return fetch(url).then(resp => resp.text()) 19 | .then(html => { 20 | //解析page对应的url,再从页面中提供src 21 | var parser = new DOMParser(); 22 | var doc = parser.parseFromString(html, "text/html"); 23 | var all = doc.querySelectorAll("#image-container img"); 24 | var src = $(all[0]).attr("src"); 25 | //下载src的图像并返回 26 | return Util.downloadImageToBlob(src); 27 | }); 28 | }); 29 | } 30 | } 31 | 32 | 33 | class NHentaiProtocol extends ProtocolBase { 34 | name = "N-Hentai"; 35 | description = "NHentai\n=========\n\n\nLicense\n-------"; 36 | 37 | protocols = "nhentai://"; 38 | isSource = true; 39 | referer = ''; 40 | lastResult = null; 41 | icon = "https://static.nhentai.net/img/logo.090da3be7b51.svg"; 42 | enable = true; 43 | 44 | 45 | paserPaginationPage(doc, sel, attr, sp) { 46 | try { 47 | var e = $(doc.querySelectorAll(sel)); 48 | var a = e.attr(attr); 49 | var v = parseInt(a.split(sp)[1]); 50 | return v; 51 | }catch (e) { 52 | } 53 | return 0; 54 | } 55 | 56 | get(filter, isContinue) { 57 | return new Promise((resolve, reject) => { 58 | //如果继续上一次的搜索并且有最后一次的搜索结果,那么pageNo从上一次的结果增1,否则从0开始 59 | var pageNo = isContinue && this.lastResult ? this.lastResult.pageNo + 1 : 1; 60 | 61 | //请求书籍列表的url 62 | var url = 'https://nhentai.net/'; 63 | //如果带有filter,则url替换为搜索用的url 64 | if (filter) 65 | url += "search/?q=" + filter + "&page=" + pageNo; 66 | else 67 | url += "?page=" + pageNo; 68 | 69 | //访问url 70 | fetch(url).then(resp => { 71 | return resp.text(); 72 | }).then(html => { 73 | var result = { 74 | filter: filter, 75 | pageNo: pageNo, 76 | }; 77 | var books = []; 78 | //解析html页面,得到相关的信息 79 | var parser = new DOMParser(); 80 | var doc = parser.parseFromString(html, "text/html"); 81 | var all = doc.querySelectorAll(".gallery"); 82 | var pages = this.paserPaginationPage(doc, ".pagination .last", "href", "page="); 83 | 84 | //解析并构造books中的数据 85 | all.forEach(item => { 86 | var cover = $(".cover img", item).attr("data-src"); 87 | var url = $(".cover", item).attr("href"); 88 | var name = $(".caption", item).text(); 89 | var book = { 90 | name: name, 91 | path: "nhentai://" + url, 92 | thumbnail: cover, 93 | }; 94 | books.push(book); 95 | }); 96 | 97 | result.books = books; 98 | result.hasMore = pageNo < pages; 99 | this.lastResult = result; 100 | resolve(result); 101 | }); 102 | }); 103 | } 104 | 105 | 106 | parseDetail(html) { 107 | var parser = new DOMParser(); 108 | var doc = parser.parseFromString(html, "text/html"); 109 | var all = doc.querySelectorAll(".thumb-container .gallerythumb"); 110 | var pages = []; 111 | all.forEach((i) => { 112 | var url = $(i).attr("href"); 113 | pages.push(url); 114 | }); 115 | 116 | var tmp = $(doc.querySelectorAll("#tags .tag-container")).get(2); 117 | var tags = $( ".tags .tag .name", tmp ).map( (i,v) => { 118 | return { name : v.innerText }; 119 | }).get(); 120 | 121 | return { 122 | name: $(doc.querySelectorAll(".title span")).text(), 123 | thumbnail: $(doc.querySelectorAll("#content #cover img")).attr("data-src"), 124 | pages: pages, 125 | tags : tags, 126 | } 127 | } 128 | 129 | open(uri) { 130 | return new Promise((resolve, reject) => { 131 | var url = "https://nhentai.net" + uri; 132 | var self = this; 133 | fetch(url).then(resp => { 134 | return resp.text(); 135 | }).then(html => { 136 | var book = new NHentaiBookDetail(); 137 | book.category = []; 138 | book.protocol = self; 139 | book.url = url; 140 | 141 | //因为此站书籍下没有子分类及章节,所以构造一个分类和一个章节 142 | var category = new BookCategory("", 0); 143 | category.name = ""; 144 | book.category.push(category); 145 | 146 | var chapter = new BookChapter(0, "", 0); 147 | category.chapters.push(chapter); 148 | 149 | //构造BookDetail 150 | var detail = this.parseDetail(html); 151 | book.name = detail.name; 152 | book.thumbnail = detail.thumbnail; 153 | book.tags = detail.tags; 154 | 155 | chapter.pages = detail.pages; 156 | 157 | resolve(book); 158 | }); 159 | }); 160 | } 161 | }; 162 | 163 | 164 | 165 | module.exports = { 166 | NHentaiProtocol, 167 | default : NHentaiProtocol, 168 | } 169 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # Kola Manga Reader插件文档 2 | 3 | Kola Manga Reader支持通过编写插件来扩展系统功能。目前支持对以下的功能进行扩展 4 | - 界面语言 5 | - 远程书籍源 6 | - 本地文件源 7 | 8 | 9 | # 警告 10 | 使用自行或他人编写的插件产生各种后果(包括但不限于用户系统或数据被侵害,产生某些费用,各种权利侵害等)都由使用者自行负担。 11 | 请在明确的知晓自己的行为后再开始插件的使用和开发。 12 | 13 | 14 | 15 | 16 | 插件结构 17 | ---- 18 | 19 | ```js 20 | + 插件目录 21 | |--- index.js 22 | |--- readme.md 23 | |--- 其他依赖文件 24 | ``` 25 | 一个插件将一个文件夹以zip的形式发布。其中将包含: 26 | 27 | - index.js 插件入口,需要按要求导出一个class 28 | - readme.md 插件的readme(可选)。用于在选项页面时显示插件信息。当文件不存在时,使用导出的class的description字段 29 | - 插件依赖的资源 30 | 31 | ### index.js 32 | 插件入口,需要按要求导出一个ProtocolBase的派生类 33 | ```js 34 | //基本的index.js例子 35 | //__common__是针对特定路径的require的封装方法,注意模块名的大小写问题,否则会导致模块重复加载 36 | const {PluginBase} = __common__("Plugin"); 37 | //定义一个ProtocolBase的派生类 38 | class dummy_Protocol extends PluginBase { 39 | name = "dummy"; 40 | description = "dummy"; 41 | } 42 | //导出上面定义的派生类 43 | module.exports = { 44 | default : dummy_Protocol, 45 | } 46 | ``` 47 | 上面的例子定义了一个没有任何功能的插件,仅仅是能做为一个合法的插件导入而已。 48 | 49 | #### 下面简单几类基本插件的编写的例子。 50 | # 语言扩展 51 | 语言扩展能为系统增加新的界面显示语言的支持 52 | 原理是在index.js中注册新的语言类型,并为其加载相应的json文件 53 | 一个语言扩展插件包括以下文件 54 | ```js 55 | + zh_TW 56 | |--- index.js 57 | |--- readme.md 58 | |--- language.json 59 | ``` 60 | #### index.js 61 | ```js 62 | const {LanguagePack} = __common__("language"); 63 | //派生一个语言包子类,并设置相应信息,构造时会自动添加入语言模块中 64 | class zh_TW_Protocol extends LanguagePack { 65 | name = "繁體中文語言包"; 66 | description = "繁體中文的語言界面支援包"; 67 | languageCodeName = "zh-TW"; 68 | languageName = "繁體中文"; 69 | languageJsonPath = __dirname + "/language.json"; 70 | } 71 | 72 | //导出声明的语言包类 73 | module.exports = { 74 | zh_TW_Protocol, 75 | default : zh_TW_Protocol, 76 | } 77 | ``` 78 | 79 | ### language.json 80 | ```json 81 | { 82 | "zh-TW": [ 83 | {"key": "common.name", "value": "通用"}, 84 | {"key": "common.language", "value": "當前語言"}, 85 | {"key": "common.devMode", "value": "開發者模式"}, 86 | {"key": "common.devmode.nodrag", "value": "打開開發者工具時視窗將無法被拖移"}, 87 | ... 88 | ... 89 | {"key": "config.uninstall", "value": "卸載"}, 90 | {"key": "config.filterplaceholder", "value": "查找插件"}, 91 | {"key": "form.path.select", "value": "選擇路徑"}, 92 | {"key": "form.path.open", "value": "打開路徑"}, 93 | {"key": "plugins.name", "value": "插件"} 94 | ] 95 | } 96 | ``` 97 | 98 | # 远程书籍源 99 | >远程书籍源能为系统增加新的在线书籍源 100 | 原理是通过插件请求远程的http源,获得书籍信息后,封装成系统能识别的书籍数据。 101 | 一个书籍源扩展插件包括以下文件 102 | ```js 103 | + dmzj 104 | |--- index.js 105 | |--- readme.md 106 | |--- 依赖文件 107 | ``` 108 | ### index.js 109 | >插件入口,需要按要求导出一个ProtocolBase的派生类 110 | 并重载ProtocolBase中的方法,实现对远程数据的请求/解析/封装 111 | 插件的编写将会使用到以下的类 112 | ```js 113 | //书籍源的基类 114 | class ProtocolBase extends PluginBase { 115 | name = ""; 116 | description = ""; 117 | protocols = "protocols://;"; 118 | isSource = false; 119 | icon = ""; 120 | reset() {} 121 | get(filter, isContinue) {} 122 | open(uri) {} 123 | close(book) {} 124 | } 125 | ``` 126 | 127 | #### ProtocolBase.name : string 128 | 插件的名称,用于显示在选项页中 129 | #### ProtocolBase.description : string 130 | 插件的描述,当插件没有提供readme.md时,将这个字段显示为插件的描述 131 | #### ProtocolBase.isSource : boolean 132 | 是否为一个书籍源,当只有isSource为true时,才会被当做一个源来注册入系统 133 | #### ProtocolBase.icon : string 134 | 源的图标的url。用于显示在书库界面中 135 | #### ProtocolBase.reset() 136 | 重置状态(目前没有使用) 137 | #### ProtocolBase.get( filter : string, isContinue : boolean ) : Promise({}) 138 | 从远程服务器获得书籍列表 139 | 此方法应该返回一个Promise,成功时返回一个如下结构的dict,失败时可以简单的返回错误信息字符串或是异常对象 140 | ```js 141 | { 142 | filter: filter, //当前请求的filter参数 143 | pageNo: pageNo, //当前请求的页号 144 | books : [], //当前请求得到的书籍信息的array 145 | hasMore : false, //当前请求之后,是否还有更多的结果可以请求 146 | } 147 | ``` 148 | books中的数据结构 149 | ```js 150 | { 151 | name: "", //书籍的显示用的名字 152 | path: "", //书籍的path,为protocols中的某一个值+书籍的标识符组成的一串唯一的路径,由插件产生并解析 153 | thumbnail: "" //书籍的封面缩略图的url 154 | } 155 | ``` 156 | 157 | filter参数是由空格分隔开的关键词,如果关键词以$开始,则是一个tag,远程源可以简单的将$移除,或是将其接入对应的远端的搜索系统中 158 | isCountinue参数表示是否继续上一次的搜索。即如果上一次的搜索返回了pageNo为0并且hasMore为true的结果,则这次应该开始pageNo为1的结果集的获取 159 | 160 | #### ProtocolBase.open( uri : string) : Promise(BookDetail) 161 | 从远程服务器获得uri对应的书籍的详细信息并组织成一个BookDetail返回。 162 | 此方法应该返回一个Promise,成功时返回一个BookDetail,失败时可以简单的返回错误信息字符串或是异常对象 163 | uri即get方法中返回的book的path 164 | 165 | ```js 166 | //书籍详细信息的基类 167 | //仅展示需要由插件填入的成员及需要重载的方法 168 | class BookDetail { 169 | id = 0; //书籍的id 170 | name = ""; //书籍的名称 171 | path = ""; //书籍的path,应该与get方法中返回的一致 172 | thumbnail = "name"; //书籍的封面url 173 | category = [BookChapter] ; //书籍的分类 174 | tags = []; //标签列表 175 | vertical = false; //是否是纵向阅读 176 | LoadChapter( categoryIdx : int, chapterIdx : int ) : Promise(BookChapter) //读取一个指定的category中的chapter 177 | LoadImage( page : BookPageInfo ) : Promise(Blob) //读取并返回page指定的页的图像blob对象 178 | } 179 | 180 | //表示一个具体的章节 181 | class BookChapter 182 | { 183 | name = ""; //章节名字,显示用 184 | id = 0; //保留,未使用 185 | order = 0; //章节顺序,对章节排序用 186 | pages = []; //章节之下所有的页的array,具体定义由插件自行解释 187 | constructor( id, name, order ) 188 | } 189 | 190 | //表示一个书籍中的一个具体的分类 191 | //用在动漫之家这类会在一个书籍下有多个版本的情况,比如不同汉化组或是连载与单行本的分别 192 | class BookCategory 193 | { 194 | id = 0; //保留,未使用 195 | name = "name"; //分类名字 196 | chapters = []; //分类下的章节 197 | constructor(name, id); 198 | } 199 | 200 | //表示一个书籍中的一个具体的页的位置 201 | class BookPageInfo { 202 | category = 0; //分类索引 203 | chapter = 0; //章节索引 204 | page = 0; //页面索引 205 | constructor( category, chapter, page); 206 | } 207 | ``` 208 | 209 | ###### BookDetail.LoadChapter( categoryIdx : int, chapterIdx : int ) : Promise(BookChapter) 210 | 读取一个指定的category中的chapter。用于无法在ProtocolBase.open方法中得到具体的章节细节数据时使用。 211 | 有些情况下,你可能只能在open的请求中得到章节的数目,而不能得到其他的详细信息。当开始阅读时,会先调用LoadChapter来准备好chapter中的数据,然后返回给系统。 212 | 213 | ###### BookDetail.LoadImage( page : BookPageInfo ) : Promise(Blob) 214 | 读取并返回page指定的页的图的Blob对象。 215 | 216 | 217 | 218 | #### ProtocolBase.close( book : BookDetail) : Promise 219 | 关闭打开的书籍源,以释放相应的资源。 220 | 对于基于http的源而言,基本不需要关注此方法。 221 | 主要用于支持本地文件做为源时,关闭打开的文件之类的资源。 222 | 223 | 224 | 225 | [参考插件例子](https://github.com/typehm/KolaMangaReader-plugins/tree/main/examples) 226 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /examples/zh_TW/index.js: -------------------------------------------------------------------------------- 1 | const {LanguagePack} = __common__("language"); 2 | 3 | //declare a protocol 4 | class zh_TW_Protocol extends LanguagePack { 5 | name = "繁體中文語言包"; 6 | description = "繁體中文的語言界面支援包"; 7 | languageCodeName = "zh-TW"; 8 | languageName = "繁體中文"; 9 | languageJsonPath = __dirname + "/language.json"; 10 | } 11 | 12 | //export the protocol 13 | module.exports = { 14 | zh_TW_Protocol, 15 | default : zh_TW_Protocol, 16 | } 17 | -------------------------------------------------------------------------------- /examples/zh_TW/language.json: -------------------------------------------------------------------------------- 1 | { 2 | "zh-TW": [ 3 | 4 | {"key": "common.name", "value": "通用"}, 5 | {"key": "common.language", "value": "當前語言"}, 6 | {"key": "common.devMode", "value": "開發者模式"}, 7 | {"key": "common.devmode.nodrag", "value": "打開開發者工具時視窗將無法被拖移"}, 8 | {"key": "common.devmode.openDevTool", "value": "打開開發者視窗"}, 9 | {"key": "common.showHelpOnStart", "value": "程式啓動時顯示幫助"}, 10 | {"key": "common.checkUpdateOnStart", "value": "啓動時檢查更新"}, 11 | {"key": "common.autoCheckUpdate", "value": "自動檢查更新"}, 12 | {"key": "common.updateInterval", "value": "檢查更新的間隔時間 (分鐘)"}, 13 | {"key": "common.useCache", "value": "對網絡請求啓用本地緩存"}, 14 | 15 | 16 | 17 | {"key": "library.local", "value": "書庫"}, 18 | {"key": "library.allbooks", "value": "全部書籍"}, 19 | {"key": "library.online", "value": "源"}, 20 | {"key": "libaray.newlirary", "value": "新建書架"}, 21 | {"key": "libaray.newliraryplaceholder", "value": "請輸入書架的名字"}, 22 | {"key": "libaray.new", "value": "新建"}, 23 | {"key": "library.menu.view", "value": "查看"}, 24 | {"key": "library.menu.sort", "value": "排序"}, 25 | {"key": "library.menu.sort.name", "value": "名稱"}, 26 | {"key": "library.menu.sort.rate", "value": "評分"}, 27 | {"key": "library.menu.sort.lastview", "value": "最後閱讀"}, 28 | {"key": "library.menu.tag", "value": "標籤..."}, 29 | {"key": "library.menu.rate", "value": "評分"}, 30 | {"key": "library.menu.import", "value": "添加到本地書庫"}, 31 | {"key": "library.menu.download", "value": "下載到本地"}, 32 | {"key": "library.menu.delete", "value": "從書庫中移除"}, 33 | {"key": "library.name", "value": "書庫"}, 34 | {"key": "library.cellWidth.title", "value": "縮略圖寬度"}, 35 | {"key": "library.downloadPath.title", "value": "檔案下載路徑"}, 36 | {"key": "library.minBookPerFetch.title", "value": "最小書籍顯示數量"}, 37 | {"key": "library.minBookPerFetch.description", "value": "會不斷從源請求書籍數據直至大於此值或是無法獲取到更多的數據"}, 38 | {"key": "library.autoImportTagsWhenImportBook.title", "value": "自動導入標籤"}, 39 | {"key": "library.autoImportTagsWhenImportBook.inlinetitle", "value": "在導入書籍時一並導入從源得到的標籤信息"}, 40 | 41 | {"key": "library.key.tag", "value": "管理書籍標籤"}, 42 | 43 | {"key": "library.tooltip.config", "value": "選項"}, 44 | {"key": "library.tooltip.menu", "value": "主菜單"}, 45 | {"key": "library.tooltip.addcategory", "value": "添加書籍分類"}, 46 | {"key": "library.tooltip.addsource", "value": "添加遠程書籍源"}, 47 | 48 | 49 | {"key": "viewer.name", "value": "閱讀"}, 50 | {"key": "viewer.zoomMode.title", "value": "默認圖像綻放模式"}, 51 | {"key": "viewer.maxPreload.title", "value": "預載圖片數目"}, 52 | {"key": "viewer.backgroundColor.title", "value": "背景顏色"}, 53 | {"key": "viewer.autoFullScreen.title", "value": "自動切換爲全屏幕"}, 54 | {"key": "viewer.autoFullScreen.inlinetitle", "value": "進入閱讀視圖時自動切換爲全屏幕"}, 55 | {"key": "viewer.key.prev", "value": "向上翻頁"}, 56 | {"key": "viewer.key.next", "value": "向下翻頁"}, 57 | {"key": "viewer.key.close", "value": "退出閱讀"}, 58 | {"key": "viewer.key.detail", "value": "詳細信息"}, 59 | {"key": "viewer.key.fullscreen", "value": "切換全屏"}, 60 | {"key": "viewer.key.fitOrigin", "value": "原始圖像尺寸"}, 61 | {"key": "viewer.key.fitWidth", "value": "匹配窗口寬度"}, 62 | {"key": "viewer.key.fitHeight", "value": "匹配窗口高度"}, 63 | 64 | {"key": "detail.tag", "value": "標籤"}, 65 | 66 | 67 | {"key": "tag.selected", "value": "選擇的標籤"}, 68 | {"key": "tag.all", "value": "全部標籤"}, 69 | {"key": "tag.close", "value": "關閉"}, 70 | {"key": "tag.ok", "value": "確定"}, 71 | {"key": "tag.newtag", "value": "新建標籤"}, 72 | {"key": "tag.newtagplaceholder", "value": "請輸入標籤的名字"}, 73 | {"key": "tag.new", "value": "新建"}, 74 | 75 | 76 | {"key": "config.option", "value": "選項"}, 77 | {"key": "config.cancel", "value": "取消"}, 78 | {"key": "config.ok", "value": "確定"}, 79 | {"key": "config.apply", "value": "應用"}, 80 | {"key": "config.enable", "value": "已啓用"}, 81 | {"key": "config.disable", "value": "已禁用"}, 82 | {"key": "config.uninstall", "value": "卸載"}, 83 | {"key": "config.filterplaceholder", "value": "查找插件"}, 84 | 85 | {"key": "form.path.select", "value": "選擇路徑"}, 86 | {"key": "form.path.open", "value": "打開路徑"}, 87 | 88 | {"key": "plugins.name", "value": "插件"} 89 | ] 90 | } 91 | --------------------------------------------------------------------------------