@@ -3,15 +3,18 @@ import {
33 domParse ,
44 fileType ,
55 log ,
6+ onUrlChange ,
67 querySelector ,
78 querySelectorAll ,
89 scrollIntoView ,
910 singleThreaded ,
1011 t ,
1112 useStyle ,
13+ wait ,
1214} from 'helper' ;
1315import { ReactiveSet , request , toast , useInit } from 'main' ;
1416import { getAdPageByContent } from 'userscript/detectAd' ;
17+ import { getNhentaiData , getNhentaiImageUrl } from '../userscript/nhentaiApi' ;
1518
1619type Images = {
1720 thumbnail : { t : keyof typeof fileType ; w : number ; h : number } ;
@@ -31,47 +34,76 @@ declare const _gallery: { num_pages: number; media_id: string; images: Images };
3134 detect_ad : true ,
3235 } ) ;
3336
34- // 在漫画详情页
35- if ( Reflect . has ( unsafeWindow , 'gallery' ) ) {
37+ let currentGalleryId : string | null = null ;
38+
39+ const removeComicReadMode = ( ) => {
40+ querySelector ( '#comicReadMode' ) ?. remove ( ) ;
41+ } ;
42+
43+ const createComicReadMode = ( ) => {
44+ const el = document . createElement ( 'a' ) ;
45+ el . href = 'javascript:;' ;
46+ el . id = 'comicReadMode' ;
47+ el . className = 'btn btn-secondary' ;
48+ el . addEventListener ( 'click' , showComic ) ;
49+ el . innerHTML = '<i class="fa fa-book"></i> Read' ;
50+ return el ;
51+ } ;
52+
53+ const setupDetailPage = async ( id : string ) => {
54+ if ( currentGalleryId === id ) return ;
55+ currentGalleryId = id ;
56+
57+ removeComicReadMode ( ) ;
58+
3659 setState ( 'manga' , {
3760 onExit ( isEnd ) {
3861 if ( isEnd ) scrollIntoView ( '#comment-container' ) ;
3962 setState ( 'manga' , 'show' , false ) ;
4063 } ,
4164 } ) ;
4265
43- // nh 自己是每张图随机选一个 cdn,但反正只是分流,简单点顺序分配应该也没问题吧
44- const cdn = unsafeWindow . _n_app . options . image_cdn_urls as string [ ] ;
45- const getImgList = ( ) =>
46- _gallery . images . pages . map ( ( { t, w : width , h : height } , i ) => {
47- const src = `https://${ cdn [ i % cdn . length ] } /galleries/${ _gallery . media_id } /${ i + 1 } .${ fileType [ t ] } ` ;
48- return { src, width, height } ;
49- } ) ;
50- setState ( 'comicMap' , '' , { getImgList } ) ;
66+ let gallery : any = null ;
67+ try {
68+ gallery = await getNhentaiData ( id ) ;
69+ } catch ( error ) {
70+ log . error ( 'nhentai getNhentaiData failed' , error ) ;
71+ }
72+
73+ if ( gallery ) {
74+ const getImgList = ( ) =>
75+ gallery . pages . map ( ( page : any , i : number ) => ( {
76+ src : getNhentaiImageUrl ( gallery , i ) ,
77+ width : page . width ,
78+ height : page . height ,
79+ } ) ) ;
80+
81+ setState ( 'comicMap' , '' , { getImgList } ) ;
82+ }
83+
5184 setState ( 'fab' , 'initialShow' , options . autoShow ) ;
5285
53- const comicReadModeDom = (
54- < a
55- href = "javascript:;"
56- id = "comicReadMode"
57- class = "btn btn-secondary"
58- onClick = { ( ) => showComic ( ) }
59- >
60- { /* eslint-disable-next-line i18next/no-literal-string */ }
61- < i class = "fa fa-book" /> Read
62- </ a >
63- ) as HTMLAnchorElement ;
64- document . getElementById ( 'download' ) ! . after ( comicReadModeDom ) ;
65-
66- const enableDetectAd =
67- options . detect_ad && querySelector ( '#tags .tag.tag-144644' ) ;
86+ const comicReadModeDom = createComicReadMode ( ) ;
87+ const downloadBtn = document . getElementById ( 'download' ) ;
88+ if ( downloadBtn ) downloadBtn . after ( comicReadModeDom ) ;
89+ else document . body . append ( comicReadModeDom ) ;
90+
91+ const shouldDetectAd = options . detect_ad ;
92+ const adTagSelector = '#tags a[href="/tag/extraneous-ads/"]' ;
93+ const detectedTag = shouldDetectAd
94+ ? await wait (
95+ ( ) => querySelector ( adTagSelector ) ,
96+ 1000 ,
97+ )
98+ : null ;
99+ const enableDetectAd = Boolean ( detectedTag ) ;
68100 if ( enableDetectAd ) {
69101 setState ( 'comicMap' , '' , 'adList' , new ReactiveSet ( ) ) ;
70102
71103 // 先使用缩略图识别
72104 await getAdPageByContent (
73105 querySelectorAll < HTMLImageElement > ( '.thumb-container img' ) . map (
74- ( img ) => img . dataset . src ,
106+ ( img ) => img . dataset . src || img . src ,
75107 ) ,
76108 store . comicMap [ '' ] . adList ! ,
77109 ) ;
@@ -100,92 +132,111 @@ declare const _gallery: { num_pages: number; media_id: string; images: Images };
100132 return styleList . join ( '\n' ) ;
101133 } ) ;
102134 }
135+ } ;
103136
104- return ;
105- }
137+ const setupListPage = async ( ) => {
138+ currentGalleryId = null ;
139+ removeComicReadMode ( ) ;
140+
141+ // 在漫画浏览页
142+ if ( ! document . getElementsByClassName ( 'gallery' ) . length ) return ;
143+ if ( location . pathname . startsWith ( '/g/' ) ) return ;
106144
107- // 在漫画浏览页
108- if ( document . getElementsByClassName ( 'gallery' ) . length > 0 ) {
109145 if ( options . open_link_new_page )
110146 for ( const e of querySelectorAll ( 'a:not([href^="javascript:"])' ) )
111147 e . setAttribute ( 'target' , '_blank' ) ;
112148
113- const blacklist : number [ ] = ( unsafeWindow ?. _n_app ?? unsafeWindow ?. n )
114- ?. options ?. blacklisted_tags ;
115- if ( blacklist === undefined )
149+ const app = await wait (
150+ ( ) => ( window as any ) . _n_app ?? ( window as any ) . n ,
151+ 2000 ,
152+ 100 ,
153+ ) ;
154+ const blacklist : number [ ] | null | undefined = app ?. options ?. blacklisted_tags ;
155+
156+ if ( typeof blacklist === 'undefined' && app !== undefined )
116157 toast . error ( t ( 'site.nhentai.tag_blacklist_fetch_failed' ) ) ;
117- // blacklist === null 时是未登录
118158
119- if ( options . block_totally && blacklist ? .length )
159+ if ( options . block_totally && Array . isArray ( blacklist ) && blacklist . length )
120160 useStyle ( '.blacklisted.gallery { display: none; }' ) ;
121161
122- if ( options . auto_page_turn ) {
123- let nextUrl = querySelector < HTMLAnchorElement > ( 'a.next' ) ?. href ;
124- if ( ! nextUrl ) return ;
125-
126- useStyle ( `
127- hr { bottom: 1px; box-sizing: border-box; margin: -1em auto 2em; }
128- hr:last-child { position: relative; animation: load .8s linear alternate infinite; }
129- hr:not(:last-child) { display: none; }
130- @keyframes load { 0% { width: 100%; } 100% { width: 0; } }
131- ` ) ;
132-
133- const blackSet = new Set ( blacklist ) ;
134- const contentDom = document . getElementById ( 'content' ) ! ;
135- const getObserveDom = ( ) =>
136- contentDom . querySelector (
137- ':is(.index-container, #favcontainer):last-of-type' ,
162+ const blackSet = new Set ( Array . isArray ( blacklist ) ? blacklist : [ ] ) ;
163+ const contentDom = document . getElementById ( 'content' ) ! ;
164+ let nextUrl = querySelector < HTMLAnchorElement > ( 'a.next' ) ?. href ;
165+ const getObserveDom = ( ) =>
166+ contentDom . querySelector (
167+ ':is(.index-container, #favcontainer):last-of-type' ,
168+ ) ! ;
169+
170+ const loadNextPage = singleThreaded (
171+ async ( ) : Promise < void > => {
172+ if ( ! nextUrl ) return ;
173+
174+ const res = await request ( nextUrl , {
175+ fetch : true ,
176+ errorText : t ( 'site.nhentai.fetch_next_page_failed' ) ,
177+ } ) ;
178+ const html = domParse ( res . responseText ) ;
179+ history . replaceState ( null , '' , nextUrl ) ;
180+
181+ const container = html . querySelector (
182+ '.index-container, #favcontainer' ,
138183 ) ! ;
139-
140- const loadNextPage = singleThreaded (
141- async ( ) : Promise < void > => {
142- if ( ! nextUrl ) return ;
143-
144- const res = await request ( nextUrl , {
145- fetch : true ,
146- errorText : t ( 'site.nhentai.fetch_next_page_failed' ) ,
147- } ) ;
148- const html = domParse ( res . responseText ) ;
149- history . replaceState ( null , '' , nextUrl ) ;
150-
151- const container = html . querySelector (
152- '.index-container, #favcontainer' ,
153- ) ! ;
154- for ( const galleryDom of container . querySelectorAll < HTMLElement > (
155- '.gallery' ,
156- ) ) {
157- for ( const img of galleryDom . getElementsByTagName ( 'img' ) )
158- img . setAttribute ( 'src' , img . dataset . src ! ) ;
159-
160- // 判断是否有黑名单标签
161- const tags = galleryDom . dataset . tags ! . split ( ' ' ) . map ( Number ) ;
162- if ( tags . some ( ( tag ) => blackSet . has ( tag ) ) )
163- galleryDom . classList . add ( 'blacklisted' ) ;
184+ for ( const galleryDom of container . querySelectorAll < HTMLElement > (
185+ '.gallery' ,
186+ ) ) {
187+ for ( const img of galleryDom . getElementsByTagName ( 'img' ) ) {
188+ const src = img . dataset . src || img . src ;
189+ if ( src ) img . setAttribute ( 'src' , src ) ;
164190 }
165191
166- const pagination = html . querySelector < HTMLElement > ( '.pagination' ) ! ;
167- nextUrl = pagination . querySelector < HTMLAnchorElement > ( 'a.next' ) ?. href ;
192+ const tags = galleryDom . dataset . tags ! . split ( ' ' ) . map ( Number ) ;
193+ if ( tags . some ( ( tag ) => blackSet . has ( tag ) ) )
194+ galleryDom . classList . add ( 'blacklisted' ) ;
195+ }
168196
169- contentDom . append ( container , pagination ) ;
197+ const pagination = html . querySelector < HTMLElement > ( '.pagination' ) ! ;
198+ nextUrl = pagination . querySelector < HTMLAnchorElement > ( 'a.next' ) ?. href ;
170199
171- const hr = document . createElement ( 'hr' ) ;
172- contentDom . append ( hr ) ;
173- observer . disconnect ( ) ;
174- observer . observe ( getObserveDom ( ) ) ;
175- if ( ! nextUrl ) hr . style . animationPlayState = 'paused' ;
176- } ,
177- { abandon : true } ,
178- ) ;
179-
180- loadNextPage ( ) ;
200+ contentDom . append ( container , pagination ) ;
181201
182- const observer = new IntersectionObserver (
183- ( entries ) => entries [ 0 ] . isIntersecting && loadNextPage ( ) ,
184- ) ;
185- observer . observe ( getObserveDom ( ) ) ;
186-
187- if ( querySelector ( 'section.pagination' ) )
188- contentDom . append ( document . createElement ( 'hr' ) ) ;
202+ const hr = document . createElement ( 'hr' ) ;
203+ contentDom . append ( hr ) ;
204+ observer . disconnect ( ) ;
205+ observer . observe ( getObserveDom ( ) ) ;
206+ if ( ! nextUrl ) hr . style . animationPlayState = 'paused' ;
207+ } ,
208+ { abandon : true } ,
209+ ) ;
210+
211+ loadNextPage ( ) ;
212+
213+ const observer = new IntersectionObserver (
214+ ( entries ) => entries [ 0 ] . isIntersecting && loadNextPage ( ) ,
215+ ) ;
216+ observer . observe ( getObserveDom ( ) ) ;
217+
218+ if ( querySelector ( 'section.pagination' ) )
219+ contentDom . append ( document . createElement ( 'hr' ) ) ;
220+ } ;
221+
222+ const processPage = async ( ) => {
223+ const galleryPathMatch = location . pathname . match ( / ^ \/ g \/ ( \d + ) / ) ;
224+ if ( galleryPathMatch ) {
225+ await setupDetailPage ( galleryPathMatch [ 1 ] ) ;
226+ } else {
227+ await setupListPage ( ) ;
228+ }
229+ } ;
230+
231+ await processPage ( ) ;
232+ onUrlChange ( async ( _ , nowUrl ) => {
233+ const match = new URL ( nowUrl ) . pathname . match ( / ^ \/ g \/ ( \d + ) / ) ;
234+ if ( match ) {
235+ await setupDetailPage ( match [ 1 ] ) ;
236+ } else {
237+ await setupListPage ( ) ;
189238 }
190- }
239+ } ) ;
240+
241+ return ;
191242} ) ( ) . catch ( ( error ) => log . error ( error ) ) ;
0 commit comments