16 |
17 |
18 | You are currently using storage for:
19 |
SYNC SPACE
20 |
LOCAL SPACE
21 | v1.1.0 Change note:
22 | Since v1.1.0 book state saving is done in local database, so no Google sync anymore.
23 | Because sync storage available for extension to save is only up to 100Kb, which is super small.
24 | Instead now you can import/export data to other device or for backup.
25 | Export
26 |
27 | Import
28 |
29 |
30 |
31 |
32 |
33 | Account control
34 |
35 | (Must be logged in, would take some time if you have like thousands)
36 |
37 | (Fill book info, name on book number that not yet has its name)
38 |
39 |
40 |
41 |
Manual hint
42 |
43 |
Google Takeout browsing history
44 | Inspect your own entire browsing history and populate "READ" book id by searching for
45 | URL pattern like 'nhentai.net/g/NNNNNNN/'
46 | Select BrowserHistory.json and submit it here
47 | It is processed using JavaScript locally in the extension, it is not upload to remote server anywhere, you can check the source code. This is a matter of trust, you and me :)
48 |
52 |
Using URL file per line
53 | Submit a text file where each line containing URL, the line could include other stuff but extension will search only the URL pattern in each line.
54 | You could export this from history tool or something else. History Trends Unlimited is good tool to export such data from your history.
55 |
59 |
60 |
61 |
62 |
STATUS TEXT
63 |
--
64 | Filter list:
65 |
71 |
72 |
If the book is displayed without name (only id) that's mean extension is not yet seen/stored meta data for that book. Go into book index page to make an update.
73 |
book display (If book list does not show up or refresh, that's mean background service broke, reload the extension off-on again)
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # WAPPEN's NHentai Tracker Extension
3 | A Chrome extension to help you keep track of read book number.
4 | Display badge over all book covers. Never got lost in search result again!
5 |
6 | 
7 | 
8 | 
9 |
10 | ### Features
11 | - Mark book as READ or IGNORED and display them over search result to quickly skim for new book to read.
12 | - Also auto mark as READ when you go into any of its page to read.
13 | - Mark as read later for reading list.
14 | - See [changelog.md](changelog.md) for change list.
15 |
16 | ### Chrome
17 | Chrome Store: **Sorry none yet!** It needs to pass some ~~fapping~~ err I mean QA testing first.
18 |
19 | ### Firefox
20 | I'm never intended to port to FF yet. Sooooorry! (CHECKMATE FF users!)
21 |
22 | ------
23 |
24 | ### Installing
25 | - As this is not published to store, the only way to install is via developer mode.
26 | - Download this entire repo, top right of this web page 'Code' button -> download zip (.zip)
27 | - Unzip it to some folder, but dont unzip your pants yet, still some more steps to go.
28 | - Go into Chrome extension tab, enable developer mode (upper right).
29 | - Select load unpacked extension (upper left).
30 | - Point the to folder where you unzip it, where "manifest.json" is. (manifest.json wont show on dialog, that's ok, just select the folder)
31 | - Extension is installed as "NHentai Tracker".
32 | - If you want more help regarding above steps, search for something like **Chrome install unpacked extension**.
33 | - Unzip your pants, go to NHentai.
34 | - What you can do next:
35 | - Open extension option page by clicking extension badge. Import your favorite list. (Must be logged in)
36 | - (Advanced) Export past browsing history and import to extension to scan for past visit. Read more below.
37 |
38 | ### Pulling past visit from history
39 | Extension can do it in 2 ways, none of them are automated, so you need some work.
40 | - Google takeout (not much detailed history though, I pulled only a few out of it), to start, google something like "google export browsing history".
41 | - Or you export current Chrome history (note that [chrome will only hold up to 3 months of history locally](https://superuser.com/questions/364470/), which is stupid if you ask me)
42 | - I used this extension, [History Trends Unlimited](https://chrome.google.com/webstore/detail/history-trends-unlimited/pnmchffiealhkdloeffcdnbgdnedheme) which crunch history and further saving it longer than 3 months limit.
43 | Anyway, if you just installed this Unlimited extension, you still have no way to bring back your history beyond that 3 months anyway.
44 | - If you have history exported from **History Trends Unlimited**, scan it in NHTracker's setting page in the second file submit section.
45 |
46 | ### Upgrading
47 | - Upgrading from previous version, just redownload this again and unzip to same location.
48 | - In your chrome extension page, click the circle arrow (reload extension) and you are done. Notice the version number should changed.
49 | - If you have any NH tab already open you need to refresh them once for the new version to take effect.
50 |
51 | ------
52 |
53 | ### Q&A
54 | **Why there are 'READ' and 'IGNORED' book state? What's the difference?**
55 | - The 'IGNORED' state is used to mark book as **"Checked that out but passed"** rather than **"I read that! (to the end!)"**.
56 | - Example usage is using IGNORE tag on the book you totally not interested in.
57 | - Or another usage is to mark them on another sibling books. (incompleted, reuploaded, low quality, bad translation, etc)
58 | - Ignored book will not show in read book listing.
59 | - Ignored bool will colored differently in search result.
60 |
61 | **I want XX YY ZZ feature! Make it!! plzzzz**
62 | - If it sounds good, pitch it to me via issue page.
63 | - Else you have to learn HTML+JavaScript and do it yourself. :P
64 |
65 | **What is it inspired from?**
66 | - I noticed that browser always mark visited link, like from blue to purple for an eon since internet started, but now in 2022 I have trouble looking at doujin search result, how comes?
67 | - So I searched chrome extension store and github for something like this, hoping that some gentleman would already made it.
68 | - But I found stuff like extension that let you highlight NH number and jump to the site (to save some steps and typing), or the other that when you highlight NH number it will popup and preview the doujin (in fear of stepping into degenerated doujin).
69 | - I was like WTF this is the best you people can think of??? What The Heck??? Why do you people so serious about those little number?? Just copy paste it and go to the site to check it out like a man!!
70 | - Ok I'm done ranting, that was how this extension was born.
71 |
72 | **Problems, HALP!**
73 | - Report it in Github issue. Thank you!
74 | - Or fix it yourself if you are a wizkid.
75 |
76 | ### Credit
77 | - To great myself.
78 | - Took code skeleton from https://github.com/Xwilarg/NHentaiAnalytics thanks!
79 | - Stackoverflow for making me able to go through JavaScript hell after not writing it for 5 years. At least it is better than Java shit.
80 |
--------------------------------------------------------------------------------
/js/base64-string.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2013 Pieroxy
2 | // This work is free. You can redistribute it and/or modify it
3 | // under the terms of the WTFPL, Version 2
4 | // For more information see LICENSE.txt or http://www.wtfpl.net/
5 | //
6 | // This lib is part of the lz-string project.
7 | // For more information, the home page:
8 | // http://pieroxy.net/blog/pages/lz-string/index.html
9 | //
10 | // Base64 compression / decompression for already compressed content (gif, png, jpg, mp3, ...)
11 | // version 1.4.1
12 | var Base64String = {
13 |
14 | compressToUTF16 : function (input) {
15 | var output = [],
16 | i,c,
17 | current,
18 | status = 0;
19 |
20 | input = this.compress(input);
21 |
22 | for (i=0 ; i> 1)+32));
27 | current = (c & 1) << 14;
28 | break;
29 | case 1:
30 | output.push(String.fromCharCode((current + (c >> 2))+32));
31 | current = (c & 3) << 13;
32 | break;
33 | case 2:
34 | output.push(String.fromCharCode((current + (c >> 3))+32));
35 | current = (c & 7) << 12;
36 | break;
37 | case 3:
38 | output.push(String.fromCharCode((current + (c >> 4))+32));
39 | current = (c & 15) << 11;
40 | break;
41 | case 4:
42 | output.push(String.fromCharCode((current + (c >> 5))+32));
43 | current = (c & 31) << 10;
44 | break;
45 | case 5:
46 | output.push(String.fromCharCode((current + (c >> 6))+32));
47 | current = (c & 63) << 9;
48 | break;
49 | case 6:
50 | output.push(String.fromCharCode((current + (c >> 7))+32));
51 | current = (c & 127) << 8;
52 | break;
53 | case 7:
54 | output.push(String.fromCharCode((current + (c >> 8))+32));
55 | current = (c & 255) << 7;
56 | break;
57 | case 8:
58 | output.push(String.fromCharCode((current + (c >> 9))+32));
59 | current = (c & 511) << 6;
60 | break;
61 | case 9:
62 | output.push(String.fromCharCode((current + (c >> 10))+32));
63 | current = (c & 1023) << 5;
64 | break;
65 | case 10:
66 | output.push(String.fromCharCode((current + (c >> 11))+32));
67 | current = (c & 2047) << 4;
68 | break;
69 | case 11:
70 | output.push(String.fromCharCode((current + (c >> 12))+32));
71 | current = (c & 4095) << 3;
72 | break;
73 | case 12:
74 | output.push(String.fromCharCode((current + (c >> 13))+32));
75 | current = (c & 8191) << 2;
76 | break;
77 | case 13:
78 | output.push(String.fromCharCode((current + (c >> 14))+32));
79 | current = (c & 16383) << 1;
80 | break;
81 | case 14:
82 | output.push(String.fromCharCode((current + (c >> 15))+32, (c & 32767)+32));
83 | status = 0;
84 | break;
85 | }
86 | }
87 | output.push(String.fromCharCode(current + 32));
88 | return output.join('');
89 | },
90 |
91 |
92 | decompressFromUTF16 : function (input) {
93 | var output = [],
94 | current,c,
95 | status=0,
96 | i = 0;
97 |
98 | while (i < input.length) {
99 | c = input.charCodeAt(i) - 32;
100 |
101 | switch (status++) {
102 | case 0:
103 | current = c << 1;
104 | break;
105 | case 1:
106 | output.push(String.fromCharCode(current | (c >> 14)));
107 | current = (c&16383) << 2;
108 | break;
109 | case 2:
110 | output.push(String.fromCharCode(current | (c >> 13)));
111 | current = (c&8191) << 3;
112 | break;
113 | case 3:
114 | output.push(String.fromCharCode(current | (c >> 12)));
115 | current = (c&4095) << 4;
116 | break;
117 | case 4:
118 | output.push(String.fromCharCode(current | (c >> 11)));
119 | current = (c&2047) << 5;
120 | break;
121 | case 5:
122 | output.push(String.fromCharCode(current | (c >> 10)));
123 | current = (c&1023) << 6;
124 | break;
125 | case 6:
126 | output.push(String.fromCharCode(current | (c >> 9)));
127 | current = (c&511) << 7;
128 | break;
129 | case 7:
130 | output.push(String.fromCharCode(current | (c >> 8)));
131 | current = (c&255) << 8;
132 | break;
133 | case 8:
134 | output.push(String.fromCharCode(current | (c >> 7)));
135 | current = (c&127) << 9;
136 | break;
137 | case 9:
138 | output.push(String.fromCharCode(current | (c >> 6)));
139 | current = (c&63) << 10;
140 | break;
141 | case 10:
142 | output.push(String.fromCharCode(current | (c >> 5)));
143 | current = (c&31) << 11;
144 | break;
145 | case 11:
146 | output.push(String.fromCharCode(current | (c >> 4)));
147 | current = (c&15) << 12;
148 | break;
149 | case 12:
150 | output.push(String.fromCharCode(current | (c >> 3)));
151 | current = (c&7) << 13;
152 | break;
153 | case 13:
154 | output.push(String.fromCharCode(current | (c >> 2)));
155 | current = (c&3) << 14;
156 | break;
157 | case 14:
158 | output.push(String.fromCharCode(current | (c >> 1)));
159 | current = (c&1) << 15;
160 | break;
161 | case 15:
162 | output.push(String.fromCharCode(current | c));
163 | status=0;
164 | break;
165 | }
166 |
167 |
168 | i++;
169 | }
170 |
171 | return this.decompress(output.join(''));
172 | //return output;
173 |
174 | },
175 |
176 |
177 | // private property
178 | _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
179 |
180 | decompress : function (input) {
181 | var output = [];
182 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
183 | var i = 1;
184 | var odd = input.charCodeAt(0) >> 8;
185 |
186 | while (i < input.length*2 && (i < input.length*2-1 || odd==0)) {
187 |
188 | if (i%2==0) {
189 | chr1 = input.charCodeAt(i/2) >> 8;
190 | chr2 = input.charCodeAt(i/2) & 255;
191 | if (i/2+1 < input.length)
192 | chr3 = input.charCodeAt(i/2+1) >> 8;
193 | else
194 | chr3 = NaN;
195 | } else {
196 | chr1 = input.charCodeAt((i-1)/2) & 255;
197 | if ((i+1)/2 < input.length) {
198 | chr2 = input.charCodeAt((i+1)/2) >> 8;
199 | chr3 = input.charCodeAt((i+1)/2) & 255;
200 | } else
201 | chr2=chr3=NaN;
202 | }
203 | i+=3;
204 |
205 | enc1 = chr1 >> 2;
206 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
207 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
208 | enc4 = chr3 & 63;
209 |
210 | if (isNaN(chr2) || (i==input.length*2+1 && odd)) {
211 | enc3 = enc4 = 64;
212 | } else if (isNaN(chr3) || (i==input.length*2 && odd)) {
213 | enc4 = 64;
214 | }
215 |
216 | output.push(this._keyStr.charAt(enc1));
217 | output.push(this._keyStr.charAt(enc2));
218 | output.push(this._keyStr.charAt(enc3));
219 | output.push(this._keyStr.charAt(enc4));
220 | }
221 |
222 | return output.join('');
223 | },
224 |
225 | compress : function (input) {
226 | var output = [],
227 | ol = 1,
228 | output_,
229 | chr1, chr2, chr3,
230 | enc1, enc2, enc3, enc4,
231 | i = 0, flush=false;
232 |
233 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
234 |
235 | while (i < input.length) {
236 |
237 | enc1 = this._keyStr.indexOf(input.charAt(i++));
238 | enc2 = this._keyStr.indexOf(input.charAt(i++));
239 | enc3 = this._keyStr.indexOf(input.charAt(i++));
240 | enc4 = this._keyStr.indexOf(input.charAt(i++));
241 |
242 | chr1 = (enc1 << 2) | (enc2 >> 4);
243 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
244 | chr3 = ((enc3 & 3) << 6) | enc4;
245 |
246 | if (ol%2==0) {
247 | output_ = chr1 << 8;
248 | flush = true;
249 |
250 | if (enc3 != 64) {
251 | output.push(String.fromCharCode(output_ | chr2));
252 | flush = false;
253 | }
254 | if (enc4 != 64) {
255 | output_ = chr3 << 8;
256 | flush = true;
257 | }
258 | } else {
259 | output.push(String.fromCharCode(output_ | chr1));
260 | flush = false;
261 |
262 | if (enc3 != 64) {
263 | output_ = chr2 << 8;
264 | flush = true;
265 | }
266 | if (enc4 != 64) {
267 | output.push(String.fromCharCode(output_ | chr3));
268 | flush = false;
269 | }
270 | }
271 | ol+=3;
272 | }
273 |
274 | if (flush) {
275 | output.push(String.fromCharCode(output_));
276 | output = output.join('');
277 | output = String.fromCharCode(output.charCodeAt(0)|256) + output.substring(1);
278 | } else {
279 | output = output.join('');
280 | }
281 |
282 | return output;
283 |
284 | }
285 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/js/content_script.js:
--------------------------------------------------------------------------------
1 | /* Include ////////////////////////////////////////////////////*/
2 |
3 | // Done via manifest
4 |
5 | /* Content script /////////////////////////////////////////////////*/
6 |
7 | let g_IndexPageInfo = new PageLocation();
8 | let g_BookList = {};
9 |
10 | function ParseBookInfoFromIndexPage( doc, id )
11 | {
12 | let info = new Doujinshi();
13 | info.id = id;
14 | // Name node can fail if nhentai has error page
15 | let nameNode = doc.querySelector( "#info > h1 > span.pretty" );
16 | if( nameNode == null )
17 | return null;
18 |
19 | info.name = nameNode.innerText;
20 | info.image = doc.querySelector( "#cover > a > img" ).src;
21 | info.tags = []; // no for now
22 |
23 | return info;
24 | }
25 |
26 | // Check current book number for this URL
27 | async function CheckPageAndAdd()
28 | {
29 | // Check that it must be nhentai.net/g/NNNNNNN/
30 | let page = ParseBookNumberFromUrl( location.href );
31 | g_IndexPageInfo = page;
32 |
33 | let bookInfo = null;
34 | let state = 0;
35 |
36 | // If in the reading sub page
37 | if( page.pageNumber > 0 )
38 | state = STATE_READ;
39 |
40 | // Snatch book info if this is index page
41 | if( page.isIndexPage )
42 | {
43 | bookInfo = ParseBookInfoFromIndexPage( document, page.bookId );
44 | if( bookInfo == null )
45 | {
46 | // It could also be a purged 404 page
47 | // Display dropdown box to change status instead
48 | // continue down below
49 | }
50 |
51 | // Check if fav button is written 'Unfavorite' that's mean we fav this one
52 | let favTextNode = document.querySelector( "#favorite>.text" );
53 | if( favTextNode != null )
54 | {
55 | if( favTextNode.innerText.includes( "Unfav" ) )
56 | state = STATE_FAV; // Change to fav state now
57 |
58 | // Also add a monitor, check for text change from NH doing instead, it is faster than guessing the delay
59 | // Using new MutationObserver as DOMSubtreeModified is deprecated
60 | //favTextNode.addEventListener( "DOMSubtreeModified", () => _OnFavStateChanged( favTextNode ) );
61 | const config = { attributes: false, childList: true, subtree: true };
62 | const callback = (mutationList, observer) =>
63 | {
64 | // Assumes mutation is only subtree we want, I did not checked or iterate the list https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
65 | for (const mutation of mutationList)
66 | {
67 | console.log(`The ${mutation.type} on ${mutation.target} was modified.`);
68 | }
69 | _OnFavStateChanged( favTextNode )
70 | };
71 |
72 | const observer = new MutationObserver(callback);
73 |
74 | // Start observing the target node for configured mutations
75 | observer.observe(favTextNode, config);
76 | }
77 | }
78 |
79 | // Note: Do not check for 'bookInfo != null' it can be null when reading page 1 of manga
80 | if( page.bookId > 0 )
81 | {
82 | // Content script cannot access background page, must use messaging
83 | // Even if state == 0, it still update book info
84 | chrome.runtime.sendMessage(
85 | {
86 | cmd: "setbook",
87 | id: page.bookId,
88 | state: state,
89 | info: bookInfo
90 | } );
91 | }
92 |
93 | // Acquire book state into global var
94 | await AcquireGlobalBookState();
95 |
96 | if( page.isIndexPage )
97 | {
98 | // index page also has recommendation below, write their state
99 | WriteGridResult( g_BookList, true );
100 |
101 | let state = g_BookList[g_IndexPageInfo.bookId];
102 | WriteIndexPageTool( state );
103 | }
104 | else // Could be a search result or main page
105 | {
106 | WriteGridResult( g_BookList, true );
107 | }
108 |
109 | g_CheckPageAndAddRunOnce = true;
110 | }
111 |
112 | function _OnFavStateChanged( favTextNode )
113 | {
114 | if( g_IndexPageInfo == null )
115 | return;
116 |
117 | let newState = STATE_READ;
118 | if( favTextNode.innerText.includes( "Unfav" ) )
119 | newState = STATE_FAV;
120 | SetBookCoverState( g_IndexPageInfo.bookId, g_IndexPageCover, newState, true );
121 | }
122 |
123 | async function AcquireGlobalBookState()
124 | {
125 | await SendMessagePromise( { cmd: "getbook" }, ( response ) =>
126 | {
127 | g_BookList = response.books;
128 | } );
129 | }
130 |
131 | // Check if current page has linking to another book
132 | // Write state if read or already in fav
133 | function WriteGridResult( database, sendBookInfoHint )
134 | {
135 | if( sendBookInfoHint == null )
136 | sendBookInfoHint = false;
137 |
138 | let allCovers = document.getElementsByClassName( 'cover' );
139 | allCovers = Array.from( allCovers ); // Note: allCovers is dynamic array, it could expand if new cover class is added later, so take snapshot of it
140 | let c = allCovers.length;
141 | for( let i = 0; i < c; i++ )
142 | {
143 | let cover = allCovers[i];
144 | let page = ParseBookNumberFromUrl( cover.href );
145 | if( page.bookId == 0 )
146 | continue;
147 |
148 | if( sendBookInfoHint )
149 | {
150 | // Auto send book hint to background service
151 | // In case it discover some book that does not seen meta data yet
152 | let bookInfo = ParseBookInfoFromCoverNode( cover, page.bookId );
153 | chrome.runtime.sendMessage( { cmd: "bookinfohint", info: bookInfo } );
154 | }
155 |
156 | let state = database[page.bookId];
157 | DecorateCoverWithState( page.bookId, cover, state );
158 | }
159 | }
160 |
161 | function CreateMarkButtonForCover( coverNode, state, bookId )
162 | {
163 | let div = null;
164 | let needToshowButton = false;
165 |
166 | if( state == null || state == 0 )
167 | needToshowButton = true;
168 |
169 | // Try existing one first
170 | div = coverNode.getElementsByClassName( "coverButtonRoot" )[0];
171 | if( needToshowButton && div == null )
172 | {
173 | // Create
174 | div = document.createElement( 'div' );
175 | coverNode.insertAdjacentElement( 'afterbegin', div ); // As first child
176 | div.id = div.className = "coverButtonRoot";
177 |
178 | let thisBookId = bookId;
179 | let CreateHeaderButton = function (root, text, className, setToState)
180 | {
181 | let b = document.createElement( "button" );
182 | b.innerText = text;
183 | b.className = className;
184 | root.appendChild( b );
185 | b.addEventListener( "click", ( e ) =>
186 | {
187 | SetBookCoverState( thisBookId, coverNode, setToState );
188 | div.remove(); // Eject entire div root
189 | e.preventDefault(); // Do not jump for link
190 | } );
191 | };
192 |
193 | CreateHeaderButton( div, "READ", "coverButton read", STATE_READ );
194 | CreateHeaderButton( div, "Later", "coverButton later", STATE_TOREAD );
195 | CreateHeaderButton( div, "Ignore", "coverButton ignore", STATE_IGNORE );
196 | }
197 | else if( !needToshowButton && div != null )
198 | {
199 | div.remove();
200 | }
201 | }
202 |
203 |
204 |
205 | function ParseBookInfoFromCoverNode( coverNode, id )
206 | {
207 | let info = new Doujinshi();
208 | info.id = id;
209 | if( coverNode == null )
210 | return null;
211 |
212 | // ParseBookInfoFromCoverNode will fail if called with cover in index page
213 | let captionNode = coverNode.querySelector( ".caption" );
214 | if( captionNode == null )
215 | return null;
216 |
217 | info.name = captionNode.innerText;
218 | info.image = coverNode.querySelector( "img" ).src;
219 | info.tags = []; // no for now
220 |
221 | return info;
222 | }
223 |
224 | function SetBookCoverState( bookId, coverNode, state, force )
225 | {
226 | // Auto extract book info from cover
227 | let bookInfo = ParseBookInfoFromCoverNode( coverNode, bookId );
228 |
229 | chrome.runtime.sendMessage(
230 | {
231 | cmd: "setbook",
232 | id: bookId,
233 | state: state,
234 | force: force, // Probably vis selector, override the state
235 | info: bookInfo
236 | } );
237 | DecorateCoverWithState( bookId, coverNode, state ); // Mock local state
238 | }
239 |
240 | let g_IndexPageCover = null;
241 | function WriteIndexPageTool( state )
242 | {
243 | g_IndexPageCover = document.querySelector( "#cover" );
244 |
245 | // Or use
404 Not Found node for purged book
246 | let skipCoverEffect = false;
247 | if( g_IndexPageCover == null )
248 | {
249 | // But create as sibling next to it instead
250 | let notFoundNode = document.querySelector(".error > h1")
251 | if( notFoundNode != null )
252 | {
253 | g_IndexPageCover = notFoundNode.insertAdjacentElement( 'afterend', document.createElement( 'div' ) );
254 | skipCoverEffect = true;
255 | }
256 | }
257 |
258 | if( !skipCoverEffect )
259 | {
260 | // This is running for index page cover, which run separately from suggestion cover below the index page
261 | DecorateCoverWithState( g_IndexPageInfo.bookId, g_IndexPageCover, state ); // Index page cover never use dim
262 | }
263 |
264 | CreateBookStateSelector( g_IndexPageCover ); // Also add selector for user to override book state
265 | }
266 |
267 | function DecorateCoverWithState( bookId, cover, state )
268 | {
269 | if( cover == null )
270 | return;
271 |
272 | let useDimEffect = false;
273 | let modifyImgClassTo = "cover read"; // This is custom class injected by our css
274 |
275 | // Get existing or create new
276 | let header = cover.querySelector( "#coverStatus" );
277 | if( header == null )
278 | {
279 | header = document.createElement( "div" );
280 | cover.insertAdjacentElement( 'afterbegin', header ); // Then insert it at the top of cover box
281 | header.id = "coverStatus";
282 | }
283 |
284 | if( state === STATE_READ )
285 | {
286 | header.className = "coverStatus read";
287 | header.innerHTML = "READ";
288 | useDimEffect = true;
289 | }
290 | else if( state === STATE_FAV )
291 | {
292 | header.className = "coverStatus fav";
293 | header.innerHTML = "IN FAVORITE";
294 | useDimEffect = true;
295 | }
296 | else if( state === STATE_IGNORE )
297 | {
298 | header.className = "coverStatus ignore";
299 | header.innerHTML = "IGNORED";
300 | modifyImgClassTo = "cover ignore";
301 | useDimEffect = true;
302 | }
303 | else if( state === STATE_TOREAD )
304 | {
305 | header.className = "coverStatus toread";
306 | header.innerHTML = "TO READ";
307 | useDimEffect = false;
308 | }
309 | else
310 | {
311 | // cover is not needed
312 | header.remove();
313 | }
314 |
315 | CreateMarkButtonForCover( cover, state, bookId );
316 |
317 | //// If gallery is already black listed, dont touch its style
318 | //let coverParent = cover.parentNode;
319 | //if( coverParent.className.includes( 'blacklisted' ) ) // Likely 'gallery blacklisted'
320 | // useDimEffect = false;
321 |
322 | // Hack: Index page will not dim cover
323 | if( cover === g_IndexPageCover )
324 | useDimEffect = false;
325 |
326 | let img = cover.getElementsByTagName( 'img' )[0]; // Get img tag inside the cover root, which is cover image itself.
327 | if( img == null )
328 | return;
329 |
330 | if( useDimEffect )
331 | {
332 | // Modify image node inside not cover root
333 | // Because it will also modify our "READ" caption (bad)
334 | img.className = modifyImgClassTo;
335 | }
336 | else
337 | {
338 | // Restore its original class
339 | img.className = "lazyload";
340 | }
341 | }
342 |
343 | function CreateBookStateSelector( coverNode )
344 | {
345 | // Selector always persist at every state
346 | let selector = coverNode.querySelector( "#stateSelector" );
347 | if( selector != null )
348 | return;
349 |
350 | selector = document.createElement( "select" );
351 | coverNode.insertAdjacentElement( "beforeend", selector ); // As last child
352 | selector.id = "stateSelector";
353 | selector.style = "width:80%;";
354 | selector.innerHTML =
355 | '' +
356 | `` +
357 | `` +
358 | `` +
359 | ``;
360 | selector.addEventListener( 'change', function ()
361 | {
362 | let option = this.options[this.selectedIndex].value;
363 | option = parseInt( option );
364 | if( Number.isNaN( option ) )
365 | return;
366 |
367 | SetBookCoverState( g_IndexPageInfo.bookId, coverNode, option, true );
368 | } );
369 | }
370 |
371 | /* Page refocus service //////////////////////////////////////////////*/
372 |
373 | let g_CheckPageAndAddRunOnce = false;
374 |
375 | window.addEventListener( "focus", OnPageRefocus );
376 | async function OnPageRefocus()
377 | {
378 | if( !g_CheckPageAndAddRunOnce )
379 | return;
380 |
381 | console.log( 'focus check, update all book grid again' );
382 | await AcquireGlobalBookState();
383 | WriteGridResult( g_BookList, false ); // Rewrite grid result again in case something is updated
384 | }
385 |
386 | /* Page init //////////////////////////////////////////////////////////*/
387 |
388 | CheckPageAndAdd();
389 |
--------------------------------------------------------------------------------
/js/settings.js:
--------------------------------------------------------------------------------
1 | /* Include ////////////////////////////////////////////////////*/
2 |
3 | // Dnoe via HTML
4 |
5 | /*///////////////////////////////////////////////////////////////////*/
6 |
7 | //document.getElementById( "save" ).addEventListener( "click", function ()
8 | //{
9 | // chrome.runtime.sendMessage( { cmd: "save" } );
10 |
11 | //} );
12 |
13 | document.getElementById( "clearStorage" ).addEventListener( "click", function ()
14 | {
15 | chrome.runtime.sendMessage( { cmd: "wipe" } );
16 | setTimeout( RefreshPage, 300 );
17 | } );
18 |
19 | let g_StatusNode = document.getElementById( "nbDoujinshi" );
20 | let g_StatusNode2 = document.getElementById( "statusText2" );
21 |
22 | function SetStatusText( txt, append )
23 | {
24 | if( append )
25 | g_StatusNode.innerHTML += txt;
26 | else
27 | g_StatusNode.innerHTML = txt;
28 | }
29 |
30 | function SetStatusText2( txt, append )
31 | {
32 | if( append )
33 | g_StatusNode2.innerHTML += txt;
34 | else
35 | g_StatusNode2.innerHTML = txt;
36 | }
37 |
38 | async function WriteStorageInfoText()
39 | {
40 | let syncByte = await chrome.storage.sync.getBytesInUse();
41 | document.getElementById( "syncSpace" ).innerText = `Sync storage ${( syncByte / 1024 ).toFixed( 2 )}K (v1.1.x does not use this anymore because of very limited space)`;
42 |
43 | let localByte = await chrome.storage.local.getBytesInUse();
44 | document.getElementById( "localSpace" ).innerText = `Local storage ${( localByte / 1024 ).toFixed( 2 )}K`;
45 | }
46 |
47 | /* Favorite fetching ///////////////////////////////////////////////*/
48 |
49 | document.getElementById( "update" ).addEventListener( "click", function ()
50 | {
51 | _StartAnimateStatusText();
52 | SetStatusText( "Fetching... (might take a while)" );
53 | chrome.runtime.sendMessage( { cmd: "getfav" }, ( response ) => OnFavLoaded( response.succeed, response.reason ) );
54 |
55 | } );
56 |
57 | function OnFavLoaded( succeed, reason )
58 | {
59 | _StopAnimateStatusText();
60 | if( succeed )
61 | {
62 | alert( `Done, ${reason} books fetched from favorite.` );
63 | RefreshPage();
64 | }
65 | else
66 | {
67 | SetStatusText( "Error due to " + reason );
68 | }
69 | }
70 |
71 | let g_AnimateStatusText = false;
72 | function _StartAnimateStatusText()
73 | {
74 | g_AnimateStatusText = true;
75 | setTimeout( _AnimateStatusText, 500 );
76 | }
77 |
78 | function _AnimateStatusText()
79 | {
80 | SetStatusText( "|", true );
81 | if( g_AnimateStatusText ) // While not stop
82 | setTimeout( _AnimateStatusText, 250 );
83 | }
84 |
85 | function _StopAnimateStatusText()
86 | {
87 | g_AnimateStatusText = false;
88 | }
89 |
90 | /* Missing book info filing /////////////////////////////////////////////*/
91 |
92 | document.getElementById( "getmissinginfo" ).addEventListener( "click", function ()
93 | {
94 | if( confirm( "This will fetch book with missing name.\nLeave it running until dialog box come up, do not press the button again.\nIf you have too many data too fetch, site might kick you, try again later." ) )
95 | {
96 | GetMissingBookInfoOneByOne( );
97 | }
98 |
99 | } );
100 |
101 | async function GetMissingBookInfoOneByOne( )
102 | {
103 | SetStatusText( "Fetching... (might take a while)" );
104 | let working = true;
105 | while( working )
106 | {
107 | await SendMessagePromise( { cmd: "getmissinginfo" }, ( response ) => {
108 | if( response.succeed )
109 | {
110 | // if reason is non null, background has fetched something
111 | // if reason is null, background has no more to fetch
112 | let reason = response.reason;
113 | if( reason == null )
114 | {
115 | // Done
116 | alert( "DONE, No more to fetch!" );
117 | RefreshPage( );
118 | working = false;
119 | }
120 | else
121 | {
122 | //
123 | SetStatusText( `Fetching... ${reason}` );
124 | }
125 | }
126 | else
127 | {
128 | working = false;
129 | SetStatusText( "Error due to " + reason );
130 | }
131 | } );
132 | }
133 |
134 | }
135 |
136 | function OnMissingInfoLoaded( succeed, reason )
137 | {
138 |
139 | }
140 |
141 | /* History submit service ////////////////////////////////////////////////*/
142 |
143 | document.getElementById( "historySubmit" ).addEventListener( "submit", RunHistoryCheckUsingGoogleTakeOut );
144 | document.getElementById( "historySubmit2" ).addEventListener( "submit", RunHistoryCheckUsingLineFile );
145 |
146 | function _LoadFileFromFileElement( elementId, callback )
147 | {
148 | const selectedFile = document.getElementById( elementId ).files[0];
149 | if( selectedFile != null ) // selectedFile is file object
150 | {
151 | let reader = new FileReader();
152 | reader.onload = function ( e )
153 | {
154 | callback( e.target.result );
155 | };
156 | reader.readAsText( selectedFile );
157 | }
158 | else
159 | {
160 | alert( "File not selected, dont be shy, select one!" );
161 | }
162 | event.preventDefault(); // Do not reload the page
163 | }
164 |
165 | function RunHistoryCheckUsingGoogleTakeOut( event )
166 | {
167 | _LoadFileFromFileElement( "historyFile", _ProcessHistoryGoogleTakeOut );
168 | }
169 |
170 | function RunHistoryCheckUsingLineFile( event )
171 | {
172 | _LoadFileFromFileElement( "historyFile2", _ProcessHistoryLineFile );
173 | }
174 |
175 | /*
176 | * File look like this
177 | * {
178 | "Browser History": [
179 | {
180 | "favicon_url": "https://nhentai.net/favicon.ico",
181 | "page_transition": "RELOAD",
182 | "title": "Asa Okitara Imouto ga Hadaka Apron Sugata datta node Hamete Mita | I Woke Up to my Naked Apron Sister and Tried Fucking Her - Ch.2 » nhentai: hentai doujinshi and manga",
183 | "url": "https://nhentai.net/g/394923/",
184 | "client_id": "xPuNMd8l0AefScLPuV8eYA==",
185 | "time_usec": 1647165548781917
186 | },
187 | * */
188 | function _ProcessHistoryGoogleTakeOut( text )
189 | {
190 | var obj = null;
191 | try
192 | {
193 | obj = JSON.parse( text );
194 | } catch( _ ) { }
195 |
196 | if( obj == null )
197 | {
198 | alert( "Not valid JSON" );
199 | return;
200 | }
201 |
202 | obj = obj["Browser History"]; // This resolve to an array
203 | if( obj == null )
204 | {
205 | alert( "Not valid Google Browser History file" );
206 | return;
207 | }
208 |
209 | let seen = {};
210 | let c = obj.length;
211 | for( let i = 0; i < c; i++ )
212 | {
213 | let entry = obj[i];
214 | // Inspect this URL
215 | let page = ParseBookNumberFromUrl( entry.url );
216 | if( page.bookId == 0 )
217 | continue;
218 |
219 | // Write into this in case there is dupe URL
220 | seen[page.bookId] = 1;
221 | }
222 |
223 | alert( "Imported total " + Object.keys( seen ).length + " entire(s)." );
224 |
225 | for( let bookId in seen )
226 | {
227 | chrome.runtime.sendMessage(
228 | {
229 | cmd: "setbook",
230 | id: bookId,
231 | state: STATE_READ,
232 | info: null // Have no info at this moment
233 | } );
234 | }
235 |
236 | sleep( 100 ).then( RefreshPage );
237 | }
238 |
239 | function _ProcessHistoryLineFile( text )
240 | {
241 | // read each line then scan for URL
242 | let lines = text.split( /\r?\n/ );
243 |
244 | let seen = {};
245 | let c = lines.length;
246 | for( let i = 0; i < c; i++ )
247 | {
248 | let line = lines[i];
249 |
250 | // Inspect this URL
251 | let page = ParseBookNumberFromUrl( line );
252 | if( page.bookId == 0 )
253 | continue;
254 |
255 | // Write into this in case there is dupe URL
256 | seen[page.bookId] = 1;
257 | }
258 |
259 | alert( "Imported total " + Object.keys( seen ).length + " entire(s)." );
260 |
261 | for( let bookId in seen )
262 | {
263 | chrome.runtime.sendMessage(
264 | {
265 | cmd: "setbook",
266 | id: bookId,
267 | state: STATE_READ,
268 | info: null // Have no info at this moment
269 | } );
270 | }
271 |
272 | sleep( 100 ).then( RefreshPage );
273 | }
274 |
275 | /* Book listing ////////////////////////////////////*/
276 |
277 | bookDisplayType.addEventListener( 'change', function ()
278 | {
279 | g_DisplayFilter = this.options[this.selectedIndex].value;
280 |
281 | // Also save this new option
282 | chrome.storage.local.set( { g_DisplayFilter } );
283 | RefreshPage();
284 | } );
285 |
286 | let g_DisplayFilter = null;
287 |
288 | function RefreshPage()
289 | {
290 | // Write extension version
291 | {
292 | var manifestData = chrome.runtime.getManifest();
293 | document.getElementById( "version" ).innerText = "v" + manifestData.version;
294 | }
295 |
296 | WriteStorageInfoText();
297 |
298 | chrome.runtime.sendMessage( { cmd: "getbook" }, ( response ) =>
299 | {
300 | let books = response.books;
301 |
302 | let allCount = 0;
303 | let favCount = 0;
304 | let ignoreCount = 0;
305 | let toreadCount = 0;
306 |
307 | for( let bookId in books )
308 | {
309 | let state = books[bookId];
310 | if( state == null || state === 0 )
311 | continue;
312 |
313 | allCount++;
314 | if( state === STATE_FAV )
315 | favCount++;
316 | else if( state === STATE_IGNORE )
317 | ignoreCount++;
318 | else if( state === STATE_TOREAD )
319 | toreadCount++;
320 | }
321 |
322 | SetStatusText( `You have read ${allCount} books, ${favCount} in favorite. (${toreadCount} in reading queue) (${ignoreCount} ignored)` );
323 |
324 | DisplayReadBooks( books );
325 | } );
326 | }
327 |
328 | async function DisplayReadBooks( bookState )
329 | {
330 | let root = document.getElementById( "bookDisplay" );
331 | root.innerHTML = ''; // Clear all children
332 |
333 | // If this is first run, g_DisplayFilter will be null on page load
334 | // Check for previous value in storage
335 | if( g_DisplayFilter == null )
336 | {
337 | // Ask from last time, or default to "default" filter
338 | let save = await chrome.storage.local.get( { g_DisplayFilter: "default" } );
339 | g_DisplayFilter = save["g_DisplayFilter"];
340 | bookDisplayType.value = g_DisplayFilter;
341 | }
342 |
343 | let allIds = Object.keys( bookState );
344 |
345 | // Sort ascending, but convert to number first
346 | allIds = allIds.map( ( item ) => parseInt( item ) );
347 | allIds.sort( ( a, b ) => a - b ); // Sort using number method
348 |
349 | let processLater = [];
350 | let c = allIds.length;
351 | for( let i = 0; i < c; i++ )
352 | {
353 | let id = allIds[i];
354 | if( id == null || id == 0 ) // ID zero is never shown
355 | continue;
356 |
357 | // Check with filter
358 | let state = bookState[id];
359 | if( state == null || state === 0 )
360 | continue; // Book can be in Unread (0) state by index page selector override, filter it out
361 |
362 | if( g_DisplayFilter === "fav" )
363 | {
364 | if( state !== STATE_FAV )
365 | continue;
366 | }
367 | else if( g_DisplayFilter === "toread" )
368 | {
369 | if( state !== STATE_TOREAD )
370 | continue;
371 | }
372 | else if( g_DisplayFilter === "ignore" )
373 | {
374 | if( state !== STATE_IGNORE )
375 | continue;
376 | }
377 | else // Default filter
378 | {
379 | // Filter only these
380 | if( state !== STATE_READ && state !== STATE_FAV )
381 | continue;
382 | }
383 |
384 | let line = document.createElement( "div" );
385 | root.appendChild( line );
386 |
387 | let link = document.createElement( "a" );
388 | line.appendChild( link );
389 | link.textContent = id;
390 | link.href = "https://nhentai.net/g/" + id;
391 | link.target = "_blank"; // Open in new tab
392 |
393 | let text = document.createTextNode( "" );
394 | line.appendChild( text );
395 | processLater.push( { line: text, id, state } );
396 | }
397 |
398 | _QueryBookInfoForList( processLater );
399 | }
400 |
401 | let g_QueryJobId = 0;
402 | async function _QueryBookInfoForList( list )
403 | {
404 | // While for loop is running on very long list, user could change filter option
405 | // Will use g_QueryJobId to check if this job can be terminated
406 | // Every new call to _QueryBookInfoForList will increment to new job id
407 | const thisJobId = ++g_QueryJobId;
408 | const c = list.length;
409 | let haveInfo = 0;
410 | for( let i = 0; i < c; i++ )
411 | {
412 | if( g_QueryJobId != thisJobId ) break;
413 |
414 | let item = list[i];
415 | await SendMessagePromise( { cmd: "getbookinfo", id: item.id }, ( response ) =>
416 | {
417 | if( g_QueryJobId != thisJobId ) return;
418 | if( item.line == null ) return; // Maybe destroyed from switching
419 | if( response.bookInfo == null ) return;
420 |
421 | haveInfo++;
422 | _WriteBookLineInfo( item.line, response.bookInfo, item.state );
423 | } );
424 |
425 | SetStatusText2( `Book info available ${haveInfo}/${c}` );
426 | }
427 | }
428 |
429 | function _WriteBookLineInfo( textNode, bookInfo, state )
430 | {
431 | let txt = " " + bookInfo.name;
432 | if( state === STATE_FAV )
433 | txt += " (FAVORITE)";
434 | if( state === STATE_IGNORE )
435 | txt += " (IGNORED)";
436 | if( state === STATE_TOREAD )
437 | txt += " (TO READ LATER)";
438 | textNode.textContent = txt;
439 | }
440 |
441 | /* Import export service ///////////////////////////////////////////////*/
442 |
443 | document.getElementById( "exportData" ).addEventListener( "click", ExportUserData );
444 | document.getElementById( "importData" ).addEventListener( "click", ImportUserData );
445 |
446 | function ExportUserData()
447 | {
448 | chrome.runtime.sendMessage( { cmd: "dump" }, ( response ) =>
449 | {
450 | let books = response.books; // Book state
451 | let db = response.db; // Book info db
452 | let json = JSON.stringify( { books, db } );
453 | let blob = new Blob( [json], { type: "text/plain" } );
454 | var url = URL.createObjectURL( blob );
455 | chrome.downloads.download( {
456 | url: url,
457 | filename: "NHTrackerData.json",
458 | saveAs: true // Download using save as dialog
459 | } );
460 | } );
461 | }
462 |
463 | function ImportUserData()
464 | {
465 | _LoadFileFromFileElement( "importFile", _ProcessImportUserData );
466 | }
467 |
468 | function _ProcessImportUserData( text )
469 | {
470 | try
471 | {
472 | let obj = JSON.parse( text );
473 | if( obj.books != null && obj.db != null )
474 | {
475 | chrome.runtime.sendMessage( { cmd: "importDump", books: obj.books, db: obj.db } );
476 | alert(
477 | `Merged ${Object.keys(obj.books).length} books state, ${Object.keys(obj.db).length} DB entires.` +
478 | "\nNote that this is a merge operation, if you wish to start new, wipe data then import again."
479 | );
480 | setTimeout( RefreshPage, 300 );
481 | }
482 | else
483 | {
484 | throw false;
485 | }
486 | }
487 | catch( e )
488 | {
489 | alert( "File is not valid NHTracker save file" );
490 | }
491 | }
492 |
493 | /* Page init //////////////////////////////////////////////////////////*/
494 | RefreshPage();
495 |
--------------------------------------------------------------------------------
/js/lz-string.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2013 Pieroxy
2 | // This work is free. You can redistribute it and/or modify it
3 | // under the terms of the WTFPL, Version 2
4 | // For more information see LICENSE.txt or http://www.wtfpl.net/
5 | //
6 | // For more information, the home page:
7 | // http://pieroxy.net/blog/pages/lz-string/testing.html
8 | //
9 | // LZ-based compression algorithm, version 1.4.4
10 | var LZString = (function() {
11 |
12 | // private property
13 | var f = String.fromCharCode;
14 | var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
15 | var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
16 | var baseReverseDic = {};
17 |
18 | function getBaseValue(alphabet, character) {
19 | if (!baseReverseDic[alphabet]) {
20 | baseReverseDic[alphabet] = {};
21 | for (var i=0 ; i>> 8;
66 | buf[i*2+1] = current_value % 256;
67 | }
68 | return buf;
69 | },
70 |
71 | //decompress from uint8array (UCS-2 big endian format)
72 | decompressFromUint8Array:function (compressed) {
73 | if (compressed===null || compressed===undefined){
74 | return LZString.decompress(compressed);
75 | } else {
76 | var buf=new Array(compressed.length/2); // 2 bytes per character
77 | for (var i=0, TotalLen=buf.length; i> 1;
159 | }
160 | } else {
161 | value = 1;
162 | for (i=0 ; i> 1;
184 | }
185 | }
186 | context_enlargeIn--;
187 | if (context_enlargeIn == 0) {
188 | context_enlargeIn = Math.pow(2, context_numBits);
189 | context_numBits++;
190 | }
191 | delete context_dictionaryToCreate[context_w];
192 | } else {
193 | value = context_dictionary[context_w];
194 | for (i=0 ; i> 1;
204 | }
205 |
206 |
207 | }
208 | context_enlargeIn--;
209 | if (context_enlargeIn == 0) {
210 | context_enlargeIn = Math.pow(2, context_numBits);
211 | context_numBits++;
212 | }
213 | // Add wc to the dictionary.
214 | context_dictionary[context_wc] = context_dictSize++;
215 | context_w = String(context_c);
216 | }
217 | }
218 |
219 | // Output the code for w.
220 | if (context_w !== "") {
221 | if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) {
222 | if (context_w.charCodeAt(0)<256) {
223 | for (i=0 ; i> 1;
244 | }
245 | } else {
246 | value = 1;
247 | for (i=0 ; i> 1;
269 | }
270 | }
271 | context_enlargeIn--;
272 | if (context_enlargeIn == 0) {
273 | context_enlargeIn = Math.pow(2, context_numBits);
274 | context_numBits++;
275 | }
276 | delete context_dictionaryToCreate[context_w];
277 | } else {
278 | value = context_dictionary[context_w];
279 | for (i=0 ; i> 1;
289 | }
290 |
291 |
292 | }
293 | context_enlargeIn--;
294 | if (context_enlargeIn == 0) {
295 | context_enlargeIn = Math.pow(2, context_numBits);
296 | context_numBits++;
297 | }
298 | }
299 |
300 | // Mark the end of the stream
301 | value = 2;
302 | for (i=0 ; i> 1;
312 | }
313 |
314 | // Flush the last char
315 | while (true) {
316 | context_data_val = (context_data_val << 1);
317 | if (context_data_position == bitsPerChar-1) {
318 | context_data.push(getCharFromInt(context_data_val));
319 | break;
320 | }
321 | else context_data_position++;
322 | }
323 | return context_data.join('');
324 | },
325 |
326 | decompress: function (compressed) {
327 | if (compressed == null) return "";
328 | if (compressed == "") return null;
329 | return LZString._decompress(compressed.length, 32768, function(index) { return compressed.charCodeAt(index); });
330 | },
331 |
332 | _decompress: function (length, resetValue, getNextValue) {
333 | var dictionary = [],
334 | next,
335 | enlargeIn = 4,
336 | dictSize = 4,
337 | numBits = 3,
338 | entry = "",
339 | result = [],
340 | i,
341 | w,
342 | bits, resb, maxpower, power,
343 | c,
344 | data = {val:getNextValue(0), position:resetValue, index:1};
345 |
346 | for (i = 0; i < 3; i += 1) {
347 | dictionary[i] = i;
348 | }
349 |
350 | bits = 0;
351 | maxpower = Math.pow(2,2);
352 | power=1;
353 | while (power!=maxpower) {
354 | resb = data.val & data.position;
355 | data.position >>= 1;
356 | if (data.position == 0) {
357 | data.position = resetValue;
358 | data.val = getNextValue(data.index++);
359 | }
360 | bits |= (resb>0 ? 1 : 0) * power;
361 | power <<= 1;
362 | }
363 |
364 | switch (next = bits) {
365 | case 0:
366 | bits = 0;
367 | maxpower = Math.pow(2,8);
368 | power=1;
369 | while (power!=maxpower) {
370 | resb = data.val & data.position;
371 | data.position >>= 1;
372 | if (data.position == 0) {
373 | data.position = resetValue;
374 | data.val = getNextValue(data.index++);
375 | }
376 | bits |= (resb>0 ? 1 : 0) * power;
377 | power <<= 1;
378 | }
379 | c = f(bits);
380 | break;
381 | case 1:
382 | bits = 0;
383 | maxpower = Math.pow(2,16);
384 | power=1;
385 | while (power!=maxpower) {
386 | resb = data.val & data.position;
387 | data.position >>= 1;
388 | if (data.position == 0) {
389 | data.position = resetValue;
390 | data.val = getNextValue(data.index++);
391 | }
392 | bits |= (resb>0 ? 1 : 0) * power;
393 | power <<= 1;
394 | }
395 | c = f(bits);
396 | break;
397 | case 2:
398 | return "";
399 | }
400 | dictionary[3] = c;
401 | w = c;
402 | result.push(c);
403 | while (true) {
404 | if (data.index > length) {
405 | return "";
406 | }
407 |
408 | bits = 0;
409 | maxpower = Math.pow(2,numBits);
410 | power=1;
411 | while (power!=maxpower) {
412 | resb = data.val & data.position;
413 | data.position >>= 1;
414 | if (data.position == 0) {
415 | data.position = resetValue;
416 | data.val = getNextValue(data.index++);
417 | }
418 | bits |= (resb>0 ? 1 : 0) * power;
419 | power <<= 1;
420 | }
421 |
422 | switch (c = bits) {
423 | case 0:
424 | bits = 0;
425 | maxpower = Math.pow(2,8);
426 | power=1;
427 | while (power!=maxpower) {
428 | resb = data.val & data.position;
429 | data.position >>= 1;
430 | if (data.position == 0) {
431 | data.position = resetValue;
432 | data.val = getNextValue(data.index++);
433 | }
434 | bits |= (resb>0 ? 1 : 0) * power;
435 | power <<= 1;
436 | }
437 |
438 | dictionary[dictSize++] = f(bits);
439 | c = dictSize-1;
440 | enlargeIn--;
441 | break;
442 | case 1:
443 | bits = 0;
444 | maxpower = Math.pow(2,16);
445 | power=1;
446 | while (power!=maxpower) {
447 | resb = data.val & data.position;
448 | data.position >>= 1;
449 | if (data.position == 0) {
450 | data.position = resetValue;
451 | data.val = getNextValue(data.index++);
452 | }
453 | bits |= (resb>0 ? 1 : 0) * power;
454 | power <<= 1;
455 | }
456 | dictionary[dictSize++] = f(bits);
457 | c = dictSize-1;
458 | enlargeIn--;
459 | break;
460 | case 2:
461 | return result.join('');
462 | }
463 |
464 | if (enlargeIn == 0) {
465 | enlargeIn = Math.pow(2, numBits);
466 | numBits++;
467 | }
468 |
469 | if (dictionary[c]) {
470 | entry = dictionary[c];
471 | } else {
472 | if (c === dictSize) {
473 | entry = w + w.charAt(0);
474 | } else {
475 | return null;
476 | }
477 | }
478 | result.push(entry);
479 |
480 | // Add w+entry[0] to the dictionary.
481 | dictionary[dictSize++] = w + entry.charAt(0);
482 | enlargeIn--;
483 |
484 | w = entry;
485 |
486 | if (enlargeIn == 0) {
487 | enlargeIn = Math.pow(2, numBits);
488 | numBits++;
489 | }
490 |
491 | }
492 | }
493 | };
494 | return LZString;
495 | })();
496 |
497 | if (typeof define === 'function' && define.amd) {
498 | define(function () { return LZString; });
499 | } else if( typeof module !== 'undefined' && module != null ) {
500 | module.exports = LZString
501 | } else if( typeof angular !== 'undefined' && angular != null ) {
502 | angular.module('LZString', [])
503 | .factory('LZString', function () {
504 | return LZString;
505 | });
506 | }
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | /* Include ////////////////////////////////////////////////////*/
2 |
3 | try
4 | {
5 | importScripts( '/js/include.js', '/js/lz-string.js' );
6 | }
7 | catch( e )
8 | {
9 | console.error( e );
10 | }
11 |
12 | /* Background worker ////////////////////////////////////////////*/
13 |
14 | // All read book numbers
15 | // Key: string book number
16 | // Value: int book state, see STATE_READ
17 | let g_ReadBooks = {};
18 |
19 | // Basic book data such as name or image
20 | // Get updated only in occasion that plugin come into contact with such info
21 | // Key: string book number
22 | // Value: Doujinshi object
23 | let g_BookDb = {};
24 |
25 | chrome.action.onClicked.addListener( ( tab ) =>
26 | {
27 | chrome.runtime.openOptionsPage();
28 | } );
29 |
30 | chrome.runtime.onInstalled.addListener( () =>
31 | {
32 |
33 | } );
34 |
35 | chrome.runtime.onSuspend.addListener( () =>
36 | {
37 | // This never called somehow
38 | console.log( "Suspending event page" );
39 | SaveDatabase();
40 | } );
41 |
42 | // We dont know when chrome will unload our background task, so save quickly
43 | const SAVE_INTERVAL = 4;
44 | function OnUpdateFunction()
45 | {
46 | SaveDatabase(); // Check check dirty flag inside
47 | setTimeout( OnUpdateFunction, SAVE_INTERVAL * 1000 ); // Recall this after some time
48 | }
49 |
50 | chrome.runtime.onMessage.addListener( _OnMessage );
51 |
52 | function _OnMessage( request, sender, sendResponse )
53 | {
54 | //console.log( "onMessage ", request, sender.tab ? "from a content script:" + sender.tab.url : "from the extension" );
55 |
56 | // Note: As chrome background worker can be put into sleep (unload)
57 | // Most API will need to wait for DB to load first if message is sent from child content script just wake the worker
58 | if( request.cmd === "setbook" )
59 | {
60 | _WriteBookStateFromRequestAsync( request );
61 | }
62 | else if( request.cmd == "getbook" )
63 | {
64 | _GetBookStateAsync( () => sendResponse( { books: g_ReadBooks } ) );
65 | return true;
66 | }
67 | else if( request.cmd == "getfav" )
68 | {
69 | LoadFavorites( ( succeed, reason ) => sendResponse( { succeed, reason } ) );
70 | return true; // This is async request, it took long
71 | }
72 | else if( request.cmd == "getmissinginfo" )
73 | {
74 | LoadMissingInfo( ( succeed, reason ) => sendResponse( { succeed, reason } ) );
75 | return true; // This is async request, it took long
76 | }
77 | else if( request.cmd == "getbookinfo" )
78 | {
79 | GetBookInfoAsync( request.id, ( bookInfo ) => sendResponse( { bookInfo } ) );
80 | return true;
81 | }
82 | else if( request.cmd == "save" )
83 | {
84 | g_BookStateDirty = true;
85 | g_DbStateDirty = true;
86 | SaveDatabase();
87 | }
88 | else if( request.cmd == "bookinfohint" )
89 | {
90 | _SaveBookInfoFromRequestAsync( request );
91 | }
92 | else if( request.cmd == "wipe" )
93 | {
94 | g_ReadBooks = {};
95 | g_BookDb = {};
96 | chrome.storage.local.clear();
97 | chrome.storage.sync.clear();
98 | SaveDatabase( true );
99 | }
100 | else if( request.cmd == "dump" )
101 | {
102 | _GetBookStateAsync( () => sendResponse( { books: g_ReadBooks, db: g_BookDb } ) );
103 | return true; // This is async request, it took long
104 | }
105 | else if( request.cmd == "importDump" )
106 | {
107 | _GetBookStateAsync( () =>
108 | {
109 | MergeObject( g_ReadBooks, request.books );
110 | MergeObject( g_BookDb, request.db );
111 | SaveDatabase( true );
112 | } );
113 | return true; // This is async request, it took long
114 | }
115 | }
116 |
117 | async function _WriteBookStateFromRequestAsync( request )
118 | {
119 | await WaitForDbToLoad();
120 | if( request.info != null )
121 | SetBookInfo( request.info );
122 | SetCoverState( request.id, request.state, request.force );
123 | }
124 |
125 | async function _GetBookStateAsync( callback )
126 | {
127 | await WaitForDbToLoad();
128 | callback();
129 | }
130 |
131 | async function _SaveBookInfoFromRequestAsync( request )
132 | {
133 | await WaitForDbToLoad();
134 | if( request.info != null )
135 | {
136 | // Only write if we have seen this book in state
137 | let bookInfo = request.info;
138 | let state = g_ReadBooks[bookInfo.id];
139 | if( state != null && state > 0 )
140 | SetBookInfo( request.info );
141 | }
142 | }
143 |
144 | let g_DatabaseLoaded = 0;
145 | function InitDatabase()
146 | {
147 | if( g_DatabaseLoaded != 0 )
148 | return;
149 |
150 | g_DatabaseLoaded++;
151 | let OnLoadBookState = function ( save )
152 | {
153 | g_DatabaseLoaded++;
154 | if( save.books == null ) return;
155 |
156 | // Use merge
157 | console.log( "InitDatabase OnLoadBookState called" );
158 | MergeObject( g_ReadBooks, save.books );
159 | //console.log( 'InitDatabase books: ', Object.keys( g_ReadBooks ).length );
160 | };
161 |
162 | console.log( "Issue InitDatabase" );
163 | SyncGetPartitioned( "books", OnLoadBookState ); // Migrate from 1.0.x, try in sync storage first
164 | SyncGetPartitioned( "books", OnLoadBookState, "local" ); // Then local for 1.1.x
165 |
166 | SyncGetPartitioned( "bookdb",
167 | function ( save )
168 | {
169 | g_DatabaseLoaded++;
170 | if( save.bookdb == null ) return;
171 | g_BookDb = save.bookdb;
172 | //console.log( 'InitDatabase bookdb: ', Object.keys( g_BookDb ).length );
173 | }, "local" );
174 | }
175 |
176 | async function WaitForDbToLoad()
177 | {
178 | // Have to wait for DB to load
179 | while( g_DatabaseLoaded < 4 )
180 | {
181 | console.log( "Stall to wait for g_DatabaseLoaded" );
182 | await sleep( 50 );
183 | }
184 | }
185 |
186 | async function _WaitForDbToLoadAndResponseAsync( callback )
187 | {
188 | await WaitForDbToLoad();
189 | callback();
190 | }
191 |
192 |
193 | let g_BookStateDirty = false;
194 | let g_DbStateDirty = false;
195 |
196 | function SaveDatabase( force )
197 | {
198 | if( force || g_BookStateDirty )
199 | {
200 | SyncStorePartitioned( "books", g_ReadBooks, "local" ); // 1.1.x now save to local for unlimited storage
201 | console.log( "SaveDatabase ran" );
202 | }
203 |
204 | if( force || g_DbStateDirty )
205 | SyncStorePartitioned( "bookdb", g_BookDb, "local" );
206 |
207 | g_BookStateDirty = false;
208 | g_DbStateDirty = false;
209 | }
210 |
211 | function SetCoverState( id, targetState, force )
212 | {
213 | if( targetState === undefined )
214 | targetState = STATE_READ;
215 |
216 | let state = g_ReadBooks[id];
217 | if( force || _CanBookStateTranslateFrom( state, targetState ) ) // Write only if state translation is allowed
218 | {
219 | console.log( `Set book state ${id} from ${state} => ${targetState}` );
220 | g_ReadBooks[id] = targetState;
221 | g_BookStateDirty = true;
222 | }
223 | }
224 |
225 | function _CanBookStateTranslateFrom( from, to )
226 | {
227 | if( from === undefined )
228 | return true; // If first state is blank, then it can turn into any
229 |
230 | // TOREAD is special state, it is actually has priority lower then READ (1) but since we cannot have value under 1
231 | // It is hacked for state transition here
232 | // It can turn into any state higher than 0
233 | if( from === STATE_TOREAD && to > 0 )
234 | return true;
235 |
236 | // Else, generic rule, state must be higher in number only
237 | return to > from;
238 | }
239 |
240 | function SetBookInfo( bookInfo )
241 | {
242 | // bookInfo object must have id, name field
243 | if( bookInfo == null || bookInfo.id == null || bookInfo.name == null )
244 | {
245 | console.error( "SetBookInfo passes invalid argument bookInfo", bookInfo );
246 | return;
247 | }
248 |
249 | console.log( "save book info for", bookInfo.id );
250 | g_BookDb[bookInfo.id] = bookInfo;
251 | g_DbStateDirty = true;
252 | }
253 |
254 | // Request book db info, search cache or fire API.
255 | // callback with Doujinshi object
256 | async function GetBookInfoAsync( id, callback )
257 | {
258 | await WaitForDbToLoad();
259 |
260 | // Must wait for DB to load first
261 | if( g_BookDb[id] !== undefined )
262 | {
263 | callback( g_BookDb[id] );
264 | }
265 |
266 | // TODO: Fire nhentai API and cache
267 | callback( undefined );
268 | }
269 |
270 | InitDatabase();
271 | OnUpdateFunction(); // This will fire initial save cycle chain
272 |
273 | /* Helper for storage partitioning //////////////////////////////////*/
274 |
275 | function _MakeStoreSegmentName( key, index )
276 | {
277 | return key + "_" + index;
278 | }
279 |
280 | function SyncStorePartitioned( key, objectToStore, storageApi )
281 | {
282 | let i = 0;
283 | let segmentName = "";
284 | let storage = {};
285 | let str = JSON.stringify( objectToStore );
286 |
287 | if( storageApi === "local" )
288 | storageApi = chrome.storage.local;
289 | else
290 | storageApi = chrome.storage.sync;
291 |
292 | // Technical note: if string contains a lot of escape character, re-json it will produce more escape character
293 | // Better gzip that
294 | str = LZString.compressToBase64( str );
295 | const LIMIT = storageApi.QUOTA_BYTES_PER_ITEM - key.length - 16; // Reserve 16 byte for safer margin
296 |
297 | while( str.length > 0 )
298 | {
299 | segmentName = _MakeStoreSegmentName( key, i );
300 | let thisCycle = str.length;
301 | if( thisCycle > LIMIT )
302 | thisCycle = LIMIT;
303 | storage[segmentName] = str.substr( 0, thisCycle );
304 | str = str.substring( thisCycle, str.length ); // Next part
305 | i++;
306 | }
307 |
308 | // As how the fetching logic will work in reverse
309 | // Also make sure that next i+1 chunk remain saved as empty string to know that this is terminal
310 | // This is also required if new saving data size is shrinking from previous (has fewer chunk count)
311 | segmentName = _MakeStoreSegmentName( key, i );
312 | storage[segmentName] = "";
313 |
314 | // Store all chunks
315 | storageApi.set( storage );
316 | }
317 |
318 | function SyncGetPartitioned( key, callback, storageApi )
319 | {
320 | if( storageApi === "local" )
321 | storageApi = chrome.storage.local;
322 | else
323 | storageApi = chrome.storage.sync;
324 |
325 | _SyncGetPartitionedInternal( key, 0, "", callback, storageApi );
326 | }
327 |
328 | function _SyncGetPartitionedInternal( key, index, str, callback, storageApi )
329 | {
330 | let segmentName = _MakeStoreSegmentName( key, index );
331 | storageApi.get( segmentName, function ( elems )
332 | {
333 | let data = elems[segmentName];
334 | if( data === undefined || data === "" ) // Found terminal
335 | {
336 | try
337 | {
338 | str = LZString.decompressFromBase64( str );
339 |
340 | // Send combined result
341 | // But return it as same format of which storage would do
342 | let obj = {};
343 | obj[key] = JSON.parse( str );
344 | callback( obj );
345 | }
346 | catch( error )
347 | {
348 | console.log( "Error in parsing data from storage", key, error );
349 | callback( undefined );
350 | }
351 | }
352 | else
353 | {
354 | // Deeper!
355 | _SyncGetPartitionedInternal( key, index + 1, str + data, callback, storageApi );
356 | }
357 | } );
358 | }
359 |
360 | /* Fetching favorite //////////////////////////////////////////////*/
361 |
362 | let g_FavAdded = 0;
363 |
364 | function LoadFavorites( callback )
365 | {
366 | g_FavAdded = 0;
367 | fetch( "https://nhentai.net/favorites/" )
368 | .then( ( response ) =>
369 | {
370 | if( response.status === 429 )
371 | {
372 | //loadingCallback( undefined ); // Not logged in
373 | Promise.reject( new Error( "Not logged in" ) );
374 | }
375 | else if( response.status === 200 )
376 | {
377 | LoadFavoritePage( 1, callback );
378 | }
379 | else
380 | {
381 | let errorMsg = `${response.status}: ${response.statusText}`;
382 | Promise.reject( new Error( errorMsg ) );
383 | console.error( `Error while loading doujinshi count (${errorMsg})` );
384 | }
385 | } ).
386 | catch( ( error ) =>
387 | {
388 | console.error( `Error while loading doujinshi count (${error})` );
389 |
390 | callback( false, error );
391 | } );
392 | }
393 |
394 | /// Load one page of favorite into storage
395 | function LoadFavoritePage( pageNumber, callback )
396 | {
397 | let target = "https://nhentai.net/favorites/?page=" + pageNumber;
398 | console.log( "Fetch", target );
399 |
400 | fetch( target )
401 | .then( ( response ) =>
402 | {
403 | if( response.status === 200 )
404 | return response.text();
405 | else
406 | Promise.reject( new Error( response.statusText ) );
407 | } )
408 | .then( ( text ) =>
409 | {
410 | let books = GetDoujinshisFromHtml( text );
411 | books.forEach( ( book ) =>
412 | {
413 | g_FavAdded++;
414 | SetBookInfo( book );
415 | SetCoverState( book.id, STATE_FAV );
416 | } );
417 |
418 | if( books.length > 0 )
419 | LoadFavoritePage( pageNumber + 1, callback );
420 | else
421 | callback( true, g_FavAdded );
422 | } )
423 | .catch( ( error ) =>
424 | {
425 | console.error( "Error while loading favorites page " + pageNumber + " (Code " + error + ")." );
426 | } );
427 |
428 | }
429 |
430 | /// Get all doujinshis that are in a page, return an array of Doujinshi
431 | function GetDoujinshisFromHtml( html )
432 | {
433 | let currDoujinshis = [];
434 | html.split( '