Skip to content

Commit 0b9d057

Browse files
committed
refactor: ♻️ 重构 ehentai 相关
1 parent 858cb28 commit 0b9d057

13 files changed

Lines changed: 331 additions & 287 deletions

src/site/ehentai/colorizeTag.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { debounce, getGmValue, hijackFn } from 'helper';
22

33
import { updateMyTags, handleMyTagsChange, type Tag } from './myTags';
4-
5-
import { type PageType } from '.';
4+
import { type EhContext } from './context';
65

76
// 为每个标签单独生成 css。用于方便调试时排查和修改样式时使用
87
// const buildTagColorCss = (
@@ -80,10 +79,10 @@ export const updateTagColor = async (tagList: Tag[]) => {
8079
};
8180

8281
/** 标签染色 */
83-
export const colorizeTag = async (pageType: PageType) => {
82+
export const colorizeTag = async (context: EhContext) => {
8483
handleMyTagsChange.add(updateTagColor);
8584

86-
switch (pageType) {
85+
switch (context.type) {
8786
case 'gallery': {
8887
let css =
8988
location.origin === 'https://exhentai.org'

src/site/ehentai/context.tsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { querySelector } from 'helper';
2+
import { createMemo, type Component } from 'solid-js';
3+
import { request, useInit } from 'main';
4+
5+
export const listPageTypes = [
6+
'm', // 最小化
7+
'p', // 最小化 + 关注标签
8+
'l', // 紧凑 + 标签
9+
'e', // 扩展
10+
't', // 缩略图;
11+
] as const;
12+
13+
type ListPageType = (typeof listPageTypes)[number];
14+
15+
export type PageType = 'gallery' | 'mytags' | 'mpv' | ListPageType;
16+
17+
export type GalleryContext = {
18+
type: 'gallery';
19+
galleryId: number;
20+
galleryTitle: string | undefined;
21+
japanTitle: string | undefined;
22+
imgNum: number;
23+
24+
imgList: string[];
25+
pageList: string[];
26+
fileNameList: string[];
27+
28+
/** 放在原生右侧工具栏和标签选项里的漫画加载按钮 */
29+
LoadButton: Component<{
30+
id: string;
31+
onClick?: (e: MouseEvent) => unknown;
32+
}>;
33+
dom: {
34+
/** 标签输入框 */
35+
newTagField: HTMLInputElement;
36+
};
37+
} & AsyncReturnType<typeof useInit>;
38+
39+
type OtherContext = { type: Exclude<PageType, 'gallery'> } & AsyncReturnType<
40+
typeof useInit
41+
>;
42+
43+
export type EhContext = OtherContext | GalleryContext;
44+
45+
export const createEhContext = async (
46+
options: Record<string, any>,
47+
): Promise<EhContext | null> => {
48+
let type: PageType | undefined;
49+
if (Reflect.has(unsafeWindow, 'display_comment_field')) type = 'gallery';
50+
else if (location.pathname === '/mytags') type = 'mytags';
51+
else if (Reflect.has(unsafeWindow, 'mpvkey')) type = 'mpv';
52+
else
53+
type = (
54+
querySelector('option[value="t"]')?.parentElement as HTMLSelectElement
55+
)?.value as Exclude<PageType, 'gallery'> | undefined;
56+
57+
if (!type) return null;
58+
const fnMap = await useInit('ehentai', options);
59+
60+
if (type !== 'gallery') return { type, ...fnMap };
61+
62+
let imgNum = 0;
63+
imgNum = Number(
64+
querySelector('.gtb .gpc')
65+
?.textContent?.replaceAll(',', '')
66+
.match(/\d+/g)
67+
?.at(-1),
68+
);
69+
if (Number.isNaN(imgNum)) {
70+
imgNum = Number(
71+
/(?<=<td class="gdt2">)\d+(?= pages<\/td>)/.exec(
72+
(await request(window.location.href)).responseText,
73+
)?.[0],
74+
);
75+
}
76+
77+
return {
78+
type: 'gallery',
79+
...fnMap,
80+
galleryId: Number(location.pathname.split('/')[2]),
81+
galleryTitle: querySelector('#gn')?.textContent || undefined,
82+
japanTitle: querySelector('#gj')?.textContent || undefined,
83+
imgNum,
84+
85+
imgList: [],
86+
pageList: [],
87+
fileNameList: [],
88+
89+
LoadButton(props) {
90+
const tip = createMemo(() => {
91+
const _imgList = fnMap.comicMap[props.id]?.imgList;
92+
const progress = _imgList?.filter(Boolean).length;
93+
94+
switch (_imgList?.length) {
95+
case undefined:
96+
return ' Load comic';
97+
case progress:
98+
return ' Read';
99+
default:
100+
return ` loading - ${progress}/${_imgList!.length}`;
101+
}
102+
});
103+
return (
104+
<a
105+
href="javascript:;"
106+
onClick={async (e) => {
107+
await props.onClick?.(e);
108+
fnMap.showComic(props.id);
109+
}}
110+
children={tip()}
111+
/>
112+
);
113+
},
114+
dom: {
115+
newTagField: document.getElementById('newtagfield') as HTMLInputElement,
116+
},
117+
};
118+
};

src/site/ehentai/crossSiteLink.tsx

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { request, type useInit, toast } from 'main';
1+
import { request, toast } from 'main';
22
import { t, querySelector, plimit, hijackFn, querySelectorAll } from 'helper';
33
import { For, Show, type Component, type JSX } from 'solid-js';
44
import { render } from 'solid-js/web';
55
import { createStore } from 'solid-js/store';
66

7+
import { type GalleryContext } from './context';
8+
79
type ItemData = {
810
id: string;
911
title: string;
@@ -12,11 +14,11 @@ type ItemData = {
1214
class: string;
1315
};
1416

15-
const nhentai = async (
16-
setComicLoad: AsyncReturnType<typeof useInit>['setComicLoad'],
17-
dynamicLoad: AsyncReturnType<typeof useInit>['dynamicLoad'],
18-
comicTitle: string,
19-
): Promise<ItemData[]> => {
17+
const nhentai = async ({
18+
galleryTitle,
19+
setComicLoad,
20+
dynamicLoad,
21+
}: GalleryContext): Promise<ItemData[]> => {
2022
interface ComicInfo {
2123
id: number;
2224
media_id: string;
@@ -41,7 +43,7 @@ const nhentai = async (
4143
const {
4244
response: { result },
4345
} = await request<{ result: ComicInfo[] }>(
44-
`https://nhentai.net/api/galleries/search?query=${comicTitle}`,
46+
`https://nhentai.net/api/galleries/search?query=${galleryTitle}`,
4547
{
4648
responseType: 'json',
4749
errorText: t('site.ehentai.nhentai_error'),
@@ -90,29 +92,28 @@ const nhentai = async (
9092
};
9193
});
9294
};
93-
nhentai.errorTip = (comicTitle: string) =>
95+
nhentai.errorTip = (context: GalleryContext) =>
9496
t('site.ehentai.nhentai_failed', {
95-
nhentai: `<a href='https://nhentai.net/search/?q=${comicTitle}' target="_blank"> <u> nhentai </u> </a>`,
97+
nhentai: `<a href='https://nhentai.net/search/?q=${context.galleryTitle}' target="_blank"> <u> nhentai </u> </a>`,
9698
});
9799

98-
const hitomi = async (
99-
setComicLoad: AsyncReturnType<typeof useInit>['setComicLoad'],
100-
dynamicLoad: AsyncReturnType<typeof useInit>['dynamicLoad'],
101-
): Promise<ItemData[]> => {
102-
const comicId = location.pathname.split('/')[2];
103-
100+
const hitomi = async ({
101+
setComicLoad,
102+
dynamicLoad,
103+
galleryId,
104+
}: GalleryContext): Promise<ItemData[]> => {
104105
const domain = 'gold-usergeneratedcontent.net';
105106

106107
const downImg = async (url: string) => {
107108
const imgRes = await request<Blob>(url, {
108-
headers: { Referer: `https://hitomi.la/reader/${comicId}.html` },
109+
headers: { Referer: `https://hitomi.la/reader/${galleryId}.html` },
109110
responseType: 'blob',
110111
fetch: false,
111112
});
112113
return URL.createObjectURL(imgRes.response);
113114
};
114115

115-
const res = await request(`https://ltn.${domain}/galleries/${comicId}.js`, {
116+
const res = await request(`https://ltn.${domain}/galleries/${galleryId}.js`, {
116117
errorText: t('site.ehentai.hitomi_error'),
117118
noTip: true,
118119
});
@@ -146,8 +147,7 @@ const hitomi = async (
146147
const imageId = gg.s(hash);
147148
const m = /[\da-f]{61}([\da-f]{2})([\da-f])/.exec(hash)!;
148149
const g = Number.parseInt(m[2] + m[1], 16);
149-
const subDomainOffset = gg.m(g) + 1;
150-
return `https://w${subDomainOffset}.${domain}/${gg.b}${imageId}/${hash}.webp`;
150+
return `https://w${gg.m(g) + 1}.${domain}/${gg.b}${imageId}/${hash}.webp`;
151151
});
152152

153153
// 顺序下载避免触发反爬限制
@@ -172,22 +172,11 @@ const hitomi = async (
172172
hitomi.errorTip = () => t('site.ehentai.hitomi_error');
173173

174174
/** 关联外站 */
175-
export const crossSiteLink = async (
176-
dynamicLoad: AsyncReturnType<typeof useInit>['dynamicLoad'],
177-
setComicLoad: AsyncReturnType<typeof useInit>['setComicLoad'],
178-
LoadButton: Component<{ id: string }>,
179-
) => {
175+
export const crossSiteLink = async (context: GalleryContext) => {
180176
/** 只处理「Doujinshi」「Manga」 */
181177
if (!querySelector('#gdc > .cs:is(.ct2, .ct3)')) return;
182-
183-
const titleDom = document.getElementById('gn');
184-
if (!titleDom || !querySelector('#taglist tbody')) {
185-
if ((document.getElementById('taglist')?.children.length ?? 1) > 0)
186-
toast.error(t('site.ehentai.html_changed_link_failed'));
187-
return;
188-
}
189-
190-
const comicTitle = titleDom.textContent!.replaceAll(/\s+-/g, ' ');
178+
if (!context.galleryTitle)
179+
return toast.error(t('site.ehentai.html_changed_link_failed'));
191180

192181
const [comicMap, setComicMap] = createStore<
193182
Record<string, ItemData[] | string>
@@ -262,7 +251,7 @@ export const crossSiteLink = async (
262251
() => (
263252
<TagMenu>
264253
<a href={a.href} target="_blank" innerText=" Jump" />
265-
<LoadButton id={a.id} />
254+
<context.LoadButton id={a.id} />
266255
</TagMenu>
267256
),
268257
tagmenu_act_dom,
@@ -273,17 +262,21 @@ export const crossSiteLink = async (
273262
for (const getSiteComic of [hitomi, nhentai]) {
274263
setComicMap(getSiteComic.name, 'searching...');
275264
try {
276-
const itemList = await getSiteComic(
277-
setComicLoad,
278-
dynamicLoad,
279-
comicTitle,
280-
);
265+
const itemList = await getSiteComic(context);
281266
if (itemList.length > 0) setComicMap(getSiteComic.name, itemList);
282267
else setComicMap(getSiteComic.name, 'null');
283268
} catch (error) {
284-
const errorTip = getSiteComic.errorTip(comicTitle);
269+
const errorTip = getSiteComic.errorTip(context);
285270
console.error(errorTip, error);
286271
setComicMap(getSiteComic.name, errorTip);
287272
}
288273
}
274+
275+
const { adList } = context.comicMap[''];
276+
if (!adList) return;
277+
// 如果外站源只匹配到了一个漫画,就直接为其加上当前识别出的广告列表
278+
for (const itemList of Object.values(comicMap)) {
279+
if (typeof itemList === 'string') continue;
280+
if (itemList.length === 1) context.setComicMap(itemList[0].id, { adList });
281+
}
289282
};

src/site/ehentai/detectAd.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { ReactiveSet } from 'main';
2+
import { getAdPageByFileName, getAdPageByContent } from 'userscript/detectAd';
3+
import { querySelectorAll, useStyle, createRootMemo } from 'helper';
4+
5+
import type { GalleryContext } from './context';
6+
7+
/** 识别广告 */
8+
export const detectAd = async ({
9+
setComicMap,
10+
options,
11+
comicMap,
12+
imgList,
13+
pageList,
14+
fileNameList,
15+
}: GalleryContext) => {
16+
const enableDetectAd =
17+
options.detect_ad && document.getElementById('ta_other:extraneous_ads');
18+
if (!enableDetectAd) return;
19+
20+
setComicMap('', { adList: new ReactiveSet() });
21+
22+
/** 缩略图列表 */
23+
const thumbnailList: Array<string | HTMLImageElement> = [];
24+
for (const e of querySelectorAll<HTMLAnchorElement>('#gdt > a')) {
25+
const index = Number(/.+-(\d+)/.exec(e.href)?.[1]) - 1;
26+
if (Number.isNaN(index)) continue;
27+
pageList[index] = e.href;
28+
29+
const thumbnail = e.querySelector<HTMLElement>('[title]')!;
30+
fileNameList[index] = thumbnail.title.split(/|: /)[1];
31+
thumbnailList[index] =
32+
thumbnail.tagName === 'IMG'
33+
? (thumbnail as HTMLImageElement)
34+
: /url\("(.+)"\)/.exec(thumbnail.style.backgroundImage)![1];
35+
}
36+
37+
(async () => {
38+
// 先根据文件名判断一次
39+
await getAdPageByFileName(fileNameList, comicMap[''].adList!);
40+
// 不行的话再用缩略图识别
41+
if (comicMap[''].adList!.size === 0)
42+
await getAdPageByContent(thumbnailList, comicMap[''].adList!);
43+
44+
// 模糊广告页的缩略图
45+
useStyle(
46+
createRootMemo(() => {
47+
if (!comicMap['']?.adList?.size) return '';
48+
return [...comicMap[''].adList]
49+
.map(
50+
(i) => `a[href="${pageList[i]}"] [title]:not(:hover) {
51+
filter: blur(8px);
52+
clip-path: border-box;
53+
backdrop-filter: blur(8px);
54+
}`,
55+
)
56+
.join('\n');
57+
}),
58+
);
59+
})();
60+
61+
// 返回在图片加载时检查图片的函数
62+
return () => {
63+
getAdPageByFileName(fileNameList, comicMap[''].adList!);
64+
getAdPageByContent(imgList, comicMap[''].adList!);
65+
};
66+
};

0 commit comments

Comments
 (0)