pervert-monkey 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/core/pervertmonkey.core.es.d.ts +391 -0
- package/dist/core/pervertmonkey.core.es.js +8497 -0
- package/dist/core/pervertmonkey.core.es.js.map +1 -0
- package/dist/core/pervertmonkey.core.umd.js +8500 -0
- package/dist/core/pervertmonkey.core.umd.js.map +1 -0
- package/dist/userscripts/3hentai.user.js +1176 -0
- package/dist/userscripts/camgirlfinder.user.js +68 -0
- package/dist/userscripts/camwhores.user.js +1602 -0
- package/dist/userscripts/e-hentai.user.js +1212 -0
- package/dist/userscripts/ebalka.user.js +1231 -0
- package/dist/userscripts/eporner.user.js +1265 -0
- package/dist/userscripts/erome.user.js +1245 -0
- package/dist/userscripts/eroprofile.user.js +1194 -0
- package/dist/userscripts/javhdporn.user.js +1178 -0
- package/dist/userscripts/missav.user.js +1182 -0
- package/dist/userscripts/motherless.user.js +1380 -0
- package/dist/userscripts/namethatporn.user.js +1218 -0
- package/dist/userscripts/nhentai.user.js +1262 -0
- package/dist/userscripts/pornhub.user.js +1199 -0
- package/dist/userscripts/spankbang.user.js +1239 -0
- package/dist/userscripts/xhamster.user.js +1374 -0
- package/dist/userscripts/xvideos.user.js +1254 -0
- package/package.json +54 -0
- package/src/core/data-control/data-filter.ts +143 -0
- package/src/core/data-control/data-manager.ts +144 -0
- package/src/core/data-control/index.ts +2 -0
- package/src/core/infinite-scroll/index.ts +143 -0
- package/src/core/jabroni-config/default-scheme.ts +97 -0
- package/src/core/jabroni-config/default-store.ts +9 -0
- package/src/core/pagination-parsing/index.ts +55 -0
- package/src/core/pagination-parsing/pagination-strategies/PaginationStrategy.ts +44 -0
- package/src/core/pagination-parsing/pagination-strategies/PaginationStrategyDataParams.ts +66 -0
- package/src/core/pagination-parsing/pagination-strategies/PaginationStrategyPathnameParams.ts +77 -0
- package/src/core/pagination-parsing/pagination-strategies/PaginationStrategySearchParams.ts +56 -0
- package/src/core/pagination-parsing/pagination-strategies/index.ts +4 -0
- package/src/core/pagination-parsing/pagination-utils/index.ts +84 -0
- package/src/core/rules/index.ts +385 -0
- package/src/index.ts +42 -0
- package/src/types/index.ts +7 -0
- package/src/userscripts/ascii-logos.js +468 -0
- package/src/userscripts/index.ts +1 -0
- package/src/userscripts/meta.json +11 -0
- package/src/userscripts/scripts/3hentai.ts +20 -0
- package/src/userscripts/scripts/camgirlfinder.ts +68 -0
- package/src/userscripts/scripts/camwhores.ts +382 -0
- package/src/userscripts/scripts/e-hentai.ts +68 -0
- package/src/userscripts/scripts/ebalka.ts +58 -0
- package/src/userscripts/scripts/eporner.ts +90 -0
- package/src/userscripts/scripts/erome.ts +105 -0
- package/src/userscripts/scripts/eroprofile.ts +38 -0
- package/src/userscripts/scripts/javhdporn.ts +24 -0
- package/src/userscripts/scripts/missav.ts +28 -0
- package/src/userscripts/scripts/motherless.ts +222 -0
- package/src/userscripts/scripts/namethatporn.ts +68 -0
- package/src/userscripts/scripts/nhentai.ts +135 -0
- package/src/userscripts/scripts/pornhub.ts +53 -0
- package/src/userscripts/scripts/spankbang.ts +61 -0
- package/src/userscripts/scripts/thisvid.ts +716 -0
- package/src/userscripts/scripts/xhamster.ts +179 -0
- package/src/userscripts/scripts/xvideos.ts +83 -0
- package/src/utils/arrays/index.ts +15 -0
- package/src/utils/async/index.ts +3 -0
- package/src/utils/dom/dom-observers.ts +76 -0
- package/src/utils/dom/index.ts +156 -0
- package/src/utils/events/index.ts +2 -0
- package/src/utils/events/on-pointer-over-and-leave.ts +35 -0
- package/src/utils/events/tick.ts +27 -0
- package/src/utils/fetch/index.ts +37 -0
- package/src/utils/math/index.ts +3 -0
- package/src/utils/objects/index.ts +9 -0
- package/src/utils/objects/memoize.ts +25 -0
- package/src/utils/observers/index.ts +44 -0
- package/src/utils/observers/lazy-image-loader.ts +27 -0
- package/src/utils/parsers/index.ts +30 -0
- package/src/utils/parsers/time-parser.ts +28 -0
- package/src/utils/strings/index.ts +10 -0
- package/src/utils/strings/regexes.ts +35 -0
- package/src/vite-env.d.ts +4 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { MonkeyUserScript } from 'vite-plugin-monkey';
|
|
2
|
+
import { unsafeWindow } from '$';
|
|
3
|
+
import { RulesGlobal } from '../../core/rules';
|
|
4
|
+
import {
|
|
5
|
+
exterminateVideo,
|
|
6
|
+
getCommonParents,
|
|
7
|
+
instantiateTemplate,
|
|
8
|
+
parseHtml,
|
|
9
|
+
waitForElementToAppear,
|
|
10
|
+
watchElementChildrenCount,
|
|
11
|
+
} from '../../utils/dom';
|
|
12
|
+
import { onPointerOverAndLeave } from '../../utils/events';
|
|
13
|
+
import { fetchJson } from '../../utils/fetch';
|
|
14
|
+
import { Observer } from '../../utils/observers';
|
|
15
|
+
|
|
16
|
+
export const meta: MonkeyUserScript = {
|
|
17
|
+
name: 'Xhamster Improved',
|
|
18
|
+
version: '5.0.0',
|
|
19
|
+
description: 'Infinite scroll [optional], Filter by Title and Duration',
|
|
20
|
+
match: ['https://*.xhamster.*/*', 'https://*.xhamster.com/*'],
|
|
21
|
+
exclude: 'https://*.xhamster.com/embed*',
|
|
22
|
+
icon: 'https://www.google.com/s2/favicons?sz=64&domain=xhamster.com',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const IS_VIDEO_PAGE = /^\/videos|moments\//.test(location.pathname);
|
|
26
|
+
const IS_PLAYLIST = /^\/my\/favorites\/videos\/\w+/.test(location.pathname);
|
|
27
|
+
|
|
28
|
+
function createThumb(data: Record<string, string>): string {
|
|
29
|
+
const attrsToReplace = {
|
|
30
|
+
href: data.pageURL,
|
|
31
|
+
'data-previewvideo': data.trailerURL,
|
|
32
|
+
'data-previewvideo-fallback': data.trailerFallbackUrl,
|
|
33
|
+
'data-sprite': data.spriteURL,
|
|
34
|
+
title: data.title,
|
|
35
|
+
'data-video-id': data.id,
|
|
36
|
+
srcset: data.thumbURL,
|
|
37
|
+
src: data.imageURL,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const text = {
|
|
41
|
+
'.video-thumb-views': data.views,
|
|
42
|
+
'[title]': data.title,
|
|
43
|
+
'[data-role="video-duration"] div': data.duration,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return instantiateTemplate('.video-thumb', attrsToReplace, text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getPaginationData: RulesGlobal['getPaginationData'] = !IS_PLAYLIST
|
|
50
|
+
? undefined
|
|
51
|
+
: async (url: string): Promise<HTMLElement> => {
|
|
52
|
+
const data = await fetchJson(url);
|
|
53
|
+
const thumbsHtml = (data as any).list
|
|
54
|
+
.map((e: Record<string, string>) => createThumb(e))
|
|
55
|
+
.join('\n');
|
|
56
|
+
return parseHtml(`<div>${thumbsHtml}</div>`);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function createPlaylistPaginationStrategy() {
|
|
60
|
+
const collectionId = location.pathname
|
|
61
|
+
.split('/my/favorites/videos/')[1]
|
|
62
|
+
.split('-')[0] as string;
|
|
63
|
+
const data = (unsafeWindow as any).initials;
|
|
64
|
+
const paginationLast = data.favoritesVideoPaging.maxPages;
|
|
65
|
+
const paginationOffset = data.favoritesVideoPaging.active;
|
|
66
|
+
|
|
67
|
+
const playlistPaginationStrategy: RulesGlobal['paginationStrategyOptions'] = {
|
|
68
|
+
paginationSelector: 'nav[class *= "pagination"]',
|
|
69
|
+
getPaginationLast: () => paginationLast,
|
|
70
|
+
getPaginationOffset: () => paginationOffset,
|
|
71
|
+
getPaginationUrlGenerator: () => (offset: number) => {
|
|
72
|
+
return `https://xhamster.com/api/front/favorite/get-playlist?id=${collectionId}&perPage=60&page=${offset}`;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return playlistPaginationStrategy;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const paginationStrategyOptionsDefault: RulesGlobal['paginationStrategyOptions'] = {
|
|
80
|
+
paginationSelector: '.prev-next-list, .test-pager',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const paginationStrategyOptions = IS_PLAYLIST
|
|
84
|
+
? createPlaylistPaginationStrategy()
|
|
85
|
+
: paginationStrategyOptionsDefault;
|
|
86
|
+
|
|
87
|
+
const rules = new RulesGlobal({
|
|
88
|
+
paginationStrategyOptions,
|
|
89
|
+
getPaginationData,
|
|
90
|
+
containerSelectorLast: '.thumb-list',
|
|
91
|
+
thumbsSelector: '.video-thumb',
|
|
92
|
+
titleSelector: '.video-thumb-info__name,.video-thumb-info>a',
|
|
93
|
+
durationSelector: '.thumb-image-container__duration',
|
|
94
|
+
gropeStrategy: 'all-in-all',
|
|
95
|
+
getThumbImgDataStrategy: 'auto',
|
|
96
|
+
getThumbImgDataAttrDelete: '[loading]',
|
|
97
|
+
customThumbDataSelectors: {
|
|
98
|
+
watched: {
|
|
99
|
+
type: 'boolean',
|
|
100
|
+
selector: '[data-role="video-watched',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
customDataSelectorFns: [
|
|
104
|
+
'filterInclude',
|
|
105
|
+
'filterExclude',
|
|
106
|
+
'filterDuration',
|
|
107
|
+
{
|
|
108
|
+
filterWatched: (el, state) => !!(state.filterWatched && el.watched),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
filterUnwatched: (el, state) => !!(state.filterUnwatched && !el.watched),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
schemeOptions: [
|
|
115
|
+
'Text Filter',
|
|
116
|
+
'Badge',
|
|
117
|
+
{
|
|
118
|
+
title: 'Filter Watched',
|
|
119
|
+
content: [
|
|
120
|
+
{ filterWatched: false, label: 'watched' },
|
|
121
|
+
{ filterUnwatched: false, label: 'unwatched' },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
'Duration Filter',
|
|
125
|
+
'Advanced',
|
|
126
|
+
],
|
|
127
|
+
animatePreview,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function animatePreview() {
|
|
131
|
+
function createPreviewVideoElement(src: string, mount: HTMLElement) {
|
|
132
|
+
const video = document.createElement('video');
|
|
133
|
+
video.playsInline = true;
|
|
134
|
+
video.autoplay = true;
|
|
135
|
+
video.loop = true;
|
|
136
|
+
video.classList.add('thumb-image-container__video');
|
|
137
|
+
video.src = src;
|
|
138
|
+
video.addEventListener(
|
|
139
|
+
'loadeddata',
|
|
140
|
+
() => {
|
|
141
|
+
mount.before(video);
|
|
142
|
+
},
|
|
143
|
+
false,
|
|
144
|
+
);
|
|
145
|
+
return () => exterminateVideo(video);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
onPointerOverAndLeave(
|
|
149
|
+
document.body,
|
|
150
|
+
(e) => e.classList.contains('thumb-image-container__image'),
|
|
151
|
+
(e) => {
|
|
152
|
+
const videoSrc = e.parentElement?.getAttribute('data-previewvideo') as string;
|
|
153
|
+
const onOverCallback = createPreviewVideoElement(videoSrc, e);
|
|
154
|
+
const leaveTarget = e.parentElement?.parentElement as HTMLElement;
|
|
155
|
+
return { leaveTarget, onOverCallback };
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function expandMoreVideoPage() {
|
|
161
|
+
watchElementChildrenCount(rules.container, () => setTimeout(parseThumbs, 1800));
|
|
162
|
+
waitForElementToAppear(document.body, 'button[data-role="show-more-next"]', (el) => {
|
|
163
|
+
const observer = new Observer((target) => {
|
|
164
|
+
(target as HTMLButtonElement).click();
|
|
165
|
+
});
|
|
166
|
+
observer.observe(el);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseThumbs() {
|
|
171
|
+
const containers = getCommonParents(rules.getThumbs(document.body));
|
|
172
|
+
containers.forEach((c) => {
|
|
173
|
+
rules.dataManager.parseData(c, c);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (IS_VIDEO_PAGE) {
|
|
178
|
+
expandMoreVideoPage();
|
|
179
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { MonkeyUserScript } from 'vite-plugin-monkey';
|
|
2
|
+
import { unsafeWindow } from '$';
|
|
3
|
+
import { RulesGlobal } from '../../core/rules';
|
|
4
|
+
import { exterminateVideo, parseHtml } from '../../utils/dom';
|
|
5
|
+
import { onPointerOverAndLeave } from '../../utils/events';
|
|
6
|
+
|
|
7
|
+
export const meta: MonkeyUserScript = {
|
|
8
|
+
name: 'XVideos Improved',
|
|
9
|
+
version: '4.0.0',
|
|
10
|
+
description: 'Infinite scroll [optional], Filter by Title and Duration',
|
|
11
|
+
match: 'https://*.xvideos.com/*',
|
|
12
|
+
icon: 'https://www.google.com/s2/favicons?sz=64&domain=xvideos.com',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const xv = (unsafeWindow as any).xv;
|
|
16
|
+
|
|
17
|
+
const rules = new RulesGlobal({
|
|
18
|
+
paginationStrategyOptions: {
|
|
19
|
+
paginationSelector: '#main .pagination:last-child',
|
|
20
|
+
searchParamSelector: 'p',
|
|
21
|
+
},
|
|
22
|
+
containerSelector: '#content > div',
|
|
23
|
+
thumbsSelector: 'div.thumb-block[id^=video_]:not(.thumb-ad)',
|
|
24
|
+
titleSelector: '[class*=title]',
|
|
25
|
+
uploaderSelector: '[class*=name]',
|
|
26
|
+
durationSelector: '[class*=duration]',
|
|
27
|
+
gropeStrategy: 'all-in-one',
|
|
28
|
+
customDataSelectorFns: ['filterInclude', 'filterExclude'],
|
|
29
|
+
schemeOptions: ['Text Filter', 'Duration Filter', 'Badge', 'Advanced'],
|
|
30
|
+
animatePreview,
|
|
31
|
+
getThumbDataCallback(thumb) {
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
const id = parseInt(thumb.getAttribute('data-id') as string);
|
|
34
|
+
xv.thumbs.prepareVideo(id);
|
|
35
|
+
}, 200);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function animatePreview(container: HTMLElement) {
|
|
40
|
+
function createPreviewElement(src: string, mount: HTMLElement) {
|
|
41
|
+
const elem = parseHtml(`
|
|
42
|
+
<div class="videopv" style="display: none;">
|
|
43
|
+
<video autoplay="autoplay" playsinline="playsinline" muted="muted"></video>
|
|
44
|
+
</div>`);
|
|
45
|
+
mount.after(elem);
|
|
46
|
+
|
|
47
|
+
const video = elem.querySelector('video') as HTMLVideoElement;
|
|
48
|
+
video.src = src;
|
|
49
|
+
video.addEventListener(
|
|
50
|
+
'loadeddata',
|
|
51
|
+
() => {
|
|
52
|
+
mount.style.opacity = '0';
|
|
53
|
+
elem.style.display = 'block';
|
|
54
|
+
elem.style.background = '#000';
|
|
55
|
+
},
|
|
56
|
+
false,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
exterminateVideo(video);
|
|
61
|
+
elem.remove();
|
|
62
|
+
mount.style.opacity = '1';
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getVideoURL(src: string) {
|
|
67
|
+
return src
|
|
68
|
+
.replace(/thumbs169l{1,}/, 'videopreview')
|
|
69
|
+
.replace(/\/\w+\.\d+\.\w+/, '_169.mp4')
|
|
70
|
+
.replace(/(-\d+)_169\.mp4/, (_, b) => `_169${b}.mp4`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onPointerOverAndLeave(
|
|
74
|
+
container,
|
|
75
|
+
(target) => target.tagName === 'IMG' && target.id.includes('pic_'),
|
|
76
|
+
(target) => {
|
|
77
|
+
const videoSrc = getVideoURL((target as HTMLImageElement).src);
|
|
78
|
+
const onOverCallback = createPreviewElement(videoSrc, target);
|
|
79
|
+
const leaveTarget = target.closest('.thumb-inside') as HTMLElement;
|
|
80
|
+
return { leaveTarget, onOverCallback };
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function chunks<T>(arr: T[], size: number): T[][] {
|
|
2
|
+
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
|
|
3
|
+
arr.slice(i * size, i * size + size),
|
|
4
|
+
);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function* irange(start: number = 1, step: number = 1) {
|
|
8
|
+
for (let i = start; ; i += step) {
|
|
9
|
+
yield i;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function range(size: number, start: number = 1, step: number = 1): number[] {
|
|
14
|
+
return irange(start, step).take(size).toArray();
|
|
15
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function waitForElementToAppear(
|
|
2
|
+
parent: ParentNode,
|
|
3
|
+
selector: string,
|
|
4
|
+
callback: (el: Element) => void,
|
|
5
|
+
) {
|
|
6
|
+
const observer = new MutationObserver((_mutations) => {
|
|
7
|
+
const el = parent.querySelector(selector);
|
|
8
|
+
if (el) {
|
|
9
|
+
observer.disconnect();
|
|
10
|
+
callback(el);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
15
|
+
return observer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function waitForElementToDisappear(observable: HTMLElement, callback: () => void) {
|
|
19
|
+
const observer = new MutationObserver((_mutations) => {
|
|
20
|
+
if (!observable.isConnected) {
|
|
21
|
+
observer.disconnect();
|
|
22
|
+
callback();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
27
|
+
return observer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function watchElementChildrenCount(
|
|
31
|
+
element: ParentNode,
|
|
32
|
+
callback: (observer: MutationObserver, count: number) => void,
|
|
33
|
+
) {
|
|
34
|
+
let count = element.children.length;
|
|
35
|
+
const observer = new MutationObserver((mutationList, observer) => {
|
|
36
|
+
for (const mutation of mutationList) {
|
|
37
|
+
if (mutation.type === 'childList') {
|
|
38
|
+
if (count !== element.children.length) {
|
|
39
|
+
count = element.children.length;
|
|
40
|
+
callback(observer, count);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
observer.observe(element, { childList: true });
|
|
47
|
+
return observer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function watchDomChangesWithThrottle(
|
|
51
|
+
element: HTMLElement,
|
|
52
|
+
callback: () => void,
|
|
53
|
+
throttle = 1000,
|
|
54
|
+
times = Infinity,
|
|
55
|
+
options: MutationObserverInit = { childList: true, subtree: true, attributes: true },
|
|
56
|
+
) {
|
|
57
|
+
let lastMutationTime: number;
|
|
58
|
+
let timeout: number;
|
|
59
|
+
let times_ = times;
|
|
60
|
+
const observer = new MutationObserver((_mutationList, _observer) => {
|
|
61
|
+
if (times_ !== Infinity && times_ < 1) {
|
|
62
|
+
observer.disconnect();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
times_--;
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (lastMutationTime && now - lastMutationTime < throttle) {
|
|
68
|
+
timeout && clearTimeout(timeout);
|
|
69
|
+
}
|
|
70
|
+
timeout = window.setTimeout(callback, throttle);
|
|
71
|
+
lastMutationTime = now;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
observer.observe(element, options);
|
|
75
|
+
return observer;
|
|
76
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { sanitizeStr } from '../strings';
|
|
2
|
+
import { waitForElementToAppear } from './dom-observers';
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
waitForElementToAppear,
|
|
6
|
+
waitForElementToDisappear,
|
|
7
|
+
watchDomChangesWithThrottle,
|
|
8
|
+
watchElementChildrenCount,
|
|
9
|
+
} from './dom-observers';
|
|
10
|
+
|
|
11
|
+
export function querySelectorLast<T extends Element = HTMLElement>(
|
|
12
|
+
root: ParentNode = document,
|
|
13
|
+
selector: string,
|
|
14
|
+
): T | undefined {
|
|
15
|
+
const nodes = root.querySelectorAll<T>(selector);
|
|
16
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function querySelectorLastNumber(selector: string, e: ParentNode = document) {
|
|
20
|
+
const text = querySelectorText(e, selector);
|
|
21
|
+
return Number(text.match(/\d+/g)?.pop() || 0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function querySelectorText(e: ParentNode, selector?: string): string {
|
|
25
|
+
if (typeof selector !== 'string') return '';
|
|
26
|
+
const text = e.querySelector<HTMLElement>(selector)?.innerText || '';
|
|
27
|
+
return sanitizeStr(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseHtml(html: string): HTMLElement {
|
|
31
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html').body;
|
|
32
|
+
if (parsed.children.length > 1) return parsed;
|
|
33
|
+
return parsed.firstElementChild as HTMLElement;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function copyAttributes<T extends Element = HTMLElement>(target: T, source: T) {
|
|
37
|
+
for (const attr of source.attributes) {
|
|
38
|
+
if (attr.nodeValue) {
|
|
39
|
+
target.setAttribute(attr.nodeName, attr.nodeValue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function replaceElementTag(e: HTMLElement, tagName: string) {
|
|
45
|
+
const newTagElement = document.createElement(tagName);
|
|
46
|
+
copyAttributes(newTagElement, e);
|
|
47
|
+
|
|
48
|
+
newTagElement.innerHTML = e.innerHTML;
|
|
49
|
+
e.parentNode?.replaceChild(newTagElement, e);
|
|
50
|
+
|
|
51
|
+
return newTagElement;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function removeClassesAndDataAttributes(
|
|
55
|
+
element: HTMLElement,
|
|
56
|
+
keyword: string,
|
|
57
|
+
): void {
|
|
58
|
+
Array.from(element.classList).forEach((className) => {
|
|
59
|
+
if (className.includes(keyword)) {
|
|
60
|
+
element.classList.remove(className);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
65
|
+
if (attr.name.startsWith('data-') && attr.name.includes(keyword)) {
|
|
66
|
+
element.removeAttribute(attr.name);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getCommonParents(elements: HTMLCollection | HTMLElement[]): HTMLElement[] {
|
|
72
|
+
const parents = Array.from(elements)
|
|
73
|
+
.map((el) => el.parentElement)
|
|
74
|
+
.filter((parent): parent is HTMLElement => parent !== null);
|
|
75
|
+
|
|
76
|
+
return [...new Set(parents)];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function findNextSibling<T extends Element = HTMLElement>(e: T) {
|
|
80
|
+
if (e.nextElementSibling) return e.nextElementSibling;
|
|
81
|
+
if (e.parentElement) return findNextSibling(e.parentElement);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function checkHomogenity<T extends HTMLElement>(
|
|
86
|
+
a: T,
|
|
87
|
+
b: T,
|
|
88
|
+
options: { id?: boolean; className?: boolean },
|
|
89
|
+
) {
|
|
90
|
+
if (!a || !b) return false;
|
|
91
|
+
|
|
92
|
+
if (options.id) {
|
|
93
|
+
if (a.id !== b.id) return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (options.className) {
|
|
97
|
+
const ca = a.className;
|
|
98
|
+
const cb = b.className;
|
|
99
|
+
if (!(ca.length > cb.length ? ca.includes(cb) : cb.includes(ca))) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function instantiateTemplate(
|
|
108
|
+
sourceSelector: string,
|
|
109
|
+
attributeUpdates: Record<string, string>,
|
|
110
|
+
contentUpdates: Record<string, string>,
|
|
111
|
+
): string {
|
|
112
|
+
const source = document.querySelector(sourceSelector) as HTMLElement;
|
|
113
|
+
|
|
114
|
+
const wrapper = document.createElement('div');
|
|
115
|
+
const clone = source.cloneNode(true);
|
|
116
|
+
wrapper.append(clone);
|
|
117
|
+
|
|
118
|
+
Object.entries(attributeUpdates).forEach(([attrName, attrValue]) => {
|
|
119
|
+
wrapper.querySelectorAll(`[${attrName}]`).forEach((element) => {
|
|
120
|
+
element.setAttribute(attrName, attrValue);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
Object.entries(contentUpdates).forEach(([childSelector, textValue]) => {
|
|
125
|
+
wrapper.querySelectorAll<HTMLElement>(childSelector).forEach((element) => {
|
|
126
|
+
element.innerText = textValue;
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return wrapper.innerHTML;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function exterminateVideo(video: HTMLVideoElement) {
|
|
134
|
+
video.removeAttribute('src');
|
|
135
|
+
video.load();
|
|
136
|
+
video.remove();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function downloader(
|
|
140
|
+
options = { append: '', after: '', button: '', cbBefore: () => {} },
|
|
141
|
+
) {
|
|
142
|
+
const btn = parseHtml(options.button);
|
|
143
|
+
|
|
144
|
+
if (options.append) document.querySelector(options.append)?.append(btn);
|
|
145
|
+
if (options.after) document.querySelector(options.after)?.after(btn);
|
|
146
|
+
|
|
147
|
+
btn.addEventListener('click', (e) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
|
|
150
|
+
if (options.cbBefore) options.cbBefore();
|
|
151
|
+
|
|
152
|
+
waitForElementToAppear(document.body, 'video', (video: Element) => {
|
|
153
|
+
window.location.href = video.getAttribute('src') as string;
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function onPointerOverAndLeave(
|
|
2
|
+
container: HTMLElement,
|
|
3
|
+
subjectSelector: (target: HTMLElement) => boolean,
|
|
4
|
+
onOver: (
|
|
5
|
+
target: HTMLElement,
|
|
6
|
+
) => { onOverCallback?: () => void; leaveTarget?: HTMLElement } | void,
|
|
7
|
+
onLeave?: (target: HTMLElement) => void,
|
|
8
|
+
) {
|
|
9
|
+
let target: HTMLElement | undefined;
|
|
10
|
+
let onOverFinally: (() => void) | undefined;
|
|
11
|
+
|
|
12
|
+
function handleLeaveEvent() {
|
|
13
|
+
onLeave?.(target as HTMLElement);
|
|
14
|
+
onOverFinally?.();
|
|
15
|
+
target = undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function handleEvent(e: PointerEvent) {
|
|
19
|
+
const currentTarget = e.target as HTMLElement;
|
|
20
|
+
if (!subjectSelector(currentTarget) || target === currentTarget) return;
|
|
21
|
+
target = currentTarget;
|
|
22
|
+
|
|
23
|
+
const result = onOver(target);
|
|
24
|
+
|
|
25
|
+
onOverFinally = result?.onOverCallback;
|
|
26
|
+
const leaveSubject = result?.leaveTarget || target;
|
|
27
|
+
|
|
28
|
+
leaveSubject.addEventListener('pointerleave', handleLeaveEvent, {
|
|
29
|
+
once: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
container.addEventListener('pointerover', handleEvent);
|
|
34
|
+
}
|
|
35
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class Tick {
|
|
2
|
+
private tick?: number;
|
|
3
|
+
private callbackFinal?: () => void;
|
|
4
|
+
|
|
5
|
+
constructor(
|
|
6
|
+
private delay: number,
|
|
7
|
+
private startImmediate: boolean = true,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
public start(callback: () => void, callbackFinal?: () => void): void {
|
|
11
|
+
this.stop();
|
|
12
|
+
this.callbackFinal = callbackFinal;
|
|
13
|
+
if (this.startImmediate) callback();
|
|
14
|
+
this.tick = window.setInterval(callback, this.delay);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public stop(): void {
|
|
18
|
+
if (this.tick !== undefined) {
|
|
19
|
+
clearInterval(this.tick);
|
|
20
|
+
this.tick = undefined;
|
|
21
|
+
}
|
|
22
|
+
if (this.callbackFinal) {
|
|
23
|
+
this.callbackFinal();
|
|
24
|
+
this.callbackFinal = undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { parseHtml } from '../dom';
|
|
2
|
+
|
|
3
|
+
export const MOBILE_UA = {
|
|
4
|
+
'User-Agent': [
|
|
5
|
+
'Mozilla/5.0 (Linux; Android 10; K)',
|
|
6
|
+
'AppleWebKit/537.36 (KHTML, like Gecko)',
|
|
7
|
+
'Chrome/114.0.0.0 Mobile Safari/537.36',
|
|
8
|
+
].join(' '),
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export async function fetchWith<T extends JSON | string | HTMLElement>(
|
|
12
|
+
input: RequestInfo | URL,
|
|
13
|
+
options: {
|
|
14
|
+
init?: RequestInit;
|
|
15
|
+
type: 'json' | 'html' | 'text';
|
|
16
|
+
mobile?: boolean;
|
|
17
|
+
},
|
|
18
|
+
): Promise<T> {
|
|
19
|
+
const requestInit: RequestInit = options.init || {};
|
|
20
|
+
|
|
21
|
+
if (options.mobile) {
|
|
22
|
+
Object.assign(requestInit, { headers: new Headers(MOBILE_UA) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const r = await fetch(input, requestInit).then((r) => r);
|
|
26
|
+
|
|
27
|
+
if (options.type === 'json') return (await r.json()) as T;
|
|
28
|
+
if (options.type === 'html') return parseHtml(await r.text()) as T;
|
|
29
|
+
return (await r.text()) as T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const fetchJson = (input: RequestInfo | URL) =>
|
|
33
|
+
fetchWith<JSON>(input, { type: 'json' });
|
|
34
|
+
export const fetchHtml = (input: RequestInfo | URL) =>
|
|
35
|
+
fetchWith<HTMLElement>(input, { type: 'html' });
|
|
36
|
+
export const fetchText = (input: RequestInfo | URL) =>
|
|
37
|
+
fetchWith<string>(input, { type: 'text' });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AnyFunction } from '../../types';
|
|
2
|
+
|
|
3
|
+
export interface MemoizedFunction<T extends AnyFunction> extends CallableFunction {
|
|
4
|
+
(...args: Parameters<T>): ReturnType<T>;
|
|
5
|
+
clear: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function memoize<T extends AnyFunction>(fn: T): MemoizedFunction<T> {
|
|
9
|
+
const cache = new Map<string, ReturnType<T>>();
|
|
10
|
+
|
|
11
|
+
const memoizedFunction = ((...args: Parameters<T>): ReturnType<T> => {
|
|
12
|
+
const key = JSON.stringify(args);
|
|
13
|
+
|
|
14
|
+
if (cache.has(key)) {
|
|
15
|
+
return cache.get(key) as ReturnType<T>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = fn(...args);
|
|
19
|
+
cache.set(key, result);
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
}) as MemoizedFunction<T>;
|
|
23
|
+
|
|
24
|
+
return memoizedFunction;
|
|
25
|
+
}
|