Skip to content

Commit be9c103

Browse files
committed
1 parent 8437404 commit be9c103

8 files changed

Lines changed: 257 additions & 124 deletions

File tree

src/components/Manga/actions/imageSize.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createEffectOn } from 'helper';
33
import type { State } from '../store';
44

55
import { setState, store } from '../store';
6+
import { withOptionalState } from './helper';
67
import { updateImgType } from './imageType';
78
import {
89
abreastColumnWidth,
@@ -41,15 +42,16 @@ export const getImgDisplaySize = (
4142
};
4243

4344
/** 更新图片尺寸 */
44-
export const updateImgSize = (url: string, width: number, height: number) =>
45-
setState((state) => {
45+
export const updateImgSize = withOptionalState(
46+
(url: string, width: number, height: number, state: State) => {
4647
const img = state.imgMap[url];
4748
if (img.width === width && img.height === height) return;
4849
img.width = width;
4950
img.height = height;
5051
img.size = getImgDisplaySize(state, img);
5152
updateImgType(state, img);
52-
});
53+
},
54+
);
5355

5456
createEffectOn(
5557
[

src/components/Manga/actions/readProgress.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export const resumeReadProgress = async (state: State) => {
6868
let i = state.imgList.length;
6969
while (i--) {
7070
const imgSize = progress.imgSize[i];
71-
if (imgSize) updateImgSize(state.imgList[i], ...imgSize);
71+
if (imgSize) updateImgSize(state.imgList[i], ...imgSize, state);
7272
}
7373
state.fillEffect = progress.fillEffect;
7474
updatePageData(state);

src/components/Manga/hooks/useInit.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,22 +179,19 @@ export const useInit = (props: MangaProps) => {
179179
}
180180

181181
const oldImgList = new Set(state.imgList);
182-
183182
if (oldImgList.size === 0 && newImgList.length > 0) {
184183
resumeReadProgress(state);
185184
updateSelfhostedOptions(true);
186185
}
187186

188187
/** 被删除的图片 */
189-
const deleteList = [...oldImgList].filter(
190-
(url) => !newImgList.includes(url),
191-
);
188+
const deleteList = oldImgList.difference(new Set(newImgList));
192189
for (const url of deleteList)
193190
if (state.imgMap[url].blobUrl && state.imgMap[url].blobUrl !== url)
194191
URL.revokeObjectURL(state.imgMap[url].blobUrl);
195192

196193
/** 删除图片数 */
197-
const deleteNum = deleteList.length;
194+
const deleteNum = deleteList.size;
198195

199196
/** 传入的是否是新漫画 */
200197
const isNew = deleteNum >= oldImgList.size * 0.8; // 删掉8成图就算是新漫画

src/helper/other.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ export function range<T = number>(
8686
}
8787
}
8888

89+
/** 判断节点是否为元素节点 */
90+
export const isHTMLElement = (node: Node): node is HTMLElement =>
91+
node.nodeType === Node.ELEMENT_NODE;
92+
93+
/** 判断节点是否为图片元素节点 */
94+
export const isImageElement = (node: Node): node is HTMLImageElement =>
95+
node.nodeName === 'IMG';
96+
8997
/**
9098
* 对 document.querySelector 的封装
9199
* 将默认返回类型改为 HTMLElement

src/site/ehentai/detectAd.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { createRootMemo, querySelectorAll, useStyle } from 'helper';
1+
import {
2+
createRootMemo,
3+
isImageElement,
4+
querySelectorAll,
5+
useStyle,
6+
} from 'helper';
27
import { ReactiveSet } from 'main';
38
import { getAdPageByContent, getAdPageByFileName } from 'userscript/detectAd';
49

@@ -31,8 +36,7 @@ export const detectAd = ({
3136

3237
const thumbnail = e.querySelector<HTMLElement>('[title]')!;
3338
[, fileNameList[index]] = thumbnail.title.split(/|: /);
34-
if (thumbnail.tagName === 'IMG')
35-
thumbnailList[index] = thumbnail as HTMLImageElement;
39+
if (isImageElement(thumbnail)) thumbnailList[index] = thumbnail;
3640
if (thumbnail.style.background.includes('url('))
3741
thumbnailList[index] = await extractSpriteImage(thumbnail.style);
3842
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { isHTMLElement, isImageElement } from 'helper';
2+
3+
/**
4+
* 图片尺寸及相关信息
5+
*/
6+
export type ImageInfo = {
7+
display: { width: number; height: number };
8+
natural: { width: number; height: number };
9+
/** 图片在文档里的绝对高度 */
10+
top: number;
11+
};
12+
13+
/**
14+
* 配置选项接口
15+
*/
16+
export type ImageWatcherOptions = {
17+
/**
18+
* 判断图片是否符合条件的过滤器
19+
* @param img 图片元素
20+
* @param display 显示尺寸
21+
* @param natural 原始尺寸
22+
*/
23+
filter: (info: ImageInfo, img: HTMLImageElement) => boolean;
24+
25+
/** 当符合条件的图片集合发生变化时触发的回调 */
26+
onChanged: (map: Map<HTMLImageElement, ImageInfo>) => void;
27+
};
28+
29+
/** 监听网页上的所有图片元素的变化,筛选出符合条件的图片 */
30+
export class ImageWatcher {
31+
private options: ImageWatcherOptions;
32+
33+
private ro: ResizeObserver;
34+
35+
private mo: MutationObserver;
36+
37+
// 记录已经符合条件的图片元素及其尺寸信息
38+
// 注意:如果图片的 src 发生改变,我们会将其从这里移除,重新进行检查
39+
private qualifiedMap = new Map<HTMLImageElement, ImageInfo>();
40+
41+
// 需要监听的属性列表,涵盖了常见的懒加载属性
42+
private readonly targetAttributes = [
43+
'src',
44+
'srcset',
45+
'data-src',
46+
'data-original',
47+
'data-srcset',
48+
];
49+
50+
constructor(options: ImageWatcherOptions) {
51+
this.options = options;
52+
this.ro = new ResizeObserver(this.handleResize);
53+
this.mo = new MutationObserver(this.handleMutation);
54+
}
55+
56+
public start(): void {
57+
// 监视页面当前所有图片,确保脚本加载前已经存在的图片也被处理
58+
for (const e of document.querySelectorAll('img')) this.observeImage(e);
59+
60+
this.mo.observe(document.body, {
61+
childList: true, // 监听节点增删
62+
subtree: true, // 监听所有子孙节点
63+
attributes: true, // 监听属性变化
64+
attributeFilter: this.targetAttributes, // 只监听特定的图片相关属性
65+
});
66+
}
67+
68+
/** 停止监听并清理资源 */
69+
public stop(): void {
70+
this.mo.disconnect();
71+
this.ro.disconnect();
72+
this.qualifiedMap.clear();
73+
}
74+
75+
/** 使用 ResizeObserver 监测图片尺寸变化 */
76+
private observeImage = (img: HTMLImageElement) => this.ro.observe(img);
77+
78+
/** 处理 ResizeObserver 的回调,只有在图片尺寸发生实际变化(或初始化)时才会触发 */
79+
private handleResize = (entries: ResizeObserverEntry[]): void => {
80+
let changed = false;
81+
82+
for (const entry of entries) {
83+
const img = entry.target as HTMLImageElement;
84+
85+
const imageInfo: ImageInfo = {
86+
display: {
87+
width: entry.contentRect.width,
88+
height: entry.contentRect.height,
89+
},
90+
natural: { width: img.naturalWidth, height: img.naturalHeight },
91+
top: 0,
92+
};
93+
94+
// oxlint-disable-next-line no-array-method-this-argument
95+
if (this.qualifiedMap.has(img) || !this.options.filter(imageInfo, img))
96+
continue;
97+
98+
imageInfo.top = img.getBoundingClientRect().top + window.scrollY;
99+
this.qualifiedMap.set(img, imageInfo);
100+
changed = true;
101+
102+
// 符合条件后停止监听尺寸变化
103+
// 如果之后 src 发生改变,会被 MO 捕获并重新 observe
104+
this.ro.unobserve(img);
105+
}
106+
107+
if (changed) this.options.onChanged(this.qualifiedMap);
108+
};
109+
110+
/**
111+
* 遍历节点及其子树中的所有图片元素
112+
*/
113+
private forEachImage(
114+
nodes: NodeList,
115+
callback: (img: HTMLImageElement) => void,
116+
): void {
117+
for (const node of nodes) {
118+
if (isImageElement(node)) callback(node);
119+
else if (isHTMLElement(node))
120+
for (const img of node.querySelectorAll('img')) callback(img);
121+
}
122+
}
123+
124+
/**
125+
* 处理 MutationObserver 的回调
126+
* 负责发现新元素和属性变化
127+
*/
128+
private handleMutation = (mutations: MutationRecord[]): void => {
129+
let changed = false;
130+
const deleteImg = (img: HTMLImageElement) => {
131+
if (!this.qualifiedMap.has(img)) return;
132+
this.qualifiedMap.delete(img);
133+
changed = true;
134+
};
135+
136+
for (const mutation of mutations) {
137+
switch (mutation.type) {
138+
case 'childList': {
139+
this.forEachImage(mutation.addedNodes, this.observeImage);
140+
this.forEachImage(mutation.removedNodes, deleteImg);
141+
break;
142+
}
143+
144+
case 'attributes': {
145+
const node = mutation.target;
146+
// 图片的 src 变了以后,要将其视为一张新图来看待
147+
if (isImageElement(node)) {
148+
deleteImg(node);
149+
this.observeImage(node);
150+
}
151+
break;
152+
}
153+
}
154+
}
155+
156+
if (changed) this.options.onChanged(this.qualifiedMap);
157+
};
158+
}

0 commit comments

Comments
 (0)