├── .gitignore ├── README.md ├── bm_viewer ├── bm_viewer.py └── bookmark.pyui ├── conf ├── bookmarks.toml ├── defaul_parser.toml └── parser.toml ├── config_loader.py ├── demo.MP4 ├── e_loader ├── e_loader.py ├── ebase_loader.py ├── ebody_loader.py └── eindex_loader.py ├── ebody_viewer ├── ebook_body.pyui ├── ebook_body_viewer.py ├── eimg_body.pyui └── eimg_body_viewer.py ├── ereader.py ├── home_viewer ├── home.pyui └── viewer.py ├── index_viewer ├── index.pyui └── index_viewer.py ├── main.py ├── menu_viewer ├── menu.pyui └── viewer.py ├── requirements.txt ├── rule ├── base_rule.py └── rule.py ├── web.py ├── zh_st.py ├── zsbook_loader ├── zsbody_loader.py ├── zsbook_loader.py └── zsindex_loader.py └── zsbook_search_viewer ├── src_index.pyui └── viewer.py /.gitignore: -------------------------------------------------------------------------------- 1 | /cache_rsp/* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 阅读一些在线小说或者漫画 2 | 3 | 1. 运行main.py 4 | 1. 配置文件和书签在conf文件夹里面 5 | 1. 解析不可能做到覆盖所有网站,但是除了有特别复杂的js的网站外,理论上都可以调整之后正常支持 6 | 1. 本项目的依赖包都写在requirements.txt里面。均可[stash](https://github.com/ywangd/stash)里面用pip安装 7 | - 此项目使用了beautifulsoup4,但是pythonista自带库比老旧。在安装用`pip install module_name`(stash命令, eg:`pip install beautifulsoup4`)安装之后,用`del sys.modules['module_name']`(py脚本, eg:`del sys.modules['bs4']` )来移除原有依赖(显示`KeyError: bs4`说明已经删除了预装的依赖),之后重启pythonista会自动切换到新安装的包上,可以用`print(module_name.__file__)`(py脚本, eg: `print(bs4.__file__)`)测试是否正常移除了依赖。(beautifulsoup4缩写bs4,两者是同一个模块,但是上面不能通用) 8 | - 本项目使用的requests模块同样已经预装在pythonista中,但是也是过于老旧。更新方法是一口气安装idna、chardet、urllib3、requests(使用pip),中间不得退出app -------------------------------------------------------------------------------- /bm_viewer/bm_viewer.py: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | 4 | class BmViewer: 5 | def __init__(self, controller, bms): 6 | view = ui.load_view('bm_viewer/bookmark') 7 | tb = view['tableview'] 8 | 9 | tb.data_source.items = bms 10 | tb.data_source.edit_action = controller.del_bm 11 | self.bm_view = tb 12 | view.right_btns_desc = 'return' 13 | self.view = view 14 | tb.delegate = self 15 | self.controller = controller 16 | 17 | def tableview_did_select(self, bm_view, section, row): 18 | bm = bm_view.data_source.items[row] 19 | bm_view.reload() 20 | if bm['type'] == 'zsbook': 21 | self.controller.load_zsbook(bm['url'], bm['i'], bm['j']) 22 | elif bm['type'] == 'ebook': 23 | self.controller.load_ebook(bm['url'], bm['i'], bm['j']) 24 | elif bm['type'] == 'eimg': 25 | self.controller.load_eimg(bm['url'], bm['i'], bm['j']) 26 | 27 | def add_new_bm(self, new_bm): 28 | data_source = self.bm_view.data_source 29 | data_source.items.append(new_bm) 30 | return data_source 31 | -------------------------------------------------------------------------------- /bm_viewer/bookmark.pyui: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nodes" : [ 4 | { 5 | "nodes" : [ 6 | 7 | ], 8 | "frame" : "{{0, 0}, {363, 536}}", 9 | "class" : "TableView", 10 | "attributes" : { 11 | "uuid" : "341F4B0E-E77F-4DEF-83B1-74DC9E5ADED6", 12 | "background_color" : "RGBA(1.0, 1.0, 1.0, 1.0)", 13 | "frame" : "{{83, 188}, {200, 200}}", 14 | "data_source_items" : "Row 1\nRow 2\nRow 3", 15 | "data_source_number_of_lines" : 1, 16 | "data_source_edit_action" : "", 17 | "data_source_move_enabled" : false, 18 | "data_source_delete_enabled" : true, 19 | "data_source_font_size" : 18, 20 | "row_height" : 44, 21 | "class" : "TableView", 22 | "name" : "tableview", 23 | "flex" : "" 24 | }, 25 | "selected" : true 26 | } 27 | ], 28 | "frame" : "{{0, 0}, {380, 573}}", 29 | "class" : "View", 30 | "attributes" : { 31 | "enabled" : true, 32 | "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)", 33 | "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)", 34 | "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)", 35 | "flex" : "" 36 | }, 37 | "selected" : false 38 | } 39 | ] -------------------------------------------------------------------------------- /conf/bookmarks.toml: -------------------------------------------------------------------------------- 1 | [[bookmarks]] 2 | i = 0 3 | j = 0 4 | url = "http://m.176xsw.com/7/111/read/99575.html" 5 | title = "放开那个女巫- 第一章 从今天开始做王子" 6 | type = "ebook" 7 | 8 | [[bookmarks]] 9 | i = 12 10 | j = 0 11 | url = "http://m.176xsw.com/7/111/read/99575.html" 12 | title = "放开那个女巫- 第一章 从今天开始做王子" 13 | type = "ebook" 14 | 15 | [[bookmarks]] 16 | i = 15 17 | j = 0 18 | url = "https://www.xbookcn.com/book/chest/14.htm" 19 | title = "第十三章 色魔正面挑战" 20 | type = "ebook" 21 | 22 | [[bookmarks]] 23 | i = 5 24 | j = 0 25 | url = "https://m.banzhuer.com/1_1251/352271_8.html" 26 | title = "大学刑法课(七)_大学刑法课_rescueme_第二版主网" 27 | type = "ebook" 28 | 29 | [[bookmarks]] 30 | i = 13 31 | j = 19 32 | url = "https://m.banzhuer.com/1_1251/352289_3.html" 33 | title = "大学刑法课(十九)_大学刑法课_rescueme_第二版主网" 34 | type = "ebook" 35 | 36 | [[bookmarks]] 37 | i = 2 38 | j = 0 39 | url = "https://m.banzhuer.com/1_1251/352320_3.html" 40 | title = "大学刑法课46_大学刑法课_rescueme_第二版主网" 41 | type = "ebook" 42 | 43 | [[bookmarks]] 44 | i = 0 45 | j = 127 46 | url = "https://exhentai.org/s/8154e762d7/1366417-13" 47 | title = "(C93) [Hitori de dekirumon. (Kain)] chou kyokudai kain mura" 48 | type = "eimg" 49 | 50 | [[bookmarks]] 51 | i = 1 52 | j = 290 53 | url = "https://manhwahentai.com/manhwa/the-sharehouse/chapter-36" 54 | title = "The Sharehouse - Chapter 36 - Manhwa Hentai" 55 | type = "eimg" 56 | 57 | [[bookmarks]] 58 | i = 1 59 | j = 1084 60 | url = "https://manhwahentai.com/manhwa/the-sharehouse/chapter-36" 61 | title = "The Sharehouse - Chapter 36 - Manhwa Hentai" 62 | type = "eimg" 63 | 64 | [[bookmarks]] 65 | i = 5 66 | j = 1434 67 | url = "https://manhwahentai.com/manhwa/the-sharehouse/chapter-37" 68 | title = "The Sharehouse - Chapter 37 - Manhwa Hentai" 69 | type = "eimg" 70 | 71 | [[bookmarks]] 72 | i = 0 73 | j = 0 74 | url = "58a75d70b610c9fc54fe5f4c#0" 75 | title = "第一章 陨落的天才" 76 | type = "zsbook" 77 | 78 | [[bookmarks]] 79 | i = 3 80 | j = 19 81 | url = "579b93ae561763dd2d2382e6#98" 82 | title = "第一百零章 威胁" 83 | type = "zsbook" 84 | 85 | [[bookmarks]] 86 | i = 0 87 | j = 19 88 | url = "https://m.banzhuer.com/1_1251/352282_3.html" 89 | title = "大学刑法课(十四)_大学刑法课_rescueme_第二版主网" 90 | type = "ebook" 91 | 92 | [[bookmarks]] 93 | i = 2 94 | j = 0 95 | url = "https://m.banzhuer.com/1_1251/352282_3.html" 96 | title = "大学刑法课(十四)_大学刑法课_rescueme_第二版主网" 97 | type = "ebook" 98 | 99 | [[bookmarks]] 100 | i = 6 101 | j = 0 102 | url = "5b2a2ac9c4d0bb0100a067b2#2" 103 | title = "正文卷 第3章 噩梦级任务" 104 | type = "zsbook" 105 | 106 | -------------------------------------------------------------------------------- /conf/defaul_parser.toml: -------------------------------------------------------------------------------- 1 | encoding_func = "" 2 | encoding = "utf-8" 3 | title = "title" 4 | words_tag = "p" 5 | next_tag = "a[rel='next']" 6 | -------------------------------------------------------------------------------- /conf/parser.toml: -------------------------------------------------------------------------------- 1 | [[websites]] 2 | url = "www.luoxia.com" 3 | encoding = "utf-8" 4 | # 判定标题的 5 | # 判定文章的(例如:br / b ....) 6 | [[websites.body.content]] 7 | css_selector = "div[id=\"nr1\"] > p:not([class])" 8 | # 防止有多级的url 下一页下一章混着 9 | [[websites.body.next]] 10 | name = 'a' 11 | attrs = {rel = "next"} 12 | 13 | [[websites]] 14 | url = "www.ptwxz.com" 15 | encoding = "gbk" 16 | [[websites.body.content]] 17 | css_selector = "head" 18 | [[websites.body.next]] 19 | css_selector = 'a:contains(下一)' 20 | 21 | [[websites]] 22 | url = "www.xbookcn.com" 23 | # 空 就是不管了 24 | # 其他就是直接指定 25 | encoding = "big5" 26 | [[websites.body.content]] 27 | name = "p" 28 | attrs = {class = false} 29 | [[websites.body.next]] 30 | name = 'a' 31 | string = "下一" 32 | [websites.body.index] 33 | name = 'a' 34 | string = "目錄頁" 35 | [[websites.index.content]] 36 | name = 'a' 37 | key = "href" 38 | 39 | [[websites]] 40 | url = "www.mfxsydw.com" 41 | [[websites.body.content]] 42 | name = "p" 43 | attrs = {class = false} 44 | [[websites.body.next]] 45 | name = 'a' 46 | attrs = {rel = "next"} 47 | [websites.body.index] 48 | name = "a" 49 | attrs = {rel = "contents"} 50 | [[websites.index.content]] 51 | name = "a" 52 | attrs={itemprop = "url", class = false} 53 | key = "href" 54 | 55 | [[websites]] 56 | url = "www.piaotian.com" 57 | encoding = "gbk" 58 | [[websites.body.content]] 59 | name = "head" 60 | attrs = {} 61 | [[websites.body.next]] 62 | name = 'a' 63 | string = "下一" 64 | [websites.body.index] 65 | name = "a" 66 | string = "返回目录" 67 | [[websites.index.content]] 68 | css_selector = "li > a[href$=\"html\"]" 69 | key = "href" 70 | 71 | [[websites]] 72 | url = "m.qu.la" 73 | [[websites.body.content]] 74 | name = "div" 75 | attrs = {} 76 | [[websites.body.next]] 77 | name = 'a' 78 | string = "下一" 79 | 80 | [[websites]] 81 | url = "m.hunhun520.com" 82 | [[websites.body.content]] 83 | name = "div" 84 | attrs = {id = "content"} 85 | [[websites.body.next]] 86 | name = 'a' 87 | string = "下一" 88 | 89 | [[websites]] 90 | url = "www.luoqiu.com" 91 | encoding = "gbk" 92 | [[websites.body.content]] 93 | name = "div" 94 | attrs = {id = "content"} 95 | [[websites.body.content]] 96 | name = "br" 97 | [[websites.body.next]] 98 | name = 'a' 99 | string = "下一" 100 | 101 | [[websites]] 102 | url = "www.69shu.com" 103 | encoding = "gbk" 104 | [[websites.body.content]] 105 | name = "div" 106 | attrs = {class = "yd_text2"} 107 | [[websites.body.next]] 108 | name = 'a' 109 | string = "下一" 110 | 111 | [[websites]] 112 | url = "m.00xs.cc" 113 | encoding = "gbk" 114 | [[websites.body.content]] 115 | name = "div" 116 | attrs = {id = "nr1"} 117 | [[websites.body.next]] 118 | name = 'a' 119 | string = "继续看" 120 | 121 | [[websites]] 122 | url = "www.xitxt.net" 123 | encoding = 'gbk' 124 | [[websites.body.content]] 125 | name = "article" 126 | [[websites.body.next]] 127 | name = 'a' 128 | string = "下一" 129 | 130 | # 作废,网站搞得麻烦,时效+固定书目,除非js了 131 | [[websites]] 132 | url = "m.123du.cc" 133 | # 自己抓包刷新 134 | headers = {cookie = "nxgmnmry=22b1144258bb2b3c"} 135 | [[websites.body.content]] 136 | name = "div" 137 | attrs = {class = "TxtContent"} 138 | [[websites.body.content]] 139 | name = "br" 140 | [[websites.body.next]] 141 | name = 'a' 142 | string = "下一[页頁]" 143 | [[websites.body.next]] 144 | name = 'a' 145 | string = "下一章" 146 | 147 | [[websites]] 148 | url = "novel.zhwenpg.com" 149 | [[websites.body.content]] 150 | name = "span" 151 | attrs = {class = "content"} 152 | css_selector = "span[class=\"content\"] > p" 153 | [[websites.body.next]] 154 | name = 'a' 155 | string = "下一" 156 | 157 | [[websites]] 158 | url = "www.quanben.io" 159 | [[websites.body.content]] 160 | css_selector = "div[class=\"articlebody\"] > p" 161 | [[websites.body.next]] 162 | css_selector = "a[rel=\"next\"]" 163 | 164 | [[websites]] 165 | url = "m.banzhuer.com" 166 | encoding = "gbk" 167 | [[websites.body.content]] 168 | css_selector = "article" 169 | [[websites.body.next]] 170 | css_selector = "a[class=\"dise rt\"]" 171 | [websites.body.index] 172 | name = "a" 173 | string = "返回目录" 174 | key="href" 175 | [[websites.index.content]] 176 | css_selector = "ul[class=\"lb fk\"] a[href]" 177 | key = "href" 178 | [[websites.index.next]] 179 | css_selector = "div[class=\"fenye\"]:first-child div[class=\"showpage r3\"]:first-child > ul a[class=\"xbk this tb\"] ~ li > a[class=\"xbk\"]" 180 | key = "href" 181 | 182 | [[websites]] 183 | url = "e-hentai.org" 184 | [[websites.body.content]] 185 | name = "img" 186 | attrs = {id = "img"} 187 | key = "src" 188 | [[websites.body.next]] 189 | name = 'a' 190 | attrs = {id = "next"} 191 | 192 | [[websites]] 193 | url = "exhentai.org" 194 | headers = {cookie = ""} 195 | [[websites.body.content]] 196 | css_selector = "img[id=\"img\"]" 197 | key = "src" 198 | [[websites.body.next]] 199 | css_selector = "a[id=\"next\"]" 200 | key = "href" 201 | 202 | [[websites]] 203 | url = "kkpmh.com" 204 | headers = {cookie = ""} 205 | [[websites.body.content]] 206 | css_selector = "div[class=\"cartoon\"] > img" 207 | key = "data-lazy-src" 208 | [[websites.body.next]] 209 | css_selector = "a[rel=\"next\"][href!=\"\"]" 210 | key = "href" 211 | 212 | [[websites]] 213 | url = "m.k886.net" 214 | [[websites.body.content]] 215 | name = "img" 216 | attrs = {alt = true, height = false} 217 | key = "src" 218 | [[websites.body.next]] 219 | name = 'a' 220 | string = "下一[页頁]" 221 | [[websites.body.next]] 222 | name = 'a' 223 | string = "下一章" 224 | [websites.body.index] 225 | css_selector = "h1 > a[href]" 226 | [[websites.index.content]] 227 | css_selector = "div[class=\"chapter-list\"] a[href]" 228 | key = "href" 229 | string_pattern = "title" 230 | 231 | [[websites]] 232 | url = "raws.mangazuki.co" 233 | [[websites.body.content]] 234 | name = "img" 235 | attrs = {class = "img-responsive", data-src = true} 236 | key = "data-src" 237 | [[websites.body.next]] 238 | re = "var[ ]*next_chapter[^;]+\"(.+)\"" 239 | 240 | [[websites]] 241 | url = "mangazuki.me" 242 | [[websites.body.content]] 243 | css_selector = "div[class=\"page-break\"] > img" 244 | key = "src" 245 | [[websites.body.next]] 246 | css_selector = "a[class=\"btn next_page\"]" 247 | 248 | [[websites]] 249 | url = "www.177pic.info" 250 | [[websites.body.content]] 251 | name = "img" 252 | attrs = {class = true} 253 | key = "src" 254 | [[websites.body.next]] 255 | name = 'a' 256 | string = "下一" 257 | 258 | [[websites]] 259 | url = "manhwahand.com" 260 | [[websites.body.content]] 261 | name = "img" 262 | attrs = {class = true, id = true} 263 | key = "src" 264 | [[websites.body.next]] 265 | name = 'a' 266 | attrs = {class = "btn next_page"} 267 | 268 | [[websites]] 269 | url = "nhentai.net" 270 | [[websites.body.content]] 271 | name = "img" 272 | attrs = {class = true} 273 | key = "src" 274 | [[websites.body.next]] 275 | name = 'a' 276 | attrs = {class = "next"} 277 | 278 | [[websites]] 279 | url = "mangapark.me" 280 | [[websites.body.content]] 281 | name = "img" 282 | attrs = {class = "img"} 283 | [[websites.body.next]] 284 | name = 'a' 285 | attrs = {class = "next"} 286 | 287 | [[websites]] 288 | url = "taadd.com" 289 | [[websites.body.content]] 290 | name = "img" 291 | attrs = {id = "image"} 292 | [[websites.body.next]] 293 | re = "next_page = \"(.+)\"" 294 | 295 | [[websites]] 296 | url = "original-work.simply-hentai.com" 297 | [[websites.body.content]] 298 | name = "source" 299 | attrs = {media = "(min-width: 1050px)"} 300 | key = "srcset" 301 | [[websites.body.next]] 302 | name = 'a' 303 | string = "Next" 304 | 305 | [[websites]] 306 | url = "manganelo.com" 307 | [[websites.body.content]] 308 | css_selector = "img[src$=\"jpg\"]" 309 | [[websites.body.next]] 310 | name = 'a' 311 | attrs = {class = "back"} 312 | 313 | [[websites]] 314 | url = "www.wnacg.org" 315 | [[websites.body.content]] 316 | name = "img" 317 | attrs = {class = "photo"} 318 | [[websites.body.next]] 319 | name = 'a' 320 | attrs = {class = "btnnext"} 321 | 322 | [[websites]] 323 | url = "manhwahentai.com" 324 | [[websites.body.content]] 325 | css_selector = "img[class=\"wp-manga-chapter-img\"][src]" 326 | [[websites.body.next]] 327 | css_selector = "a[class=\"btn next_page\"]" 328 | 329 | [[websites]] 330 | url = "www.mangahome.com" 331 | [[websites.body.content]] 332 | name = "img" 333 | attrs = {id = "image"} 334 | [[websites.body.next]] 335 | name = 'a' 336 | string = "next" 337 | 338 | # 可以认为这是个default设置 339 | [[websites]] 340 | url = "." 341 | encoding = "utf-8" 342 | [websites.body.title] 343 | name = "title" 344 | [[websites.body.content]] 345 | name = "p" 346 | attrs = {class = false} 347 | [[websites.body.next]] 348 | name = 'a' 349 | attrs = {rel = "next"} 350 | -------------------------------------------------------------------------------- /config_loader.py: -------------------------------------------------------------------------------- 1 | import toml 2 | 3 | 4 | class ConfigLoader(): 5 | def __init__(self): 6 | self.file_conf = 'conf/parser.toml' 7 | self.dict_conf = self.read_conf() 8 | self.file_bookmark = 'conf/bookmarks.toml' 9 | self.dict_bookmark = self.read_bookmark() 10 | # print(self.dict_conf) 11 | 12 | def read_conf(self): 13 | with open(self.file_conf, encoding="utf-8") as f: 14 | dict_conf = toml.load(f) 15 | return dict_conf 16 | 17 | def read_bookmark(self): 18 | with open(self.file_bookmark, encoding="utf-8") as f: 19 | dict_bookmark = toml.load(f) 20 | if not dict_bookmark: 21 | dict_bookmark = {'bookmarks': []} 22 | return dict_bookmark 23 | 24 | def check_bookmark(self, new_bm): 25 | if new_bm is None: 26 | return True 27 | for bm in self.dict_bookmark['bookmarks']: 28 | if bm['i'] == new_bm['i'] and \ 29 | bm['j'] == new_bm['j'] and \ 30 | bm['url'] == new_bm['url']: 31 | return True 32 | return False 33 | 34 | def refresh_file(self, bmview): 35 | self.dict_bookmark['bookmarks'] = bmview.items 36 | with open(self.file_bookmark, 'w', encoding="utf-8") as f: 37 | toml.dump(self.dict_bookmark, f) 38 | 39 | -------------------------------------------------------------------------------- /demo.MP4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjqiang/yj_ebook_reader/89158f70032f741ee2e22e396fe8b262ac25aa9e/demo.MP4 -------------------------------------------------------------------------------- /e_loader/e_loader.py: -------------------------------------------------------------------------------- 1 | from .ebody_loader import EBookBodyLoader, EImgBodyLoader 2 | from .eindex_loader import EIndexLoader 3 | from rule.rule import WebsiteRule 4 | 5 | 6 | class EBookLoader: 7 | def __init__(self, list_confs): 8 | self.list_confs = list_confs 9 | rule = WebsiteRule() 10 | 11 | self.body_loader = EBookBodyLoader(rule.body_rule, rule) 12 | self.index_loader = EIndexLoader(rule.index_rule, rule) 13 | self.rule = rule 14 | 15 | def set_url(self, url): 16 | self.url = url 17 | for i in self.list_confs['websites']: 18 | if i['url'] in url: 19 | conf = i 20 | break 21 | else: 22 | conf = None 23 | 24 | self.rule.set_rule(conf) 25 | self.body_loader.set_url(url) 26 | index_url = self.body_loader.get_index_url() 27 | self.index_loader.set_url(index_url) 28 | 29 | def get_next_bodydata(self): 30 | return self.body_loader.get_next_data() 31 | 32 | def get_next_indexdata(self): 33 | return self.index_loader.get_next_data() 34 | 35 | 36 | class EImgLoader(EBookLoader): 37 | def __init__(self, list_confs): 38 | self.list_confs = list_confs 39 | rule = WebsiteRule() 40 | 41 | self.body_loader = EImgBodyLoader(rule.body_rule, rule) 42 | self.index_loader = EIndexLoader(rule.index_rule, rule) 43 | self.rule = rule 44 | 45 | -------------------------------------------------------------------------------- /e_loader/ebase_loader.py: -------------------------------------------------------------------------------- 1 | import re 2 | import console 3 | from urllib.parse import urljoin 4 | import web 5 | from bs4 import BeautifulSoup 6 | 7 | 8 | class EPageLoader: 9 | def __init__(self, page_rule, website_rule): 10 | self.rule = page_rule 11 | self.website_rule = website_rule 12 | 13 | def set_url(self, url): 14 | self.url = url 15 | self.cur_offset = None 16 | 17 | def get_title(self): 18 | rule = self.rule.title 19 | title = rule.find_raw(self.text, self.soups, with_string=True) 20 | return str(title).strip() 21 | 22 | def get_content(self): 23 | pass 24 | 25 | def get_next_url(self): 26 | rules = self.rule.next 27 | if rules is None: 28 | return False 29 | for rule in rules: 30 | link = rule.find_attr(self.text, self.soups) 31 | # 防止j s (此时一般就是没了) 32 | if link is not None and 'javascript' not in link and link != '#': 33 | self.url = urljoin(self.url, link) 34 | return True 35 | return False 36 | 37 | def fetch_page(self): 38 | # 验证页面的话,不要缓存 39 | rsp = web.get(self.url, headers=self.website_rule.headers, allow_cache=False) 40 | # encoding为None,requests模块会自己猜测 41 | 42 | rsp.encoding = self.website_rule.encoding 43 | # print(rsp.encoding) 44 | text = rsp.text 45 | soups = BeautifulSoup(text, 'html.parser') 46 | self.text = text 47 | self.soups = soups 48 | 49 | def fetch_page_with_captcha(self): 50 | re_safe_dog = re.compile('self.location="(.+)"') 51 | while True: 52 | self.fetch_page() 53 | title = self.get_title() 54 | if '服务器安全狗防护验证页面' in title: 55 | console.hud_alert('验证') 56 | js = self.soups.find('script').string 57 | link = re_safe_dog.search(js).group(1) 58 | self.url = urljoin(self.url, link) 59 | # print(link) 60 | else: 61 | break 62 | self.title = title 63 | return 64 | -------------------------------------------------------------------------------- /e_loader/ebody_loader.py: -------------------------------------------------------------------------------- 1 | import ui 2 | from urllib.parse import urljoin 3 | from bs4 import element 4 | import web 5 | import zh_st 6 | from .ebase_loader import EPageLoader 7 | 8 | 9 | class BodyLoader(EPageLoader): 10 | def get_index_url(self): 11 | rule = self.rule.index 12 | # print('测试index') 13 | if rule is None: 14 | return None 15 | else: 16 | self.fetch_page_with_captcha() 17 | result = rule.find_attr(self.text, self.soups) 18 | url = urljoin(self.url, result.strip()) 19 | return url 20 | 21 | 22 | class EBookBodyLoader(BodyLoader): 23 | def get_next_data(self): 24 | if self.cur_offset is None: 25 | self.fetch_page_with_captcha() 26 | self.contents = self.get_content() 27 | self.cur_offset = 0 28 | if self.cur_offset > 0: 29 | if not self.get_next_url(): 30 | return None, None, None 31 | self.fetch_page_with_captcha() 32 | self.contents = self.get_content() 33 | self.cur_offset = 0 34 | 35 | if not self.contents: 36 | return None, None, None 37 | words = self.contents 38 | title = self.title 39 | words = [zh_st.t2s(line) for line in words] 40 | title = zh_st.t2s(self.title) 41 | self.cur_offset += 1 42 | return words, title, self.url 43 | 44 | def get_all_content(self, tag, start=False): 45 | # start 是为了兼容,有网站数据正文分开两部分的……所以规则解析是到两部分的共同父节点(css selector只返回element) 46 | if not start and isinstance(tag, element.Tag): 47 | if tag.name != 'br': # 目前只忽略br,虽然p也有,但是似乎会复杂化 48 | return None 49 | if isinstance(tag, element.Comment): 50 | return None 51 | if isinstance(tag, element.NavigableString): 52 | text = tag.string.strip() 53 | return [text] if text else None 54 | results = [] 55 | for i in tag.children: 56 | result = self.get_all_content(i) 57 | if result: 58 | results += result 59 | return results 60 | 61 | def get_content(self): 62 | rules = self.rule.content 63 | labels = [] 64 | for rule in rules: 65 | # 其实这里默认了不能re 66 | labels += rule.findall_raw(self.text, self.soups) 67 | words = [] 68 | for i in labels: 69 | words += self.get_all_content(i, start=True) 70 | words = ['  ' + i for i in words] 71 | return words 72 | 73 | 74 | class EImgBodyLoader(BodyLoader): 75 | def get_next_data(self): 76 | if self.cur_offset is None: 77 | self.fetch_page_with_captcha() 78 | self.contents = self.get_content() 79 | self.title = self.get_title() 80 | self.cur_offset = 0 81 | if self.cur_offset >= len(self.contents): 82 | while True: 83 | if not self.get_next_url(): 84 | return None, None, None 85 | self.fetch_page_with_captcha() 86 | self.contents = self.get_content() 87 | self.title = self.get_title() 88 | self.cur_offset = 0 89 | if self.contents: 90 | break 91 | if not self.contents: 92 | return None, None, None 93 | 94 | img_url = self.contents[self.cur_offset] 95 | img = ui.Image.from_data(web.get(img_url).content) 96 | self.cur_offset += 1 97 | return [img], self.title, self.url 98 | 99 | def get_content(self): 100 | rules = self.rule.content 101 | urls = [] 102 | for rule in rules: 103 | results = rule.findall_attr(self.text, self.soups) 104 | urls += [urljoin(self.url, result.strip()) for result in results] 105 | 106 | return urls 107 | 108 | -------------------------------------------------------------------------------- /e_loader/eindex_loader.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | from .ebody_loader import EPageLoader 3 | import zh_st 4 | 5 | 6 | class EIndexLoader(EPageLoader): 7 | def get_next_data(self): 8 | if self.url is None: 9 | return None, None 10 | if self.cur_offset is None: 11 | self.fetch_page_with_captcha() 12 | self.contents = self.get_content() 13 | self.cur_offset = 0 14 | if self.cur_offset > 0: 15 | if not self.get_next_url(): 16 | return None, None 17 | self.fetch_page_with_captcha() 18 | self.contents = self.get_content() 19 | self.cur_offset = 0 20 | # index [(index_url, index_name)] 21 | indexes = self.contents 22 | title = self.title 23 | indexes = [(urljoin(self.url, url), zh_st.t2s(name)) for url, name in indexes] 24 | title = zh_st.t2s(title) 25 | # print(indexes, len(indexes)) 26 | self.cur_offset += 1 27 | return indexes, title 28 | 29 | def get_content(self): 30 | if self.url is None: 31 | return None 32 | rules = self.rule.content 33 | contents = [] 34 | for rule in rules: 35 | results = rule.findall_attr(self.text, self.soups, with_string=True) 36 | contents += results 37 | return contents 38 | -------------------------------------------------------------------------------- /ebody_viewer/ebook_body.pyui: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nodes" : [ 4 | { 5 | "nodes" : [ 6 | 7 | ], 8 | "frame" : "{{6, 6}, {366, 537}}", 9 | "class" : "ScrollView", 10 | "attributes" : { 11 | "frame" : "{{0, 80}, {320, 320}}", 12 | "uuid" : "F0410398-BBC2-4979-9425-4E0D8CD71948", 13 | "custom_attributes" : "", 14 | "content_height" : 9999, 15 | "class" : "ScrollView", 16 | "content_width" : 281, 17 | "name" : "scrollview" 18 | }, 19 | "selected" : false 20 | }, 21 | { 22 | "nodes" : [ 23 | 24 | ], 25 | "frame" : "{{6, 6}, {360, 32}}", 26 | "class" : "Label", 27 | "attributes" : { 28 | "name" : "label0", 29 | "class" : "Label", 30 | "frame" : "{{85, 224}, {150, 32}}", 31 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 32 | "number_of_lines" : 1, 33 | "alignment" : "left", 34 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 35 | "text" : "Label0", 36 | "font_size" : 18, 37 | "font_name" : "" 38 | }, 39 | "selected" : false 40 | }, 41 | { 42 | "nodes" : [ 43 | 44 | ], 45 | "frame" : "{{6, 38}, {360, 32}}", 46 | "class" : "Label", 47 | "attributes" : { 48 | "font_name" : "", 49 | "frame" : "{{85, 224}, {150, 32}}", 50 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 51 | "class" : "Label", 52 | "alignment" : "left", 53 | "text" : "Label1", 54 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 55 | "font_size" : 18, 56 | "name" : "label1" 57 | }, 58 | "selected" : false 59 | }, 60 | { 61 | "nodes" : [ 62 | 63 | ], 64 | "frame" : "{{6, 70}, {360, 32}}", 65 | "class" : "Label", 66 | "attributes" : { 67 | "font_name" : "", 68 | "frame" : "{{85, 224}, {150, 32}}", 69 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 70 | "class" : "Label", 71 | "alignment" : "left", 72 | "text" : "Label2", 73 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 74 | "font_size" : 18, 75 | "name" : "label2" 76 | }, 77 | "selected" : false 78 | }, 79 | { 80 | "nodes" : [ 81 | 82 | ], 83 | "frame" : "{{6, 102}, {360, 32}}", 84 | "class" : "Label", 85 | "attributes" : { 86 | "font_name" : "", 87 | "frame" : "{{85, 224}, {150, 32}}", 88 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 89 | "class" : "Label", 90 | "alignment" : "left", 91 | "text" : "Label3", 92 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 93 | "font_size" : 18, 94 | "name" : "label3" 95 | }, 96 | "selected" : false 97 | }, 98 | { 99 | "nodes" : [ 100 | 101 | ], 102 | "frame" : "{{6, 134}, {360, 32}}", 103 | "class" : "Label", 104 | "attributes" : { 105 | "name" : "label4", 106 | "frame" : "{{85, 224}, {150, 32}}", 107 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 108 | "class" : "Label", 109 | "alignment" : "left", 110 | "text" : "Label4", 111 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 112 | "font_size" : 18, 113 | "font_name" : "" 114 | }, 115 | "selected" : false 116 | }, 117 | { 118 | "nodes" : [ 119 | 120 | ], 121 | "frame" : "{{6, 166}, {360, 32}}", 122 | "class" : "Label", 123 | "attributes" : { 124 | "name" : "label5", 125 | "frame" : "{{85, 224}, {150, 32}}", 126 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 127 | "class" : "Label", 128 | "alignment" : "left", 129 | "text" : "Label5", 130 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 131 | "font_size" : 18, 132 | "font_name" : "" 133 | }, 134 | "selected" : false 135 | }, 136 | { 137 | "nodes" : [ 138 | 139 | ], 140 | "frame" : "{{6, 230}, {360, 32}}", 141 | "class" : "Label", 142 | "attributes" : { 143 | "name" : "label7", 144 | "frame" : "{{85, 224}, {150, 32}}", 145 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 146 | "class" : "Label", 147 | "alignment" : "left", 148 | "text" : "Label7", 149 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 150 | "font_size" : 18, 151 | "font_name" : "" 152 | }, 153 | "selected" : false 154 | }, 155 | { 156 | "nodes" : [ 157 | 158 | ], 159 | "frame" : "{{6, 262}, {360, 32}}", 160 | "class" : "Label", 161 | "attributes" : { 162 | "name" : "label8", 163 | "frame" : "{{85, 224}, {150, 32}}", 164 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 165 | "class" : "Label", 166 | "alignment" : "left", 167 | "text" : "Label8", 168 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 169 | "font_size" : 18, 170 | "font_name" : "" 171 | }, 172 | "selected" : false 173 | }, 174 | { 175 | "nodes" : [ 176 | 177 | ], 178 | "frame" : "{{6, 294}, {360, 32}}", 179 | "class" : "Label", 180 | "attributes" : { 181 | "font_name" : "", 182 | "frame" : "{{85, 224}, {150, 32}}", 183 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 184 | "class" : "Label", 185 | "alignment" : "left", 186 | "text" : "Label9", 187 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 188 | "name" : "label9", 189 | "font_size" : 18 190 | }, 191 | "selected" : false 192 | }, 193 | { 194 | "nodes" : [ 195 | 196 | ], 197 | "frame" : "{{6, 326}, {360, 32}}", 198 | "class" : "Label", 199 | "attributes" : { 200 | "font_name" : "", 201 | "frame" : "{{85, 224}, {150, 32}}", 202 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 203 | "class" : "Label", 204 | "alignment" : "left", 205 | "text" : "Label10", 206 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 207 | "name" : "label10", 208 | "font_size" : 18 209 | }, 210 | "selected" : false 211 | }, 212 | { 213 | "nodes" : [ 214 | 215 | ], 216 | "frame" : "{{6, 358}, {360, 32}}", 217 | "class" : "Label", 218 | "attributes" : { 219 | "font_name" : "", 220 | "frame" : "{{85, 224}, {150, 32}}", 221 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 222 | "class" : "Label", 223 | "alignment" : "left", 224 | "text" : "Label11", 225 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 226 | "name" : "label11", 227 | "font_size" : 18 228 | }, 229 | "selected" : false 230 | }, 231 | { 232 | "nodes" : [ 233 | 234 | ], 235 | "frame" : "{{6, 390}, {360, 32}}", 236 | "class" : "Label", 237 | "attributes" : { 238 | "font_name" : "", 239 | "frame" : "{{85, 224}, {150, 32}}", 240 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 241 | "class" : "Label", 242 | "alignment" : "left", 243 | "text" : "Label12", 244 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 245 | "name" : "label12", 246 | "font_size" : 18 247 | }, 248 | "selected" : false 249 | }, 250 | { 251 | "nodes" : [ 252 | 253 | ], 254 | "frame" : "{{6, 422}, {360, 32}}", 255 | "class" : "Label", 256 | "attributes" : { 257 | "font_name" : "", 258 | "frame" : "{{85, 224}, {150, 32}}", 259 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 260 | "class" : "Label", 261 | "alignment" : "left", 262 | "text" : "Label13", 263 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 264 | "name" : "label13", 265 | "font_size" : 18 266 | }, 267 | "selected" : false 268 | }, 269 | { 270 | "nodes" : [ 271 | 272 | ], 273 | "frame" : "{{6, 454}, {360, 32}}", 274 | "class" : "Label", 275 | "attributes" : { 276 | "font_size" : 18, 277 | "frame" : "{{85, 224}, {150, 32}}", 278 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 279 | "class" : "Label", 280 | "alignment" : "left", 281 | "text" : "Label14", 282 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 283 | "name" : "label14", 284 | "font_name" : "" 285 | }, 286 | "selected" : false 287 | }, 288 | { 289 | "nodes" : [ 290 | 291 | ], 292 | "frame" : "{{6, 486}, {360, 32}}", 293 | "class" : "Label", 294 | "attributes" : { 295 | "font_size" : 18, 296 | "frame" : "{{85, 224}, {150, 32}}", 297 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 298 | "class" : "Label", 299 | "alignment" : "left", 300 | "text" : "Label15", 301 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 302 | "name" : "label15", 303 | "font_name" : "" 304 | }, 305 | "selected" : false 306 | }, 307 | { 308 | "nodes" : [ 309 | 310 | ], 311 | "frame" : "{{6, 550}, {360, 32}}", 312 | "class" : "Label", 313 | "attributes" : { 314 | "font_size" : 18, 315 | "frame" : "{{85, 224}, {150, 32}}", 316 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 317 | "class" : "Label", 318 | "alignment" : "left", 319 | "text" : "Label17", 320 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 321 | "name" : "label17", 322 | "font_name" : "" 323 | }, 324 | "selected" : false 325 | }, 326 | { 327 | "nodes" : [ 328 | 329 | ], 330 | "frame" : "{{6, 518}, {360, 32}}", 331 | "class" : "Label", 332 | "attributes" : { 333 | "font_size" : 18, 334 | "frame" : "{{85, 224}, {150, 32}}", 335 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 336 | "class" : "Label", 337 | "alignment" : "left", 338 | "text" : "Label16", 339 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 340 | "name" : "label16", 341 | "font_name" : "" 342 | }, 343 | "selected" : false 344 | }, 345 | { 346 | "nodes" : [ 347 | 348 | ], 349 | "frame" : "{{6, 198}, {360, 32}}", 350 | "class" : "Label", 351 | "attributes" : { 352 | "name" : "label6", 353 | "frame" : "{{85, 224}, {150, 32}}", 354 | "uuid" : "58547B3C-C156-4EC2-A02B-E1D547C8818A", 355 | "class" : "Label", 356 | "alignment" : "left", 357 | "text" : "Label6", 358 | "custom_attributes" : "{\"i\": 0, \"j\": 0}", 359 | "font_size" : 18, 360 | "font_name" : "" 361 | }, 362 | "selected" : false 363 | }, 364 | { 365 | "nodes" : [ 366 | 367 | ], 368 | "frame" : "{{6, 582}, {360, 32}}", 369 | "class" : "Label", 370 | "attributes" : { 371 | "name" : "label18", 372 | "frame" : "{{115, 271}, {150, 32}}", 373 | "uuid" : "09F9848D-73DB-4560-95DC-18204394E176", 374 | "class" : "Label", 375 | "alignment" : "left", 376 | "text" : "Label", 377 | "font_size" : 18, 378 | "font_name" : "" 379 | }, 380 | "selected" : false 381 | } 382 | ], 383 | "frame" : "{{0, 0}, {380, 573}}", 384 | "class" : "View", 385 | "attributes" : { 386 | "name" : "", 387 | "enabled" : true, 388 | "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)", 389 | "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)", 390 | "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)", 391 | "flex" : "" 392 | }, 393 | "selected" : false 394 | } 395 | ] -------------------------------------------------------------------------------- /ebody_viewer/ebook_body_viewer.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | import ui 3 | import console 4 | 5 | 6 | class EBookBodyViewer: 7 | ITEM_H = 32 8 | LEN_LINE = 19 9 | LOADING = 'LOADING...' 10 | 11 | def __init__(self, parent): 12 | view = ui.load_view('ebody_viewer/ebook_body') 13 | scrollview = view['scrollview'] 14 | self.scrollview = scrollview 15 | view.right_btns_desc = 'menu' 16 | self.view = view 17 | for i in range(19): 18 | scrollview.add_subview(view[f'label{i}']) 19 | scrollview.delegate = self 20 | self.items = deque(self.scrollview.subviews) 21 | assert (len(self.items) - 1) * self.ITEM_H > scrollview.height 22 | self.cur_offset = 0 23 | self.parent = parent 24 | 25 | def req_data_bg(self): 26 | self.parent.req_data_bg(self) 27 | 28 | def req_data(self, init=False): 29 | self.parent.req_data(self, init) 30 | 31 | def set_navi_view_name(self, name): 32 | self.parent.set_navi_view_name(name) 33 | 34 | def load_data(self): 35 | element = self.parent.load_data(self) 36 | if element is None: 37 | return None 38 | if element[0] is None: 39 | # 到底之后不再复位self.has_sent_req 40 | console.hud_alert('已经阅读完毕') 41 | return None 42 | chapter, title, url, init = element 43 | l = len(self.contents) 44 | sum_num_lines = 0 if init else 1 45 | 46 | for para in chapter: 47 | num_lines = int((len(para) - 1) / self.LEN_LINE) + 1 48 | sum_num_lines += num_lines 49 | self.contents.append((para, num_lines)) 50 | 51 | split_contents = '—' * self.LEN_LINE 52 | self.contents.append((split_contents, 1)) 53 | 54 | r = len(self.contents) 55 | self.titles.append((l, r, title, url)) 56 | self.scrollview.content_size += (0, sum_num_lines * self.ITEM_H) 57 | return sum_num_lines 58 | 59 | def refresh_title(self): 60 | off_set = self.cur_offset 61 | for item in self.items: 62 | if item.y + item.height >= off_set: 63 | i = item.i 64 | if i is None: 65 | continue 66 | 67 | for l, r, name, url in self.titles: 68 | if l <= i < r: 69 | self.set_navi_view_name(name) 70 | return 71 | 72 | def reset_view(self, i=0, j=0): 73 | scrollview = self.scrollview 74 | self.contents = [] 75 | # (l, r, name) 76 | self.titles = [] 77 | scrollview.content_size = (scrollview.width, 0) 78 | 79 | self.req_data(True) 80 | sum_num_lines = self.load_data() 81 | # 其实应该仿照img模块的,但是没必要,白白把逻辑弄麻烦了 82 | while sum_num_lines <= len(self.items): 83 | self.req_data() 84 | sum_num_lines += self.load_data() 85 | 86 | i_wanted = i 87 | j_wanted = j 88 | i, j = 0, 0 89 | y = 0 90 | for item in self.items: 91 | if len(self.contents[i][0]) <= j: 92 | i, j = i + 1, 0 93 | item.text = self.contents[i][0][j: j + self.LEN_LINE] 94 | # i代表段落index,j代表了段落里面具体的文字起始下标 95 | item.i = i 96 | item.j = j 97 | item.y = y 98 | y += self.ITEM_H 99 | j += self.LEN_LINE 100 | 101 | sum_num_lines = 0 102 | for para, num_lines in self.contents[:i_wanted]: 103 | sum_num_lines += num_lines 104 | sum_num_lines += int(j_wanted / self.LEN_LINE) 105 | # 这个破玩意儿改了之后会自动调用监听函数 106 | scrollview.content_offset = (0, sum_num_lines*self.ITEM_H) 107 | self.refresh_title() 108 | 109 | def get_offset(self): 110 | scrollview = self.scrollview 111 | off_set = scrollview.content_offset.y 112 | i = None 113 | for item in self.items: 114 | if item.y + item.height > off_set: 115 | i = item.i 116 | j = item.j 117 | break 118 | if i is None: 119 | return 120 | for l, r, name, url in self.titles: 121 | if l <= i < r: 122 | # 本页的i 相对段落数目 123 | new_bookmark = { 124 | 'i': i - l, 125 | 'j': j, 126 | 'url': url, 127 | 'title': name 128 | } 129 | return new_bookmark 130 | 131 | def reset_scrollbar(self): 132 | scrollview = self.scrollview 133 | offset_x, offset_y = scrollview.content_offset 134 | max_offset = scrollview.content_size.y - scrollview.height 135 | cur_offset = offset_y 136 | min_offset = min(cur_offset, max_offset) 137 | 138 | scrollview.content_offset = (offset_x, min_offset) 139 | 140 | def scrollview_did_scroll(self, scrollview): 141 | offset = scrollview.content_offset.y 142 | is_scroll_down = True if offset > self.cur_offset else False 143 | self.cur_offset = offset 144 | # print('t') 145 | 146 | reader_h = scrollview.height 147 | # print('t') 148 | 149 | content_size = scrollview.content_size[1] 150 | # init的时候取消函数 151 | if not content_size: 152 | return 153 | # 预加载 154 | if content_size and content_size - self.cur_offset <= 5 * reader_h: 155 | self.req_data_bg() 156 | 157 | # 滚动条下移 158 | if is_scroll_down: 159 | while True: 160 | item_end = self.items[-1] 161 | item_start = self.items[0] 162 | if item_end.y + self.ITEM_H -reader_h < offset and item_end.i is not None: 163 | i, j = item_end.i, item_end.j + self.LEN_LINE 164 | if len(self.contents[i][0]) <= j: 165 | i, j = i + 1, 0 166 | if i >= len(self.contents): 167 | text = self.LOADING 168 | self.req_data_bg() 169 | i, j = None, None 170 | 171 | else: 172 | text = self.contents[i][0][j: j + self.LEN_LINE] 173 | 174 | item_start.text = text 175 | item_start.y = item_end.y + self.ITEM_H 176 | item_start.i = i 177 | item_start.j = j 178 | self.items.rotate(-1) 179 | elif item_end.i is None and item_end.y <= offset + reader_h: 180 | if self.load_data() is None: 181 | break 182 | 183 | item = self.items[-2] 184 | i = item.i + 1 185 | j = 0 186 | 187 | # 由于一定会补足间隔行,所以这里i一定不越界 188 | text = self.contents[i][0][j: j + self.LEN_LINE] 189 | 190 | item_end.text = text 191 | item_end.y = item.y + self.ITEM_H 192 | item_end.i = i 193 | item_end.j = j 194 | 195 | else: 196 | break 197 | else: 198 | while True: 199 | item_end = self.items[-1] 200 | item_start = self.items[0] 201 | if item_start.y > offset: 202 | i = item_start.i 203 | j = item_start.j - self.LEN_LINE 204 | if i is None: 205 | break 206 | if j < 0: 207 | para, lines = self.contents[i-1] 208 | i, j = i - 1, (lines - 1) * self.LEN_LINE 209 | 210 | if i < 0: 211 | break 212 | text = self.contents[i][0][j: j + self.LEN_LINE] 213 | item_end.text = text 214 | 215 | item_end.y = item_start.y - self.ITEM_H 216 | item_end.i = i 217 | item_end.j = j 218 | self.items.rotate(1) 219 | 220 | else: 221 | break 222 | self.refresh_title() 223 | -------------------------------------------------------------------------------- /ebody_viewer/eimg_body.pyui: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nodes" : [ 4 | { 5 | "nodes" : [ 6 | 7 | ], 8 | "frame" : "{{-4, 0}, {347, 535}}", 9 | "class" : "ScrollView", 10 | "attributes" : { 11 | "frame" : "{{0, 80}, {320, 320}}", 12 | "uuid" : "F0410398-BBC2-4979-9425-4E0D8CD71948", 13 | "custom_attributes" : "", 14 | "content_height" : 999, 15 | "class" : "ScrollView", 16 | "content_width" : 281, 17 | "name" : "scrollview" 18 | }, 19 | "selected" : false 20 | }, 21 | { 22 | "nodes" : [ 23 | 24 | ], 25 | "frame" : "{{0, 0}, {337, 70}}", 26 | "class" : "ImageView", 27 | "attributes" : { 28 | "custom_attributes" : "{\"i\": 0}", 29 | "frame" : "{{122, 245}, {100, 100}}", 30 | "class" : "ImageView", 31 | "name" : "imageview0", 32 | "uuid" : "F96DD310-BF53-43D1-ACDB-EEB8D95D4C69" 33 | }, 34 | "selected" : false 35 | }, 36 | { 37 | "nodes" : [ 38 | 39 | ], 40 | "frame" : "{{0, 70}, {337, 70}}", 41 | "class" : "ImageView", 42 | "attributes" : { 43 | "custom_attributes" : "{\"i\": 0}", 44 | "frame" : "{{122, 245}, {100, 100}}", 45 | "class" : "ImageView", 46 | "name" : "imageview1", 47 | "uuid" : "5F47ED22-944D-4C6E-AE51-30C54B77DDAD" 48 | }, 49 | "selected" : false 50 | }, 51 | { 52 | "nodes" : [ 53 | 54 | ], 55 | "frame" : "{{0, 140}, {337, 70}}", 56 | "class" : "ImageView", 57 | "attributes" : { 58 | "custom_attributes" : "{\"i\": 0}", 59 | "frame" : "{{122, 245}, {100, 100}}", 60 | "class" : "ImageView", 61 | "name" : "imageview2", 62 | "uuid" : "BD05FE8E-7E4C-45B8-8C90-2F9FAF7F469D" 63 | }, 64 | "selected" : false 65 | }, 66 | { 67 | "nodes" : [ 68 | 69 | ], 70 | "frame" : "{{0, 210}, {337, 70}}", 71 | "class" : "ImageView", 72 | "attributes" : { 73 | "custom_attributes" : "{\"i\": 0}", 74 | "frame" : "{{122, 245}, {100, 100}}", 75 | "class" : "ImageView", 76 | "name" : "imageview3", 77 | "uuid" : "8CBA1503-53D6-48F5-AE14-8CE76BBA4A00" 78 | }, 79 | "selected" : false 80 | }, 81 | { 82 | "nodes" : [ 83 | 84 | ], 85 | "frame" : "{{0, 280}, {337, 70}}", 86 | "class" : "ImageView", 87 | "attributes" : { 88 | "custom_attributes" : "{\"i\": 0}", 89 | "frame" : "{{122, 245}, {100, 100}}", 90 | "class" : "ImageView", 91 | "name" : "imageview4", 92 | "uuid" : "44EBE85E-6711-41B2-853E-D3F332A6F0E2" 93 | }, 94 | "selected" : false 95 | }, 96 | { 97 | "nodes" : [ 98 | 99 | ], 100 | "frame" : "{{0, 350}, {337, 70}}", 101 | "class" : "ImageView", 102 | "attributes" : { 103 | "custom_attributes" : "{\"i\": 0}", 104 | "frame" : "{{122, 245}, {100, 100}}", 105 | "class" : "ImageView", 106 | "name" : "imageview5", 107 | "uuid" : "DD2E7E3A-5BD3-4EF9-8D96-FAF0DF628003" 108 | }, 109 | "selected" : false 110 | }, 111 | { 112 | "nodes" : [ 113 | 114 | ], 115 | "frame" : "{{0, 420}, {337, 70}}", 116 | "class" : "ImageView", 117 | "attributes" : { 118 | "custom_attributes" : "{\"i\": 0}", 119 | "frame" : "{{122, 245}, {100, 100}}", 120 | "class" : "ImageView", 121 | "name" : "imageview6", 122 | "uuid" : "3809C057-07C6-458D-8A27-4E96D6A4DCD5" 123 | }, 124 | "selected" : false 125 | }, 126 | { 127 | "nodes" : [ 128 | 129 | ], 130 | "frame" : "{{0, 490}, {337, 70}}", 131 | "class" : "ImageView", 132 | "attributes" : { 133 | "custom_attributes" : "{\"i\": 0}", 134 | "frame" : "{{122, 245}, {100, 100}}", 135 | "class" : "ImageView", 136 | "name" : "imageview7", 137 | "uuid" : "43A7B8C5-E6D8-4F3F-8A9F-E98C36B0A0E4" 138 | }, 139 | "selected" : false 140 | }, 141 | { 142 | "nodes" : [ 143 | 144 | ], 145 | "frame" : "{{0, 560}, {337, 70}}", 146 | "class" : "ImageView", 147 | "attributes" : { 148 | "custom_attributes" : "{\"i\": 0}", 149 | "frame" : "{{122, 245}, {100, 100}}", 150 | "class" : "ImageView", 151 | "name" : "imageview8", 152 | "uuid" : "534B796D-B40C-4702-A419-28BE5662DB47" 153 | }, 154 | "selected" : true 155 | } 156 | ], 157 | "frame" : "{{0, 0}, {343, 590}}", 158 | "class" : "View", 159 | "attributes" : { 160 | "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)", 161 | "enabled" : true, 162 | "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)", 163 | "name" : "", 164 | "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)", 165 | "flex" : "" 166 | }, 167 | "selected" : false 168 | } 169 | ] -------------------------------------------------------------------------------- /ebody_viewer/eimg_body_viewer.py: -------------------------------------------------------------------------------- 1 | from itertools import islice 2 | from collections import deque 3 | import ui 4 | import console 5 | from ui import Image 6 | 7 | 8 | class EImgBodyViewer: 9 | ITEM_H = 70 10 | WIDTH_LINE = 337 11 | LOADING = Image.named('iob:load_d_32') 12 | LOADING_HEIGHT = WIDTH_LINE / LOADING.size.x * LOADING.size.y 13 | 14 | def __init__(self, parent): 15 | view = ui.load_view('ebody_viewer/eimg_body') 16 | scrollview = view['scrollview'] 17 | for i in range(9): 18 | # print(view[f'imageview{i}']) 19 | scrollview.add_subview(view[f'imageview{i}']) 20 | scrollview.delegate = self 21 | self.has_sent_req = False 22 | self.scrollview = scrollview 23 | view.right_btns_desc = 'menu' 24 | self.view = view 25 | self.items = deque(self.scrollview.subviews) 26 | assert (len(self.items) - 1) * self.ITEM_H > scrollview.height 27 | self.parent = parent 28 | 29 | def req_data_bg(self): 30 | self.parent.req_data_bg(self) 31 | 32 | def req_data(self, init=False): 33 | self.parent.req_data(self, init) 34 | 35 | def set_navi_view_name(self, name): 36 | self.parent.set_navi_view_name(name) 37 | 38 | def refresh_title(self): 39 | off_set = self.cur_offset 40 | for item in self.items: 41 | if item.y + item.height >= off_set: 42 | i = item.i 43 | if i is None: 44 | continue 45 | 46 | for l, r, name, url in self.titles: 47 | if l <= i < r: 48 | self.set_navi_view_name(name) 49 | return 50 | 51 | def load_data(self): 52 | element = self.parent.load_data(self) 53 | if element is None: 54 | return False 55 | if element[0] is None: 56 | # 到底之后不再复位self.has_sent_req 57 | console.hud_alert('已经阅读完毕') 58 | return False 59 | l = len(self.contents) 60 | imgs, title, url, init = element 61 | 62 | # 保证进度条不会完全到底 63 | sum_height = -10 if init else 0 64 | for img in imgs: 65 | resized_height = self.WIDTH_LINE / img.size.x * img.size.y 66 | sum_height += resized_height 67 | 68 | self.contents.append((img, resized_height)) 69 | 70 | r = len(self.contents) 71 | if self.titles: 72 | # merge 当一个页面多个图片 73 | last_title = self.titles[-1] 74 | if last_title[3] == url: 75 | self.titles[-1] = (last_title[0], r, title, url) 76 | else: 77 | self.titles.append((l, r, title, url)) 78 | else: 79 | self.titles.append((l, r, title, url)) 80 | 81 | self.scrollview.content_size += (0, sum_height) 82 | self.has_sent_req = False 83 | return True 84 | 85 | def reset_view(self, i=0, j=0): 86 | scrollview = self.scrollview 87 | # [(image, resized_height),] 88 | self.contents = [] 89 | # (l, r, name) 90 | self.titles = [] 91 | 92 | scrollview.content_size = (scrollview.width, 0) 93 | 94 | self.cur_offset = 0 95 | 96 | self.req_data(True) 97 | self.load_data() 98 | # 条件应该是把一页填充满而且书签模式下尽量加载 99 | while scrollview.content_size.y <= scrollview.height or len(self.contents) <= i: 100 | self.req_data() 101 | self.load_data() 102 | # print(scrollview.content_size) 103 | 104 | rows = i 105 | len_content = len(self.contents) 106 | len_items = len(self.items) 107 | y = 0 108 | start = max(0, len_items - len_content) 109 | remain = islice(self.items, start, len_items) 110 | for i, (item, content) in enumerate(zip(remain, self.contents)): 111 | img, resize_height = content 112 | item.height = resize_height 113 | item.image = img 114 | item.i = i 115 | item.y = y 116 | y += resize_height 117 | 118 | # 未使用的item往上放 119 | y = 0 120 | if len_content < len_items: 121 | end = len_items - len_content - 1 122 | for i in range(end, -1, -1): 123 | item = self.items[i] 124 | item.i = None 125 | item.image = None 126 | y -= item.height 127 | item.y = y 128 | 129 | # 书签偏移量 130 | h = 0 131 | for content in self.contents[:rows]: 132 | img, resize_height = content 133 | h += resize_height 134 | 135 | # 这个破玩意儿改了之后会自动调用监听函数 136 | scrollview.content_offset = (0, h+j) 137 | self.refresh_title() 138 | 139 | def get_offset(self): 140 | scrollview = self.scrollview 141 | off_set = scrollview.content_offset.y 142 | i = None 143 | for item in self.items: 144 | if item.y + item.height >= off_set: 145 | i = item.i 146 | j = off_set - item.y 147 | break 148 | if i is None: 149 | return 150 | for l, r, name, url in self.titles: 151 | if l <= i < r: 152 | # 本页的i 相对段落数目 153 | new_bookmark = { 154 | 'i': i - l, 155 | 'j': int(j), 156 | 'url': url, 157 | 'title': name 158 | } 159 | return new_bookmark 160 | 161 | def reset_scrollbar(self): 162 | scrollview = self.scrollview 163 | offset_x, offset_y = scrollview.content_offset 164 | max_offset = scrollview.content_size.y - scrollview.height 165 | cur_offset = offset_y 166 | min_offset = min(cur_offset, max_offset) 167 | scrollview.content_offset = (offset_x, min_offset) 168 | 169 | def scrollview_did_scroll(self, scrollview): 170 | offset = scrollview.content_offset.y 171 | is_scroll_down = True if offset > self.cur_offset else False 172 | self.cur_offset = offset 173 | 174 | reader_h = scrollview.height 175 | # print('t') 176 | 177 | content_size = scrollview.content_size[1] 178 | if not content_size: 179 | return 180 | # 预加载 181 | if content_size - self.cur_offset <= 4 * reader_h: 182 | self.req_data_bg() 183 | 184 | # 滚动条下移 185 | if is_scroll_down: 186 | while True: 187 | item_end = self.items[-1] 188 | item_start = self.items[0] 189 | if (item_end.y + item_end.height - reader_h <= offset and item_end.i is not None): 190 | # console.hud_alert('应该load') 191 | i = item_end.i + 1 192 | 193 | if i >= len(self.contents): 194 | img = self.LOADING 195 | resized_height = self.LOADING_HEIGHT 196 | self.req_data_bg() 197 | i = None 198 | 199 | else: 200 | img, resized_height = self.contents[i] 201 | 202 | item_start.image = img 203 | item_start.y = item_end.y + item_end.height 204 | item_start.height = resized_height 205 | item_start.i = i 206 | self.items.rotate(-1) 207 | elif item_end.i is None and item_end.y <= offset + reader_h: 208 | # console.hud_alert(f'应该refresh') 209 | if not self.load_data(): 210 | break 211 | 212 | item = self.items[-2] 213 | i = item.i + 1 214 | 215 | if i < len(self.contents): 216 | img, resized_height = self.contents[i] 217 | 218 | item_end.image = img 219 | item_end.y = item.y + item.height 220 | item_end.height = resized_height 221 | item_end.i = i 222 | # 这个防止空白页吧……会有吗? 223 | else: 224 | break 225 | else: 226 | break 227 | else: 228 | while True: 229 | item_end = self.items[-1] 230 | item_start = self.items[0] 231 | if item_start.y >= offset: 232 | # None的时候仅仅是初始化设计导致的 233 | if item_start.i is None or item_start.i <= 0: 234 | break 235 | i = item_start.i - 1 236 | img, resized_height = self.contents[i] 237 | item_end.image = img 238 | item_end.height = resized_height 239 | item_end.y = item_start.y - resized_height 240 | item_end.i = i 241 | self.items.rotate(1) 242 | 243 | else: 244 | break 245 | self.refresh_title() 246 | 247 | -------------------------------------------------------------------------------- /ereader.py: -------------------------------------------------------------------------------- 1 | from index_viewer.index_viewer import IndexViewer 2 | from ebody_viewer.ebook_body_viewer import EBookBodyViewer 3 | from ebody_viewer.eimg_body_viewer import EImgBodyViewer 4 | 5 | 6 | class EReader: 7 | def __init__(self, EBodyViewer, controller): 8 | self.var_ebody_viewer = EBodyViewer(self) 9 | self.var_index_viewer = IndexViewer(self) 10 | self.controller = controller 11 | 12 | def req_data_bg(self, var): 13 | if var is self.var_ebody_viewer: 14 | self.controller.req_ebody_data_bg() 15 | elif var is self.var_index_viewer: 16 | self.controller.req_eindex_data_bg() 17 | 18 | def req_data(self, var, init=False): 19 | if var is self.var_ebody_viewer: 20 | self.controller.req_ebody_data(init) 21 | elif var is self.var_index_viewer: 22 | self.controller.req_eindex_data(init) 23 | 24 | def load_data(self, var): 25 | if var is self.var_ebody_viewer: 26 | return self.controller.load_ebody_data() 27 | elif var is self.var_index_viewer: 28 | return self.controller.load_eindex_data() 29 | 30 | def open_url(self, url): 31 | 32 | self.controller.load_reader(url, is_init=False) 33 | 34 | def set_navi_view_name(self, name): 35 | self.controller.set_navi_view_name(name) 36 | 37 | def reset_view(self, i, j): 38 | # 先后顺序不能反,因为两个都会调用set_navi_view_name这个api,会覆盖 39 | self.var_index_viewer.reset_view(i, j) 40 | self.var_ebody_viewer.reset_view(i, j) 41 | 42 | def reset_scrollbar(self): 43 | self.var_ebody_viewer.reset_scrollbar() 44 | 45 | def get_offset(self): 46 | return self.var_ebody_viewer.get_offset() 47 | 48 | 49 | class EImgReader(EReader): 50 | def __init__(self, controller): 51 | super().__init__(EImgBodyViewer, controller) 52 | 53 | 54 | class EBookReader(EReader): 55 | def __init__(self, controller): 56 | super().__init__(EBookBodyViewer, controller) 57 | 58 | 59 | -------------------------------------------------------------------------------- /home_viewer/home.pyui: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nodes" : [ 4 | { 5 | "nodes" : [ 6 | 7 | ], 8 | "frame" : "{{30, 80}, {104, 32}}", 9 | "class" : "Button", 10 | "attributes" : { 11 | "image_name" : "iob:ios7_eye_32", 12 | "frame" : "{{150, 271}, {80, 32}}", 13 | "title" : "查看书签", 14 | "uuid" : "D9A923CD-78BE-418B-8ED2-4B32E281363E", 15 | "class" : "Button", 16 | "font_bold" : true, 17 | "font_size" : 15, 18 | "name" : "btn_open_bm" 19 | }, 20 | "selected" : false 21 | }, 22 | { 23 | "nodes" : [ 24 | 25 | ], 26 | "frame" : "{{30, 240}, {104, 32}}", 27 | "class" : "Button", 28 | "attributes" : { 29 | "font_size" : 15, 30 | "frame" : "{{150, 271}, {80, 32}}", 31 | "title" : "在线小说", 32 | "uuid" : "18DF2A60-3D5E-4A4F-9B03-1A8C7B223192", 33 | "class" : "Button", 34 | "font_bold" : true, 35 | "name" : "btn_search_ebook", 36 | "image_name" : "iob:ios7_bookmarks_24" 37 | }, 38 | "selected" : false 39 | }, 40 | { 41 | "nodes" : [ 42 | 43 | ], 44 | "frame" : "{{30, 320}, {104, 32}}", 45 | "class" : "Button", 46 | "attributes" : { 47 | "image_name" : "iob:search_24", 48 | "frame" : "{{150, 271}, {80, 32}}", 49 | "title" : "追书神器", 50 | "uuid" : "18DF2A60-3D5E-4A4F-9B03-1A8C7B223192", 51 | "class" : "Button", 52 | "font_bold" : true, 53 | "font_size" : 15, 54 | "name" : "btn_search_zsbook" 55 | }, 56 | "selected" : true 57 | }, 58 | { 59 | "nodes" : [ 60 | 61 | ], 62 | "frame" : "{{30, 160}, {104, 32}}", 63 | "class" : "Button", 64 | "attributes" : { 65 | "font_size" : 15, 66 | "frame" : "{{150, 271}, {80, 32}}", 67 | "title" : "在线漫画", 68 | "uuid" : "18DF2A60-3D5E-4A4F-9B03-1A8C7B223192", 69 | "class" : "Button", 70 | "font_bold" : true, 71 | "name" : "btn_search_eimg", 72 | "image_name" : "iob:images_24" 73 | }, 74 | "selected" : false 75 | } 76 | ], 77 | "frame" : "{{0, 0}, {380, 573}}", 78 | "class" : "View", 79 | "attributes" : { 80 | "enabled" : true, 81 | "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)", 82 | "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)", 83 | "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)", 84 | "flex" : "" 85 | }, 86 | "selected" : false 87 | } 88 | ] -------------------------------------------------------------------------------- /home_viewer/viewer.py: -------------------------------------------------------------------------------- 1 | import ui 2 | import console 3 | 4 | 5 | class HomeViewer: 6 | def __init__(self, controller): 7 | self.controller = controller 8 | view = ui.load_view('home_viewer/home') 9 | btn_open_bm = view['btn_open_bm'] 10 | btn_open_bm.action = self._open_bm 11 | 12 | btn_search_eimg = view['btn_search_eimg'] 13 | btn_search_eimg.action = self._search_eimg 14 | 15 | btn_search_ebook = view['btn_search_ebook'] 16 | btn_search_ebook.action = self._search_ebook 17 | 18 | btn_search_zsbook = view['btn_search_zsbook'] 19 | btn_search_zsbook.action = self._search_zsbook 20 | 21 | view.right_btns_desc = 'empty' 22 | self.view = view 23 | 24 | def _open_bm(self, *args): 25 | self.controller.pop_1_view() 26 | self.controller.push_bm_viewer() 27 | 28 | def _input_alert(self, title, inputted=None): 29 | if inputted is None: 30 | inputted = '' 31 | try: 32 | text = console.input_alert(title, '', inputted) 33 | 34 | if text: 35 | return text 36 | except KeyboardInterrupt: 37 | return None 38 | return None 39 | 40 | def _search_eimg(self, *args): 41 | url = self._input_alert('请输入在线漫画的网址') 42 | if url is not None: 43 | self.controller.load_eimg(url) 44 | 45 | def _search_ebook(self, *args): 46 | url = self._input_alert('请输入在线小说的网址') 47 | if url is not None: 48 | self.controller.load_ebook(url) 49 | 50 | def _search_zsbook(self, *args): 51 | keywords = self._input_alert('请输入要搜索的书目的关键词') 52 | if keywords is not None: 53 | self.controller.search_zsbook(keywords) 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /index_viewer/index.pyui: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nodes" : [ 4 | { 5 | "nodes" : [ 6 | 7 | ], 8 | "frame" : "{{16, 12}, {364, 548}}", 9 | "class" : "ScrollView", 10 | "attributes" : { 11 | "uuid" : "2BA1A1E6-1805-4569-A830-49EC5693B4FC", 12 | "frame" : "{{30, 127}, {320, 320}}", 13 | "content_height" : 320, 14 | "class" : "ScrollView", 15 | "name" : "scrollview", 16 | "content_width" : 320 17 | }, 18 | "selected" : false 19 | }, 20 | { 21 | "nodes" : [ 22 | 23 | ], 24 | "frame" : "{{6, 6}, {360, 32}}", 25 | "class" : "Button", 26 | "attributes" : { 27 | "frame" : "{{150, 271}, {80, 32}}", 28 | "title" : "Button", 29 | "uuid" : "BE0A5B75-037B-4913-A530-B67E67F375A8", 30 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 31 | "class" : "Button", 32 | "font_bold" : true, 33 | "name" : "button0", 34 | "font_size" : 15 35 | }, 36 | "selected" : false 37 | }, 38 | { 39 | "nodes" : [ 40 | 41 | ], 42 | "frame" : "{{6, 38}, {360, 32}}", 43 | "class" : "Button", 44 | "attributes" : { 45 | "frame" : "{{150, 271}, {80, 32}}", 46 | "title" : "Button", 47 | "uuid" : "E25F1D84-4499-467C-A491-B45DEB1A39EB", 48 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 49 | "class" : "Button", 50 | "font_bold" : true, 51 | "name" : "button1", 52 | "font_size" : 15 53 | }, 54 | "selected" : false 55 | }, 56 | { 57 | "nodes" : [ 58 | 59 | ], 60 | "frame" : "{{6, 70}, {360, 32}}", 61 | "class" : "Button", 62 | "attributes" : { 63 | "frame" : "{{150, 271}, {80, 32}}", 64 | "title" : "Button", 65 | "uuid" : "EEF74293-84A4-499B-BD93-6B0F88C55C1F", 66 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 67 | "class" : "Button", 68 | "font_bold" : true, 69 | "name" : "button2", 70 | "font_size" : 15 71 | }, 72 | "selected" : false 73 | }, 74 | { 75 | "nodes" : [ 76 | 77 | ], 78 | "frame" : "{{6, 102}, {360, 32}}", 79 | "class" : "Button", 80 | "attributes" : { 81 | "frame" : "{{150, 271}, {80, 32}}", 82 | "title" : "Button", 83 | "uuid" : "468E9C32-845B-4241-AF69-42BDCE1AB532", 84 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 85 | "class" : "Button", 86 | "font_bold" : true, 87 | "name" : "button3", 88 | "font_size" : 15 89 | }, 90 | "selected" : false 91 | }, 92 | { 93 | "nodes" : [ 94 | 95 | ], 96 | "frame" : "{{6, 134}, {360, 32}}", 97 | "class" : "Button", 98 | "attributes" : { 99 | "frame" : "{{150, 271}, {80, 32}}", 100 | "title" : "Button", 101 | "uuid" : "09FF5423-A04F-43F6-8BBD-DB61EC688DE9", 102 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 103 | "class" : "Button", 104 | "font_bold" : true, 105 | "name" : "button4", 106 | "font_size" : 15 107 | }, 108 | "selected" : false 109 | }, 110 | { 111 | "nodes" : [ 112 | 113 | ], 114 | "frame" : "{{6, 166}, {360, 32}}", 115 | "class" : "Button", 116 | "attributes" : { 117 | "frame" : "{{150, 271}, {80, 32}}", 118 | "title" : "Button", 119 | "uuid" : "A374CE19-F64F-44AB-A2DD-E7EF2057C362", 120 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 121 | "class" : "Button", 122 | "font_bold" : true, 123 | "name" : "button5", 124 | "font_size" : 15 125 | }, 126 | "selected" : false 127 | }, 128 | { 129 | "nodes" : [ 130 | 131 | ], 132 | "frame" : "{{6, 198}, {360, 32}}", 133 | "class" : "Button", 134 | "attributes" : { 135 | "frame" : "{{150, 271}, {80, 32}}", 136 | "title" : "Button", 137 | "uuid" : "F07F141E-A2B9-44F2-AA16-20E986E2A0B7", 138 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 139 | "class" : "Button", 140 | "font_bold" : true, 141 | "name" : "button6", 142 | "font_size" : 15 143 | }, 144 | "selected" : false 145 | }, 146 | { 147 | "nodes" : [ 148 | 149 | ], 150 | "frame" : "{{6, 230}, {360, 32}}", 151 | "class" : "Button", 152 | "attributes" : { 153 | "frame" : "{{150, 271}, {80, 32}}", 154 | "title" : "Button", 155 | "uuid" : "24E2B957-47D7-4107-94F8-261371181D45", 156 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 157 | "class" : "Button", 158 | "font_bold" : true, 159 | "name" : "button7", 160 | "font_size" : 15 161 | }, 162 | "selected" : false 163 | }, 164 | { 165 | "nodes" : [ 166 | 167 | ], 168 | "frame" : "{{6, 262}, {360, 32}}", 169 | "class" : "Button", 170 | "attributes" : { 171 | "frame" : "{{150, 271}, {80, 32}}", 172 | "title" : "Button", 173 | "uuid" : "60F71997-71BD-4E28-8CCF-1BCF85FB3335", 174 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 175 | "class" : "Button", 176 | "font_bold" : true, 177 | "name" : "button8", 178 | "font_size" : 15 179 | }, 180 | "selected" : false 181 | }, 182 | { 183 | "nodes" : [ 184 | 185 | ], 186 | "frame" : "{{6, 294}, {360, 32}}", 187 | "class" : "Button", 188 | "attributes" : { 189 | "frame" : "{{150, 271}, {80, 32}}", 190 | "title" : "Button", 191 | "uuid" : "67F1CA92-5F7D-4799-99B6-D032BB047C57", 192 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 193 | "class" : "Button", 194 | "font_bold" : true, 195 | "name" : "button9", 196 | "font_size" : 15 197 | }, 198 | "selected" : false 199 | }, 200 | { 201 | "nodes" : [ 202 | 203 | ], 204 | "frame" : "{{6, 326}, {360, 32}}", 205 | "class" : "Button", 206 | "attributes" : { 207 | "frame" : "{{150, 271}, {80, 32}}", 208 | "title" : "Button", 209 | "uuid" : "5FF45113-F4A1-45B1-8062-00D06B73611F", 210 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 211 | "class" : "Button", 212 | "font_bold" : true, 213 | "name" : "button10", 214 | "font_size" : 15 215 | }, 216 | "selected" : false 217 | }, 218 | { 219 | "nodes" : [ 220 | 221 | ], 222 | "frame" : "{{6, 358}, {360, 32}}", 223 | "class" : "Button", 224 | "attributes" : { 225 | "frame" : "{{150, 271}, {80, 32}}", 226 | "title" : "Button", 227 | "uuid" : "B6FE1FD0-B2D7-4011-8240-B35A487DD540", 228 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 229 | "class" : "Button", 230 | "font_bold" : true, 231 | "name" : "button11", 232 | "font_size" : 15 233 | }, 234 | "selected" : false 235 | }, 236 | { 237 | "nodes" : [ 238 | 239 | ], 240 | "frame" : "{{6, 390}, {360, 32}}", 241 | "class" : "Button", 242 | "attributes" : { 243 | "frame" : "{{150, 271}, {80, 32}}", 244 | "title" : "Button", 245 | "uuid" : "58BBDB37-3DFD-4EA7-AD1C-92D0D89611E9", 246 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 247 | "class" : "Button", 248 | "font_bold" : true, 249 | "name" : "button12", 250 | "font_size" : 15 251 | }, 252 | "selected" : false 253 | }, 254 | { 255 | "nodes" : [ 256 | 257 | ], 258 | "frame" : "{{6, 422}, {360, 32}}", 259 | "class" : "Button", 260 | "attributes" : { 261 | "frame" : "{{150, 271}, {80, 32}}", 262 | "title" : "Button", 263 | "uuid" : "E536C7C5-800E-465D-9025-B2A0A94CC11A", 264 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 265 | "class" : "Button", 266 | "font_bold" : true, 267 | "name" : "button13", 268 | "font_size" : 15 269 | }, 270 | "selected" : false 271 | }, 272 | { 273 | "nodes" : [ 274 | 275 | ], 276 | "frame" : "{{6, 454}, {360, 32}}", 277 | "class" : "Button", 278 | "attributes" : { 279 | "frame" : "{{150, 271}, {80, 32}}", 280 | "title" : "Button", 281 | "uuid" : "A7054E48-B5B6-478D-AFF5-7C0CF19B7888", 282 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 283 | "class" : "Button", 284 | "font_bold" : true, 285 | "name" : "button14", 286 | "font_size" : 15 287 | }, 288 | "selected" : false 289 | }, 290 | { 291 | "nodes" : [ 292 | 293 | ], 294 | "frame" : "{{6, 486}, {360, 32}}", 295 | "class" : "Button", 296 | "attributes" : { 297 | "frame" : "{{150, 271}, {80, 32}}", 298 | "title" : "Button", 299 | "uuid" : "87A6573A-0850-4120-A0A5-D480EE7FE1AC", 300 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 301 | "class" : "Button", 302 | "font_bold" : true, 303 | "name" : "button15", 304 | "font_size" : 15 305 | }, 306 | "selected" : false 307 | }, 308 | { 309 | "nodes" : [ 310 | 311 | ], 312 | "frame" : "{{6, 518}, {360, 32}}", 313 | "class" : "Button", 314 | "attributes" : { 315 | "frame" : "{{150, 271}, {80, 32}}", 316 | "title" : "Button", 317 | "uuid" : "66D7B9B0-128E-4A6F-9F87-6C320BC8EAEE", 318 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 319 | "class" : "Button", 320 | "font_bold" : true, 321 | "name" : "button16", 322 | "font_size" : 15 323 | }, 324 | "selected" : false 325 | }, 326 | { 327 | "nodes" : [ 328 | 329 | ], 330 | "frame" : "{{6, 550}, {360, 32}}", 331 | "class" : "Button", 332 | "attributes" : { 333 | "frame" : "{{150, 271}, {80, 32}}", 334 | "title" : "Button", 335 | "uuid" : "AF2306E6-439A-44F4-BC59-17223A2C194E", 336 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 337 | "class" : "Button", 338 | "font_bold" : true, 339 | "name" : "button17", 340 | "font_size" : 15 341 | }, 342 | "selected" : false 343 | }, 344 | { 345 | "nodes" : [ 346 | 347 | ], 348 | "frame" : "{{6, 582}, {360, 32}}", 349 | "class" : "Button", 350 | "attributes" : { 351 | "frame" : "{{150, 271}, {80, 32}}", 352 | "title" : "Button", 353 | "uuid" : "D74E9EE8-9041-44EA-A4AB-1E5F308E75B9", 354 | "custom_attributes" : "{\"i\": 0, \"url\": \"\"}", 355 | "class" : "Button", 356 | "font_bold" : true, 357 | "name" : "button18", 358 | "font_size" : 15 359 | }, 360 | "selected" : true 361 | } 362 | ], 363 | "frame" : "{{0, 0}, {380, 573}}", 364 | "class" : "View", 365 | "attributes" : { 366 | "enabled" : true, 367 | "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)", 368 | "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)", 369 | "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)", 370 | "flex" : "" 371 | }, 372 | "selected" : false 373 | } 374 | ] -------------------------------------------------------------------------------- /index_viewer/index_viewer.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | import ui 3 | import console 4 | 5 | 6 | class IndexViewer: 7 | ITEM_H = 32 8 | LEN_LINE = 19 9 | LOADING = 'LOADING...' 10 | 11 | def __init__(self, parent): 12 | view = ui.load_view('index_viewer/index') 13 | scrollview = view['scrollview'] 14 | 15 | for i in range(19): 16 | button = view[f'button{i}'] 17 | scrollview.add_subview(button) 18 | button.action = self.open_url 19 | self.has_sent_req = False 20 | self.scrollview = scrollview 21 | view.right_btns_desc = 'return' 22 | self.view = view 23 | scrollview.delegate = self 24 | self.items = deque(self.scrollview.subviews) 25 | assert (len(self.items) - 1) * self.ITEM_H > scrollview.height 26 | self.parent = parent 27 | 28 | def open_url(self, sender): 29 | if sender.url is not None: 30 | self.parent.open_url(sender.url) 31 | console.hud_alert(sender.url) 32 | 33 | def req_data_bg(self): 34 | self.parent.req_data_bg(self) 35 | 36 | def req_data(self, init=False): 37 | self.parent.req_data(self, init) 38 | 39 | def set_navi_view_name(self, name): 40 | self.parent.set_navi_view_name(name) 41 | 42 | def refresh_title(self): 43 | pass 44 | 45 | def load_data(self): 46 | element = self.parent.load_data(self) 47 | if element is None: 48 | return None 49 | if element[0] is None: 50 | if not element[2]: 51 | console.hud_alert('已经阅读完毕') 52 | return None 53 | chapter, title, init = element 54 | sum_num_lines = 0 if init else 1 55 | if init: 56 | self.set_navi_view_name(title) 57 | for para in chapter: 58 | sum_num_lines += 1 59 | self.contents.append((para)) 60 | 61 | split_contents = '—' * self.LEN_LINE 62 | self.contents.append((None, split_contents)) 63 | 64 | self.scrollview.content_size += (0, sum_num_lines * self.ITEM_H) 65 | self.has_sent_req = False 66 | return sum_num_lines 67 | 68 | def reset_view(self, i=0, j=0): 69 | scrollview = self.scrollview 70 | # 这里把offset归零了,并调用了函数 71 | scrollview.content_size = (scrollview.width, 0) 72 | 73 | self.cur_offset = 0 74 | 75 | self.contents = [] 76 | 77 | self.req_data(True) 78 | sum_num_lines = self.load_data() 79 | if sum_num_lines is None: 80 | for item in self.items: 81 | item.title = '' 82 | item.url = None 83 | 84 | self.items[0].title = '无目录' 85 | self.items[0].y = 0 86 | 87 | return 88 | # 其实应该仿照img模块的,但是没必要,白白把逻辑弄麻烦了 89 | while sum_num_lines <= len(self.items): 90 | self.req_data() 91 | sum_num_lines += self.load_data() 92 | 93 | i = 0 94 | y = 0 95 | for item in self.items: 96 | item.url, item.title = self.contents[i] 97 | # i代表段落index,j代表了段落里面具体的文字起始下标 98 | item.i = i 99 | item.j = j 100 | item.y = y 101 | i += 1 102 | y += self.ITEM_H 103 | 104 | def reset_scrollbar(self): 105 | pass 106 | 107 | def scrollview_did_scroll(self, scrollview): 108 | offset = scrollview.content_offset.y 109 | is_scroll_down = True if offset > self.cur_offset else False 110 | self.cur_offset = offset 111 | # print('t') 112 | 113 | reader_h = scrollview.height 114 | # print('t') 115 | 116 | content_size = scrollview.content_size[1] 117 | # 预加载 118 | if content_size and content_size - self.cur_offset <= 3.5 * reader_h: 119 | self.req_data_bg() 120 | 121 | # 滚动条下移 122 | if is_scroll_down: 123 | while True: 124 | item_end = self.items[-1] 125 | item_start = self.items[0] 126 | if item_end.y + self.ITEM_H -reader_h < offset and item_end.i is not None: 127 | i = item_end.i + 1 128 | if i >= len(self.contents): 129 | title = self.LOADING 130 | self.req_data_bg() 131 | i = None 132 | url = None 133 | 134 | else: 135 | url, title = self.contents[i] 136 | 137 | item_start.title = title 138 | item_start.y = item_end.y + self.ITEM_H 139 | item_start.i = i 140 | item_start.url = url 141 | self.items.rotate(-1) 142 | elif item_end.i is None and item_end.y <= offset + reader_h: 143 | if self.load_data() is None: 144 | break 145 | 146 | item = self.items[-2] 147 | i = item.i + 1 148 | 149 | # 由于一定会补足间隔行,所以这里i一定不越界 150 | url, title = self.contents[i] 151 | item_end.title = title 152 | item_end.y = item.y + self.ITEM_H 153 | item_end.i = i 154 | item_end.url = url 155 | 156 | else: 157 | break 158 | else: 159 | while True: 160 | item_end = self.items[-1] 161 | item_start = self.items[0] 162 | 163 | if item_start.y >= offset: 164 | i = item_start.i 165 | if i is None or i <= 0: 166 | break 167 | 168 | i = i - 1 169 | url, text = self.contents[i] 170 | item_end.title = text 171 | 172 | item_end.y = item_start.y - self.ITEM_H 173 | item_end.i = i 174 | item_end.url = url 175 | self.items.rotate(1) 176 | 177 | else: 178 | break 179 | self.refresh_title() 180 | 181 | ''' 182 | 183 | controller = Controller(EBookLoader, IndexViewer) 184 | 185 | controller.load_reader(url) 186 | controller.navi_viewer.view.present('fullscreen') 187 | ''' 188 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from queue import Queue 3 | 4 | import ui 5 | import console 6 | 7 | from home_viewer.viewer import HomeViewer 8 | from zsbook_loader.zsbook_loader import ZSBookLoader 9 | from zsbook_search_viewer.viewer import ZSBookSearchViewer 10 | from menu_viewer.viewer import MenuViewer 11 | from bm_viewer.bm_viewer import BmViewer 12 | from e_loader.e_loader import EBookLoader, EImgLoader 13 | from ereader import EBookReader, EImgReader 14 | from config_loader import ConfigLoader 15 | 16 | 17 | class Controller: 18 | def __init__(self): 19 | conf_loader = ConfigLoader() 20 | dict_conf = conf_loader.dict_conf 21 | dict_bm = conf_loader.dict_bookmark 22 | self.conf_loader = conf_loader 23 | 24 | self.bm_viewer = BmViewer(self, dict_bm['bookmarks']) 25 | 26 | # init viewers 27 | self.var_zsbook_search_viewer = ZSBookSearchViewer(self) 28 | 29 | self.var_menu_viewer = MenuViewer(self) 30 | 31 | self.var_book_viewer = EBookReader(self) 32 | self.var_img_viewer = EImgReader(self) 33 | self.reader_viewer = None 34 | 35 | self.var_zsbook_loader = ZSBookLoader() 36 | self.var_ebook_loader = EBookLoader(dict_conf) 37 | self.var_eing_loader = EImgLoader(dict_conf) 38 | self.var_loader = None 39 | self.curr_type = None 40 | 41 | bottom_view = HomeViewer(self).view 42 | view = ui.NavigationView(bottom_view) 43 | self.stack_views = [bottom_view] # 额外保存,用于读取navigation view栈内信息 44 | 45 | view.navigation_bar_hidden = True 46 | 47 | btn_item_pop = ui.ButtonItem( 48 | image=ui.Image.named('iob:arrow_return_left_24')) 49 | btn_item_pop.action = self.pop_1_view 50 | 51 | btn_item_push_menu = ui.ButtonItem( 52 | image=ui.Image.named('iob:navicon_round_24')) 53 | btn_item_push_menu.action = self.push_menu_view 54 | 55 | self.btn_items_pop = [btn_item_pop] 56 | self.btn_items_push_menu = [btn_item_push_menu] 57 | 58 | self.view = view 59 | self.set_right_button_items(bottom_view.right_btns_desc) 60 | 61 | self.queue_body = None 62 | self.queue_index = None 63 | self.is_req_body = False 64 | self.is_req_index = False 65 | 66 | def set_navi_view_name(self, name): 67 | self.view.name = name 68 | 69 | # 设置navigation view的右上角的图标群 70 | def set_right_button_items(self, right_btns_desc): 71 | if right_btns_desc is None: 72 | return 73 | if right_btns_desc == 'return': 74 | self.view.right_button_items = self.btn_items_pop 75 | elif right_btns_desc == 'menu': 76 | self.view.right_button_items = self.btn_items_push_menu 77 | elif right_btns_desc == 'empty': 78 | self.view.right_button_items = [] 79 | 80 | # 和push_1_view+push_menu_view对应,不需要view,所以就合并了 81 | def pop_1_view(self, *args): 82 | if len(self.stack_views) > 1: # 如果栈非最底的时候 83 | self.view.pop_view() 84 | self.stack_views.pop() 85 | view = self.stack_views[-1] 86 | self.set_right_button_items(view.right_btns_desc) 87 | 88 | def pop_all_view(self): # 这个sb有bug,连着pop会失效?我佛了 89 | if len(self.stack_views) >= 4: 90 | return False 91 | while len(self.stack_views) > 1: 92 | self.pop_1_view() 93 | 94 | def push_1_view(self, view): 95 | self.view.push_view(view) 96 | self.stack_views.append(view) 97 | self.set_right_button_items(view.right_btns_desc) 98 | 99 | def push_menu_view(self, *args): 100 | self.push_1_view(self.var_menu_viewer.view) 101 | 102 | def push_index_viewer(self): 103 | self.push_1_view(self.reader_viewer.var_index_viewer.view) 104 | 105 | def push_bm_viewer(self): 106 | self.push_1_view(self.bm_viewer.view) 107 | 108 | def search_zsbook(self, keywords): 109 | book_id = self.var_zsbook_search_viewer.search_books( 110 | self.var_zsbook_loader, keywords) 111 | print('最终结果', book_id) 112 | 113 | if book_id is not None: 114 | self.var_zsbook_search_viewer.fetch_srcs( 115 | self.var_zsbook_loader, book_id) 116 | self.push_1_view(self.var_zsbook_search_viewer.view) 117 | 118 | def load_zsbook(self, src_id, i=0, j=0): 119 | self.var_loader = self.var_zsbook_loader 120 | self.curr_type = 'zsbook' 121 | self.reader_viewer = self.var_book_viewer 122 | self.pop_all_view() 123 | self.load_reader(src_id, i, j) 124 | 125 | def load_ebook(self, url, i=0, j=0): 126 | self.var_loader = self.var_ebook_loader 127 | self.curr_type = 'ebook' 128 | self.reader_viewer = self.var_book_viewer 129 | self.pop_all_view() 130 | self.load_reader(url, i, j) 131 | 132 | def load_eimg(self, url, i=0, j=0): 133 | self.var_loader = self.var_eing_loader 134 | self.curr_type = 'eimg' 135 | self.reader_viewer = self.var_img_viewer 136 | self.pop_all_view() 137 | self.load_reader(url, i, j) 138 | 139 | def load_reader(self, url, i=0, j=0, is_init=True): 140 | if self.is_req_body: 141 | self.thread_req_body.join() 142 | if self.is_req_index: 143 | self.thread_req_index.join() 144 | self.is_req_body = False 145 | self.is_req_index = False 146 | # self.queue.clean 147 | self.queue_body = Queue() 148 | self.queue_index = Queue() 149 | self.var_loader.set_url(url) 150 | 151 | self.reader_viewer.reset_view(i, j) 152 | if is_init: # 如果是初始化那么需要pu sh 153 | self.push_1_view(self.reader_viewer.var_ebody_viewer.view) 154 | else: 155 | self.pop_1_view() 156 | ui.animate(self.reader_viewer.reset_scrollbar, 0.5) 157 | 158 | def save_bm(self): 159 | new_bm = self.reader_viewer.get_offset() 160 | 161 | is_duplicate = self.conf_loader.check_bookmark(new_bm) 162 | 163 | if not is_duplicate: 164 | new_bm = {**new_bm, 'type': self.curr_type} 165 | self.conf_loader.refresh_file(self.bm_viewer.add_new_bm(new_bm)) 166 | console.hud_alert(f'已经保存') 167 | else: 168 | console.hud_alert('重复操作或者无效书签') 169 | self.pop_1_view() 170 | 171 | def get_url(self): 172 | new_bm = self.reader_viewer.get_offset() 173 | if new_bm is not None: 174 | return new_bm['url'] 175 | return '' 176 | 177 | def del_bm(self, new_data_src): 178 | self.conf_loader.refresh_file(new_data_src) 179 | 180 | def req_ebody_data(self, init=False): 181 | data = self.var_loader.get_next_bodydata() 182 | self.queue_body.put((*data, init)) 183 | # print('执行') 184 | 185 | def req_ebody_data_bg(self): 186 | if not self.is_req_body: 187 | self.is_req_body = True 188 | self.thread_req_body = threading.Thread(target=self.req_ebody_data) 189 | self.thread_req_body.start() 190 | 191 | def req_eindex_data(self, init=False): 192 | data = self.var_loader.get_next_indexdata() 193 | self.queue_index.put((*data, init)) 194 | # print('执行') 195 | 196 | def req_eindex_data_bg(self): 197 | if not self.is_req_index: 198 | self.is_req_index = True 199 | self.thread_req_index = threading.Thread( 200 | target=self.req_eindex_data) 201 | self.thread_req_index.start() 202 | 203 | def load_ebody_data(self): 204 | if not self.queue_body.empty(): 205 | element = self.queue_body.get() 206 | if element[0] is not None: 207 | self.is_req_body = False 208 | return element 209 | return None 210 | 211 | def load_eindex_data(self): 212 | if not self.queue_index.empty(): 213 | element = self.queue_index.get() 214 | if element[0] is not None: 215 | self.is_req_index = False 216 | return element 217 | return None 218 | 219 | 220 | Controller().view.present('fullscreen') 221 | -------------------------------------------------------------------------------- /menu_viewer/menu.pyui: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nodes" : [ 4 | { 5 | "nodes" : [ 6 | 7 | ], 8 | "frame" : "{{30, 80}, {104, 32}}", 9 | "class" : "Button", 10 | "attributes" : { 11 | "name" : "btn_save_bm", 12 | "frame" : "{{150, 271}, {80, 32}}", 13 | "title" : "保存书签", 14 | "uuid" : "B4101241-7A33-4548-B789-2888528FD6E9", 15 | "class" : "Button", 16 | "font_bold" : true, 17 | "font_size" : 15, 18 | "image_name" : "iob:star_32" 19 | }, 20 | "selected" : false 21 | }, 22 | { 23 | "nodes" : [ 24 | 25 | ], 26 | "frame" : "{{30, 320}, {104, 32}}", 27 | "class" : "Button", 28 | "attributes" : { 29 | "name" : "btn_go_home", 30 | "frame" : "{{150, 271}, {80, 32}}", 31 | "title" : "返回主页", 32 | "uuid" : "D9A923CD-78BE-418B-8ED2-4B32E281363E", 33 | "class" : "Button", 34 | "font_bold" : true, 35 | "font_size" : 15, 36 | "image_name" : "iob:close_24" 37 | }, 38 | "selected" : false 39 | }, 40 | { 41 | "nodes" : [ 42 | 43 | ], 44 | "frame" : "{{30, 160}, {104, 32}}", 45 | "class" : "Button", 46 | "attributes" : { 47 | "image_name" : "iob:navicon_round_24", 48 | "frame" : "{{150, 271}, {80, 32}}", 49 | "title" : "查看目录", 50 | "uuid" : "D9A923CD-78BE-418B-8ED2-4B32E281363E", 51 | "class" : "Button", 52 | "font_bold" : true, 53 | "name" : "btn_show_index", 54 | "font_size" : 15 55 | }, 56 | "selected" : false 57 | }, 58 | { 59 | "nodes" : [ 60 | 61 | ], 62 | "frame" : "{{30, 240}, {104, 32}}", 63 | "class" : "Button", 64 | "attributes" : { 65 | "image_name" : "iob:link_32", 66 | "frame" : "{{150, 271}, {80, 32}}", 67 | "title" : "当前网址", 68 | "uuid" : "D9A923CD-78BE-418B-8ED2-4B32E281363E", 69 | "class" : "Button", 70 | "font_bold" : true, 71 | "name" : "btn_get_url", 72 | "font_size" : 15 73 | }, 74 | "selected" : true 75 | } 76 | ], 77 | "frame" : "{{0, 0}, {380, 573}}", 78 | "class" : "View", 79 | "attributes" : { 80 | "enabled" : true, 81 | "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)", 82 | "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)", 83 | "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)", 84 | "flex" : "" 85 | }, 86 | "selected" : false 87 | } 88 | ] -------------------------------------------------------------------------------- /menu_viewer/viewer.py: -------------------------------------------------------------------------------- 1 | import ui 2 | import dialogs 3 | from requests.utils import requote_uri 4 | 5 | 6 | class MenuViewer: 7 | def __init__(self, controller): 8 | self.controller = controller 9 | view = ui.load_view('menu_viewer/menu') 10 | 11 | btn_save_bm = view['btn_save_bm'] 12 | btn_save_bm.action = self.save_bm 13 | 14 | btn_go_home = view['btn_go_home'] 15 | btn_go_home.action = self.go_home 16 | 17 | btn_show_index = view['btn_show_index'] 18 | btn_show_index.action = self.show_index 19 | 20 | btn_get_url = view['btn_get_url'] 21 | btn_get_url.action = self.get_url 22 | 23 | view.right_btns_desc = 'return' 24 | self.view = view 25 | 26 | def save_bm(self, *args): 27 | self.controller.save_bm() 28 | 29 | def go_home(self, *args): 30 | self.controller.pop_all_view() 31 | self.controller.set_navi_view_name('') 32 | 33 | def show_index(self, *args): 34 | self.controller.pop_1_view() 35 | self.controller.push_index_viewer() 36 | 37 | def get_url(self, *args): 38 | url = self.controller.get_url() 39 | dialogs.share_url(requote_uri(url)) 40 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | toml 3 | soupsieve 4 | diskcache 5 | requests -------------------------------------------------------------------------------- /rule/base_rule.py: -------------------------------------------------------------------------------- 1 | import re 2 | import soupsieve as sv 3 | 4 | 5 | class ReRule: 6 | def __init__(self, string): 7 | self.string = re.compile(string) 8 | 9 | # None / result 10 | def _find(self, text): 11 | return self.string.search(text) 12 | 13 | # 非list,是iter 14 | def _findall(self, text): 15 | return self.string.finditer(text) 16 | 17 | def find_raw(self, text, soup, with_string=False): 18 | result = self._find(text) 19 | if result is None: 20 | return None 21 | return result.group() 22 | 23 | # re这里attr不具有实际字面意思,仅仅是为了统一 24 | def find_attr(self, text, soup, with_string=False): 25 | result = self._find(text) 26 | if result is None: 27 | return None 28 | return result.group(1) 29 | 30 | def findall_raw(self, text, soup, with_string=False): 31 | results = self._findall(text) 32 | return [result.group() for result in results] 33 | 34 | # 同上 35 | def findall_attr(self, text, soup, with_string=False): 36 | results = self._findall(text) 37 | return [result.group(1) for result in results] 38 | 39 | 40 | class TagRule: 41 | # None / result 42 | def _find(self, soup): 43 | return None 44 | 45 | # [] / [result0, …] 46 | def _findall(self, soup): 47 | return [] 48 | 49 | def find_raw(self, text, soup, with_string=False): 50 | result = self._find(soup) 51 | if result is None: 52 | return None 53 | if not with_string: 54 | return result 55 | if self.string_pattern is None: 56 | return result.string 57 | return result[self.string_pattern] 58 | 59 | def find_attr(self, text, soup, with_string=False): 60 | result = self._find(soup) 61 | if result is None: 62 | return None 63 | if not with_string: 64 | return result[self.attr] 65 | if self.string_pattern is None: 66 | return result[self.attr], result.string 67 | return result[self.attr], result[self.string_pattern] 68 | 69 | def findall_raw(self, text, soup, with_string=False): 70 | results = self._findall(soup) 71 | if not with_string: 72 | return results 73 | if self.string_pattern is None: 74 | return [result.string for result in results] 75 | return [result[self.string_pattern] for result in results] 76 | 77 | def findall_attr(self, text, soup, with_string=False): 78 | results = self._findall(soup) 79 | if not with_string: 80 | return [result[self.attr] for result in results] 81 | if self.string_pattern is None: 82 | return [(result[self.attr], result.string) for result in results] 83 | return [(result[self.attr], result[self.string_pattern]) for result in results] 84 | 85 | 86 | class BsRule(TagRule): 87 | def __init__(self, name, attrs, string, attr, string_pattern): 88 | self.name = name 89 | self.attrs = attrs 90 | if string is not None: 91 | self.string = re.compile(string) 92 | else: 93 | self.string = None 94 | self.attr = attr 95 | self.string_pattern = string_pattern 96 | 97 | # None / result 98 | def _find(self, soup): 99 | return soup.find(name=self.name, attrs=self.attrs, string=self.string) 100 | 101 | # [] / [result0, …] 102 | def _findall(self, soup): 103 | return soup.find_all(name=self.name, attrs=self.attrs, string=self.string) 104 | 105 | 106 | class CssSelectorRule(TagRule): 107 | def __init__(self, css_selector, attr, string_pattern): 108 | self.css_selector = sv.compile(css_selector) 109 | self.attr = attr 110 | self.string_pattern = string_pattern 111 | 112 | # None / result 113 | def _find(self, soup): 114 | return self.css_selector.select_one(soup) 115 | 116 | # list 117 | def _findall(self, soup): 118 | return self.css_selector.select(soup) 119 | -------------------------------------------------------------------------------- /rule/rule.py: -------------------------------------------------------------------------------- 1 | from .base_rule import ReRule, CssSelectorRule, BsRule 2 | 3 | 4 | class PageRule: 5 | def get_rule(self, dict_rule, def_key=None, def_name=None): 6 | if 're' in dict_rule: 7 | string = dict_rule['re'] 8 | return ReRule(string) 9 | elif 'css_selector' in dict_rule: 10 | css_selector = dict_rule['css_selector'] 11 | attr = dict_rule.get('key', def_key) 12 | string_pattern = dict_rule.get('string_pattern', None) 13 | return CssSelectorRule(css_selector, attr, string_pattern) 14 | else: 15 | name = dict_rule.get('name', def_name) 16 | attrs = dict_rule.get('attrs', {}) 17 | if 'string' in dict_rule: 18 | string = dict_rule['string'] 19 | else: 20 | string = None 21 | attr = dict_rule.get('key', def_key) 22 | string_pattern = dict_rule.get('string_pattern', None) 23 | return BsRule(name, attrs, string, attr, string_pattern) 24 | 25 | def set_title_rule(self, conf): 26 | dict_rule = conf.get('title', {}) 27 | self.title = self.get_rule(dict_rule, def_name='title') 28 | 29 | def set_next_rule(self, conf): 30 | # 没有表示只有一页,比如某些index页 31 | list_rules = conf.get('next', None) 32 | if list_rules is None: 33 | self.next = None 34 | else: 35 | self.next = [] 36 | for dict_rule in list_rules: 37 | self.next.append(self.get_rule(dict_rule, def_key='href')) 38 | 39 | def set_content_rule(self, conf): 40 | list_rules = conf['content'] 41 | self.content = [] 42 | for dict_rule in list_rules: 43 | self.content.append(self.get_rule(dict_rule, def_key='src')) 44 | 45 | def set_index_rule(self, conf): 46 | # 没有表示无法连通index 47 | dict_rule = conf.get('index', None) 48 | if dict_rule is None: 49 | self.index = None 50 | else: 51 | self.index = self.get_rule(dict_rule, def_key='href') 52 | 53 | 54 | class BodyRule(PageRule): 55 | def set_rule(self, conf): 56 | self.set_title_rule(conf) 57 | self.set_content_rule(conf) 58 | self.set_next_rule(conf) 59 | self.set_index_rule(conf) 60 | 61 | 62 | class IndexRule(PageRule): 63 | def set_rule(self, conf): 64 | # 没有rule的里面一定没有body.index参数,所以无所谓 65 | if conf is None: 66 | return 67 | self.set_title_rule(conf) 68 | self.set_content_rule(conf) 69 | self.set_next_rule(conf) 70 | 71 | 72 | class WebsiteRule: 73 | def __init__(self): 74 | user_agent = ('Mozilla/5.0 (iPhone; CPU iPhone OS 11_2_6 like' 75 | 'Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko)' 76 | 'CriOS/65.0.3325.152 Mobile/15D100 Safari/604.1') 77 | 78 | self.ori_headers = {'User-Agent': user_agent} 79 | self.body_rule = BodyRule() 80 | self.index_rule = IndexRule() 81 | 82 | def set_rule(self, conf): 83 | self.url = conf['url'] 84 | self.encoding = conf.get('encoding', None) 85 | self.headers = {**self.ori_headers, **conf.get('headers', {})} 86 | self.body_rule.set_rule(conf['body']) 87 | self.index_rule.set_rule(conf.get('index', None)) 88 | -------------------------------------------------------------------------------- /web.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from diskcache import Cache 3 | 4 | 5 | session = requests.Session() 6 | cache = Cache('cache_rsp') 7 | cache.expire() 8 | 9 | 10 | def get(url, headers=None, allow_cache=True): 11 | url = url.strip() 12 | if url in cache: 13 | # print('cached', url) 14 | return cache.get(url) 15 | while True: 16 | try: 17 | with session.get(url, headers=headers, timeout=4) as rsp: 18 | if rsp.status_code == 200 or rsp.status_code == 500: 19 | if allow_cache or True: 20 | cache.set(url, rsp) 21 | return rsp 22 | except Exception as e: 23 | print(e) 24 | pass 25 | 26 | -------------------------------------------------------------------------------- /zh_st.py: -------------------------------------------------------------------------------- 1 | # # coding=UTF-8 2 | zh_s = '皑蔼碍爱翱袄奥坝罢摆败颁办绊帮绑镑谤剥饱宝报鲍辈贝钡狈备惫绷笔毕毙闭边编贬变辩辫鳖瘪濒滨宾摈饼拨钵铂驳卜补参蚕残惭惨灿苍舱仓沧厕侧册测层诧搀掺蝉馋谗缠铲产阐颤场尝长偿肠厂畅钞车彻尘陈衬撑称惩诚骋痴迟驰耻齿炽冲虫宠畴踌筹绸丑橱厨锄雏础储触处传疮闯创锤纯绰辞词赐聪葱囱从丛凑窜错达带贷担单郸掸胆惮诞弹当挡党荡档捣岛祷导盗灯邓敌涤递缔点垫电淀钓调迭谍叠钉顶锭订东动栋冻斗犊独读赌镀锻断缎兑队对吨顿钝夺鹅额讹恶饿儿尔饵贰发罚阀珐矾钒烦范贩饭访纺飞废费纷坟奋愤粪丰枫锋风疯冯缝讽凤肤辐抚辅赋复负讣妇缚该钙盖干赶秆赣冈刚钢纲岗皋镐搁鸽阁铬个给龚宫巩贡钩沟构购够蛊顾剐关观馆惯贯广规硅归龟闺轨诡柜贵刽辊滚锅国过骇韩汉阂鹤贺横轰鸿红后壶护沪户哗华画划话怀坏欢环还缓换唤痪焕涣黄谎挥辉毁贿秽会烩汇讳诲绘荤浑伙获货祸击机积饥讥鸡绩缉极辑级挤几蓟剂济计记际继纪夹荚颊贾钾价驾歼监坚笺间艰缄茧检碱硷拣捡简俭减荐槛鉴践贱见键舰剑饯渐溅涧浆蒋桨奖讲酱胶浇骄娇搅铰矫侥脚饺缴绞轿较秸阶节茎惊经颈静镜径痉竞净纠厩旧驹举据锯惧剧鹃绢杰洁结诫届紧锦仅谨进晋烬尽劲荆觉决诀绝钧军骏开凯颗壳课垦恳抠库裤夸块侩宽矿旷况亏岿窥馈溃扩阔蜡腊莱来赖蓝栏拦篮阑兰澜谰揽览懒缆烂滥捞劳涝乐镭垒类泪篱离里鲤礼丽厉励砾历沥隶俩联莲连镰怜涟帘敛脸链恋炼练粮凉两辆谅疗辽镣猎临邻鳞凛赁龄铃凌灵岭领馏刘龙聋咙笼垄拢陇楼娄搂篓芦卢颅庐炉掳卤虏鲁赂禄录陆驴吕铝侣屡缕虑滤绿峦挛孪滦乱抡轮伦仑沦纶论萝罗逻锣箩骡骆络妈玛码蚂马骂吗买麦卖迈脉瞒馒蛮满谩猫锚铆贸么霉没镁门闷们锰梦谜弥觅绵缅庙灭悯闽鸣铭谬谋亩钠纳难挠脑恼闹馁腻撵捻酿鸟聂啮镊镍柠狞宁拧泞钮纽脓浓农疟诺欧鸥殴呕沤盘庞国爱赔喷鹏骗飘频贫苹凭评泼颇扑铺朴谱脐齐骑岂启气弃讫牵扦钎铅迁签谦钱钳潜浅谴堑枪呛墙蔷强抢锹桥乔侨翘窍窃钦亲轻氢倾顷请庆琼穷趋区躯驱龋颧权劝却鹊让饶扰绕热韧认纫荣绒软锐闰润洒萨鳃赛伞丧骚扫涩杀纱筛晒闪陕赡缮伤赏烧绍赊摄慑设绅审婶肾渗声绳胜圣师狮湿诗尸时蚀实识驶势释饰视试寿兽枢输书赎属术树竖数帅双谁税顺说硕烁丝饲耸怂颂讼诵擞苏诉肃虽绥岁孙损笋缩琐锁獭挞抬摊贪瘫滩坛谭谈叹汤烫涛绦腾誊锑题体屉条贴铁厅听烃铜统头图涂团颓蜕脱鸵驮驼椭洼袜弯湾顽万网韦违围为潍维苇伟伪纬谓卫温闻纹稳问瓮挝蜗涡窝呜钨乌诬无芜吴坞雾务误锡牺袭习铣戏细虾辖峡侠狭厦锨鲜纤咸贤衔闲显险现献县馅羡宪线厢镶乡详响项萧销晓啸蝎协挟携胁谐写泻谢锌衅兴汹锈绣虚嘘须许绪续轩悬选癣绚学勋询寻驯训讯逊压鸦鸭哑亚讶阉烟盐严颜阎艳厌砚彦谚验鸯杨扬疡阳痒养样瑶摇尧遥窑谣药爷页业叶医铱颐遗仪彝蚁艺亿忆义诣议谊译异绎荫阴银饮樱婴鹰应缨莹萤营荧蝇颖哟拥佣痈踊咏涌优忧邮铀犹游诱舆鱼渔娱与屿语吁御狱誉预驭鸳渊辕园员圆缘远愿约跃钥岳粤悦阅云郧匀陨运蕴酝晕韵杂灾载攒暂赞赃脏凿枣灶责择则泽贼赠扎札轧铡闸诈斋债毡盏斩辗崭栈战绽张涨帐账胀赵蛰辙锗这贞针侦诊镇阵挣睁狰帧郑证织职执纸挚掷帜质钟终种肿众诌轴皱昼骤猪诸诛烛瞩嘱贮铸筑驻专砖转赚桩庄装妆壮状锥赘坠缀谆浊兹资渍踪综总纵邹诅组钻致钟么为只凶准启板里雳余链泄' 3 | zh_t = '皚藹礙愛翺襖奧壩罷擺敗頒辦絆幫綁鎊謗剝飽寶報鮑輩貝鋇狽備憊繃筆畢斃閉邊編貶變辯辮鼈癟瀕濱賓擯餅撥缽鉑駁蔔補參蠶殘慚慘燦蒼艙倉滄廁側冊測層詫攙摻蟬饞讒纏鏟産闡顫場嘗長償腸廠暢鈔車徹塵陳襯撐稱懲誠騁癡遲馳恥齒熾沖蟲寵疇躊籌綢醜櫥廚鋤雛礎儲觸處傳瘡闖創錘純綽辭詞賜聰蔥囪從叢湊竄錯達帶貸擔單鄲撣膽憚誕彈當擋黨蕩檔搗島禱導盜燈鄧敵滌遞締點墊電澱釣調叠諜疊釘頂錠訂東動棟凍鬥犢獨讀賭鍍鍛斷緞兌隊對噸頓鈍奪鵝額訛惡餓兒爾餌貳發罰閥琺礬釩煩範販飯訪紡飛廢費紛墳奮憤糞豐楓鋒風瘋馮縫諷鳳膚輻撫輔賦複負訃婦縛該鈣蓋幹趕稈贛岡剛鋼綱崗臯鎬擱鴿閣鉻個給龔宮鞏貢鈎溝構購夠蠱顧剮關觀館慣貫廣規矽歸龜閨軌詭櫃貴劊輥滾鍋國過駭韓漢閡鶴賀橫轟鴻紅後壺護滬戶嘩華畫劃話懷壞歡環還緩換喚瘓煥渙黃謊揮輝毀賄穢會燴彙諱誨繪葷渾夥獲貨禍擊機積饑譏雞績緝極輯級擠幾薊劑濟計記際繼紀夾莢頰賈鉀價駕殲監堅箋間艱緘繭檢堿鹼揀撿簡儉減薦檻鑒踐賤見鍵艦劍餞漸濺澗漿蔣槳獎講醬膠澆驕嬌攪鉸矯僥腳餃繳絞轎較稭階節莖驚經頸靜鏡徑痙競淨糾廄舊駒舉據鋸懼劇鵑絹傑潔結誡屆緊錦僅謹進晉燼盡勁荊覺決訣絕鈞軍駿開凱顆殼課墾懇摳庫褲誇塊儈寬礦曠況虧巋窺饋潰擴闊蠟臘萊來賴藍欄攔籃闌蘭瀾讕攬覽懶纜爛濫撈勞澇樂鐳壘類淚籬離裏鯉禮麗厲勵礫曆瀝隸倆聯蓮連鐮憐漣簾斂臉鏈戀煉練糧涼兩輛諒療遼鐐獵臨鄰鱗凜賃齡鈴淩靈嶺領餾劉龍聾嚨籠壟攏隴樓婁摟簍蘆盧顱廬爐擄鹵虜魯賂祿錄陸驢呂鋁侶屢縷慮濾綠巒攣孿灤亂掄輪倫侖淪綸論蘿羅邏鑼籮騾駱絡媽瑪碼螞馬罵嗎買麥賣邁脈瞞饅蠻滿謾貓錨鉚貿麽黴沒鎂門悶們錳夢謎彌覓綿緬廟滅憫閩鳴銘謬謀畝鈉納難撓腦惱鬧餒膩攆撚釀鳥聶齧鑷鎳檸獰甯擰濘鈕紐膿濃農瘧諾歐鷗毆嘔漚盤龐國愛賠噴鵬騙飄頻貧蘋憑評潑頗撲鋪樸譜臍齊騎豈啓氣棄訖牽扡釺鉛遷簽謙錢鉗潛淺譴塹槍嗆牆薔強搶鍬橋喬僑翹竅竊欽親輕氫傾頃請慶瓊窮趨區軀驅齲顴權勸卻鵲讓饒擾繞熱韌認紉榮絨軟銳閏潤灑薩鰓賽傘喪騷掃澀殺紗篩曬閃陝贍繕傷賞燒紹賒攝懾設紳審嬸腎滲聲繩勝聖師獅濕詩屍時蝕實識駛勢釋飾視試壽獸樞輸書贖屬術樹豎數帥雙誰稅順說碩爍絲飼聳慫頌訟誦擻蘇訴肅雖綏歲孫損筍縮瑣鎖獺撻擡攤貪癱灘壇譚談歎湯燙濤縧騰謄銻題體屜條貼鐵廳聽烴銅統頭圖塗團頹蛻脫鴕馱駝橢窪襪彎灣頑萬網韋違圍爲濰維葦偉僞緯謂衛溫聞紋穩問甕撾蝸渦窩嗚鎢烏誣無蕪吳塢霧務誤錫犧襲習銑戲細蝦轄峽俠狹廈鍁鮮纖鹹賢銜閑顯險現獻縣餡羨憲線廂鑲鄉詳響項蕭銷曉嘯蠍協挾攜脅諧寫瀉謝鋅釁興洶鏽繡虛噓須許緒續軒懸選癬絢學勳詢尋馴訓訊遜壓鴉鴨啞亞訝閹煙鹽嚴顔閻豔厭硯彥諺驗鴦楊揚瘍陽癢養樣瑤搖堯遙窯謠藥爺頁業葉醫銥頤遺儀彜蟻藝億憶義詣議誼譯異繹蔭陰銀飲櫻嬰鷹應纓瑩螢營熒蠅穎喲擁傭癰踴詠湧優憂郵鈾猶遊誘輿魚漁娛與嶼語籲禦獄譽預馭鴛淵轅園員圓緣遠願約躍鑰嶽粵悅閱雲鄖勻隕運蘊醞暈韻雜災載攢暫贊贓髒鑿棗竈責擇則澤賊贈紮劄軋鍘閘詐齋債氈盞斬輾嶄棧戰綻張漲帳賬脹趙蟄轍鍺這貞針偵診鎮陣掙睜猙幀鄭證織職執紙摯擲幟質鍾終種腫衆謅軸皺晝驟豬諸誅燭矚囑貯鑄築駐專磚轉賺樁莊裝妝壯狀錐贅墜綴諄濁茲資漬蹤綜總縱鄒詛組鑽緻鐘麼為隻兇準啟闆裡靂餘鍊洩' 4 | 5 | 6 | def t2s(string_org): 7 | string_new = [] 8 | for i in string_org: 9 | try: 10 | index = zh_t.index(i) 11 | string_new.append(zh_s[index]) 12 | except: 13 | string_new.append(i) 14 | return ''.join(string_new) 15 | 16 | -------------------------------------------------------------------------------- /zsbook_loader/zsbody_loader.py: -------------------------------------------------------------------------------- 1 | from urllib import parse 2 | import web 3 | 4 | 5 | class ZSBodyLoader: 6 | def __init__(self, root): 7 | self.root = root 8 | self.url = None 9 | self.cur_offset = None 10 | self.headers = { 11 | "User-Agent": "YouShaQi/4.1.0 (iPhone; iOS 12.1.2; Scale/2.00)", 12 | "X-User-Agent": "YouShaQi/4.1.0 (iPhone; iOS 12.1.2; Scale/2.00)" 13 | } 14 | 15 | def set_url(self, offset): 16 | self.cur_offset = offset 17 | 18 | # 存取bm用的是index url(格式: src_id!offset),因为api不提供next功能 19 | # 追书api不分页 20 | def get_next_data(self): 21 | if self.cur_offset is None: 22 | self.cur_offset = 0 23 | if self.cur_offset >= 0: 24 | while True: 25 | # 这里next与ebook里面有区别,set_url其实什么也没做 26 | if not self.get_next_url(): 27 | return None, None, None 28 | self.contents = self.get_content() 29 | if self.contents: 30 | break 31 | self.cur_offset += 1 32 | 33 | if not self.contents: 34 | return None, None, None 35 | words = self.contents 36 | title = self.title 37 | self.cur_offset += 1 38 | return words, title, f'{self.root.get_srcid()}#{self.cur_offset-1}' 39 | 40 | def get_content(self): 41 | 42 | json_rsp = web.get(self.url, headers=self.headers).json() 43 | # print(json_rsp) 44 | if not json_rsp['ok']: 45 | return [] 46 | chapter = json_rsp['chapter'] 47 | # self.title = chapter['title'] 48 | words = [] 49 | for para in chapter['body'].split('\n'): 50 | para = para.strip() 51 | if para: 52 | para = '  ' + para 53 | words.append(para) 54 | return words 55 | 56 | def get_next_url(self): 57 | result = self.root.get_chapter_info(self.cur_offset) 58 | if result is None: 59 | return False 60 | url, self.title = result 61 | encode_url = parse.quote_plus(url) 62 | self.url = f'http://chapter2.zhuishushenqi.com/chapter/{encode_url}' 63 | return True 64 | 65 | -------------------------------------------------------------------------------- /zsbook_loader/zsbook_loader.py: -------------------------------------------------------------------------------- 1 | import web 2 | from .zsindex_loader import ZSIndexLoader 3 | from .zsbody_loader import ZSBodyLoader 4 | 5 | 6 | class ZSBookLoader: 7 | def __init__(self): 8 | self.body_loader = ZSBodyLoader(self) 9 | self.index_loader = ZSIndexLoader(self) 10 | 11 | def set_url(self, orig): 12 | # src_id#offset / src_id 13 | if '#' in orig: 14 | src_id, offset = orig.split('#') 15 | offset = int(offset) 16 | else: 17 | src_id = orig 18 | offset = None 19 | self.index_loader.set_url(src_id) 20 | self.body_loader.set_url(offset) 21 | 22 | def search_books(self, keywords): 23 | url = f'http://api.zhuishushenqi.com/book/fuzzy-search?query={keywords}&start=0&limit=40' 24 | json_rsp = web.get(url).json() 25 | books = json_rsp['books'] 26 | list_books = [(book['_id'], book['author'], book['title']) for book in books] 27 | return list_books 28 | 29 | def fetch_srcs(self, book_id): 30 | url = f'http://api.zhuishushenqi.com/toc?view=summary&book={book_id}' 31 | json_rsp = web.get(url).json() 32 | srcs = [] 33 | for src in json_rsp: 34 | if src['source'] != 'zhuishuvip': 35 | srcs.append((src['_id'], src['name'], src['lastChapter'], src['updated'])) 36 | return srcs 37 | 38 | def get_chapter_info(self, offset): 39 | return self.index_loader.get_chapter_info(offset) 40 | 41 | def get_srcid(self): 42 | return self.index_loader.get_srcid() 43 | 44 | def get_next_bodydata(self): 45 | return self.body_loader.get_next_data() 46 | 47 | def get_next_indexdata(self): 48 | return self.index_loader.get_next_data() 49 | -------------------------------------------------------------------------------- /zsbook_loader/zsindex_loader.py: -------------------------------------------------------------------------------- 1 | import web 2 | 3 | 4 | class ZSIndexLoader: 5 | def __init__(self, root): 6 | self.root = root 7 | self.url = None 8 | 9 | def set_url(self, src_id): 10 | self.src_id = src_id 11 | self.url = f'http://api.zhuishushenqi.com/toc/{src_id}?view=chapters' 12 | self.cur_offset = None 13 | self.all_contents = [] 14 | 15 | def get_next_data(self, is_for_body_next=False): 16 | if self.cur_offset is None: 17 | self.cur_offset = 0 18 | self.all_contents += self.get_content() 19 | if self.cur_offset >= len(self.all_contents) and self.cur_offset: 20 | if not self.get_next_url(): 21 | return None, None 22 | self.all_contents += self.get_content() 23 | # index [(index_url, index_name)] 24 | indexes = [] 25 | for i, content in enumerate(self.all_contents[self.cur_offset:]): 26 | indexes.append((f'{self.src_id}#{i+self.cur_offset}', content[1])) 27 | title = '' 28 | # print(indexes, len(indexes)) 29 | if not is_for_body_next: 30 | self.cur_offset = len(self.all_contents) 31 | return indexes, title 32 | 33 | def get_content(self): 34 | json_rsp = web.get(self.url).json() 35 | list_chapters = json_rsp['chapters'] 36 | chapters = [(chapter['link'], chapter['title']) for chapter in list_chapters] 37 | # 反序 38 | # chapters_list = chapterslist[::-1] 39 | return chapters 40 | 41 | def get_chapter_info(self, offset): 42 | if offset >= len(self.all_contents): 43 | self.get_next_data(is_for_body_next=True) 44 | if offset >= len(self.all_contents): 45 | return None 46 | return self.all_contents[offset] 47 | 48 | # 追书index api不分页 49 | def get_next_url(self): 50 | return False 51 | 52 | def get_srcid(self): 53 | return self.src_id 54 | -------------------------------------------------------------------------------- /zsbook_search_viewer/src_index.pyui: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nodes" : [ 4 | { 5 | "nodes" : [ 6 | 7 | ], 8 | "frame" : "{{0, 6}, {297, 388}}", 9 | "class" : "TableView", 10 | "attributes" : { 11 | "flex" : "WH", 12 | "data_source_items" : "Row 1\nRow 2\nRow 3", 13 | "name" : "tableview", 14 | "frame" : "{{41, 100}, {200, 200}}", 15 | "data_source_number_of_lines" : 1, 16 | "class" : "TableView", 17 | "background_color" : "RGBA(1.0, 1.0, 1.0, 1.0)", 18 | "data_source_delete_enabled" : true, 19 | "data_source_font_size" : 18, 20 | "row_height" : 44, 21 | "uuid" : "889589A9-2E4B-487E-AE4F-E5B4A46B22F1" 22 | }, 23 | "selected" : true 24 | } 25 | ], 26 | "frame" : "{{0, 0}, {282, 400}}", 27 | "class" : "View", 28 | "attributes" : { 29 | "enabled" : true, 30 | "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)", 31 | "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)", 32 | "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)", 33 | "flex" : "" 34 | }, 35 | "selected" : false 36 | } 37 | ] -------------------------------------------------------------------------------- /zsbook_search_viewer/viewer.py: -------------------------------------------------------------------------------- 1 | import console 2 | import ui 3 | 4 | 5 | class ZSBookSearchViewer: 6 | def __init__(self, controller): 7 | self.controller = controller 8 | view = ui.load_view('zsbook_search_viewer/src_index') 9 | tb = view['tableview'] 10 | 11 | tb.data_source.items = [] 12 | self.tb = tb 13 | view.right_btns_desc = 'return' 14 | self.view = view 15 | tb.delegate = self 16 | 17 | def tableview_did_select(self, bm_view, section, row): 18 | src_id = self.srcs[row][0] 19 | print(src_id) 20 | self.controller.load_zsbook(src_id) 21 | return src_id 22 | 23 | # 返回bookid 24 | def search_books(self, zs_loader, keywords): 25 | list_books = zs_loader.search_books(keywords) 26 | list_msg = [] 27 | for i, values in enumerate(list_books): 28 | _, author, title = values 29 | list_msg.append(f'{i}.{author}:《{title}》') 30 | str_msg = '\n'.join(reversed(list_msg)) 31 | try: 32 | i = int(console.input_alert('请输入index的号码', str_msg, hide_cancel_button=True)) 33 | except KeyboardInterrupt: 34 | return 35 | except ValueError: 36 | return 37 | return list_books[i][0] 38 | 39 | def tableview_accessory_button_tapped(self, tb_view, _, row): 40 | # section datasrc为single的 41 | src = self.srcs[row] 42 | msg = f'最新章节{src[2]}({src[3]})' 43 | console.hud_alert(msg) 44 | 45 | def fetch_srcs(self, zs_loader, book_id): 46 | srcs = zs_loader.fetch_srcs(book_id) 47 | items = [{'title': src[1], 'accessory_type': 'detail_button'} for src in srcs] 48 | self.srcs = srcs 49 | self.tb.data_source.items = items 50 | return True 51 | 52 | 53 | 54 | --------------------------------------------------------------------------------