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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/core/pervertmonkey.core.es.d.ts +391 -0
  4. package/dist/core/pervertmonkey.core.es.js +8497 -0
  5. package/dist/core/pervertmonkey.core.es.js.map +1 -0
  6. package/dist/core/pervertmonkey.core.umd.js +8500 -0
  7. package/dist/core/pervertmonkey.core.umd.js.map +1 -0
  8. package/dist/userscripts/3hentai.user.js +1176 -0
  9. package/dist/userscripts/camgirlfinder.user.js +68 -0
  10. package/dist/userscripts/camwhores.user.js +1602 -0
  11. package/dist/userscripts/e-hentai.user.js +1212 -0
  12. package/dist/userscripts/ebalka.user.js +1231 -0
  13. package/dist/userscripts/eporner.user.js +1265 -0
  14. package/dist/userscripts/erome.user.js +1245 -0
  15. package/dist/userscripts/eroprofile.user.js +1194 -0
  16. package/dist/userscripts/javhdporn.user.js +1178 -0
  17. package/dist/userscripts/missav.user.js +1182 -0
  18. package/dist/userscripts/motherless.user.js +1380 -0
  19. package/dist/userscripts/namethatporn.user.js +1218 -0
  20. package/dist/userscripts/nhentai.user.js +1262 -0
  21. package/dist/userscripts/pornhub.user.js +1199 -0
  22. package/dist/userscripts/spankbang.user.js +1239 -0
  23. package/dist/userscripts/xhamster.user.js +1374 -0
  24. package/dist/userscripts/xvideos.user.js +1254 -0
  25. package/package.json +54 -0
  26. package/src/core/data-control/data-filter.ts +143 -0
  27. package/src/core/data-control/data-manager.ts +144 -0
  28. package/src/core/data-control/index.ts +2 -0
  29. package/src/core/infinite-scroll/index.ts +143 -0
  30. package/src/core/jabroni-config/default-scheme.ts +97 -0
  31. package/src/core/jabroni-config/default-store.ts +9 -0
  32. package/src/core/pagination-parsing/index.ts +55 -0
  33. package/src/core/pagination-parsing/pagination-strategies/PaginationStrategy.ts +44 -0
  34. package/src/core/pagination-parsing/pagination-strategies/PaginationStrategyDataParams.ts +66 -0
  35. package/src/core/pagination-parsing/pagination-strategies/PaginationStrategyPathnameParams.ts +77 -0
  36. package/src/core/pagination-parsing/pagination-strategies/PaginationStrategySearchParams.ts +56 -0
  37. package/src/core/pagination-parsing/pagination-strategies/index.ts +4 -0
  38. package/src/core/pagination-parsing/pagination-utils/index.ts +84 -0
  39. package/src/core/rules/index.ts +385 -0
  40. package/src/index.ts +42 -0
  41. package/src/types/index.ts +7 -0
  42. package/src/userscripts/ascii-logos.js +468 -0
  43. package/src/userscripts/index.ts +1 -0
  44. package/src/userscripts/meta.json +11 -0
  45. package/src/userscripts/scripts/3hentai.ts +20 -0
  46. package/src/userscripts/scripts/camgirlfinder.ts +68 -0
  47. package/src/userscripts/scripts/camwhores.ts +382 -0
  48. package/src/userscripts/scripts/e-hentai.ts +68 -0
  49. package/src/userscripts/scripts/ebalka.ts +58 -0
  50. package/src/userscripts/scripts/eporner.ts +90 -0
  51. package/src/userscripts/scripts/erome.ts +105 -0
  52. package/src/userscripts/scripts/eroprofile.ts +38 -0
  53. package/src/userscripts/scripts/javhdporn.ts +24 -0
  54. package/src/userscripts/scripts/missav.ts +28 -0
  55. package/src/userscripts/scripts/motherless.ts +222 -0
  56. package/src/userscripts/scripts/namethatporn.ts +68 -0
  57. package/src/userscripts/scripts/nhentai.ts +135 -0
  58. package/src/userscripts/scripts/pornhub.ts +53 -0
  59. package/src/userscripts/scripts/spankbang.ts +61 -0
  60. package/src/userscripts/scripts/thisvid.ts +716 -0
  61. package/src/userscripts/scripts/xhamster.ts +179 -0
  62. package/src/userscripts/scripts/xvideos.ts +83 -0
  63. package/src/utils/arrays/index.ts +15 -0
  64. package/src/utils/async/index.ts +3 -0
  65. package/src/utils/dom/dom-observers.ts +76 -0
  66. package/src/utils/dom/index.ts +156 -0
  67. package/src/utils/events/index.ts +2 -0
  68. package/src/utils/events/on-pointer-over-and-leave.ts +35 -0
  69. package/src/utils/events/tick.ts +27 -0
  70. package/src/utils/fetch/index.ts +37 -0
  71. package/src/utils/math/index.ts +3 -0
  72. package/src/utils/objects/index.ts +9 -0
  73. package/src/utils/objects/memoize.ts +25 -0
  74. package/src/utils/observers/index.ts +44 -0
  75. package/src/utils/observers/lazy-image-loader.ts +27 -0
  76. package/src/utils/parsers/index.ts +30 -0
  77. package/src/utils/parsers/time-parser.ts +28 -0
  78. package/src/utils/strings/index.ts +10 -0
  79. package/src/utils/strings/regexes.ts +35 -0
  80. package/src/vite-env.d.ts +4 -0
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "pervert-monkey",
3
+ "description": "daddy told us not to be ashamed of our userscripts",
4
+ "version": "1.0.0",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "userscript",
8
+ "tampermonkey",
9
+ "violentmonkey",
10
+ "sleazy-fork"
11
+ ],
12
+ "author": "ViolentOrangutan atm.mormon@protonmail.com (https://github.com/smartacephale)",
13
+ "homepage": "https://github.com/smartacephale/sleazy-fork#readme",
14
+ "type": "module",
15
+ "main": "dist/perverpervertmonkey.core.umd.js",
16
+ "module": "dist/perverpervertmonkey.core.es.js",
17
+ "types": "dist/perverpervertmonkey.core.es.d.ts",
18
+ "repository": "github:smartacephale/sleazy-fork",
19
+ "bugs": {
20
+ "url": "https://github.com/smartacephale/sleazy-fork/issues",
21
+ "email": "atm.mormon@protonmail.com"
22
+ },
23
+ "browser": "dist/perverpervertmonkey.core.es.js",
24
+ "unpkg": "dist/perverpervertmonkey.core.es.js",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/perverpervertmonkey.core.es.d.ts",
28
+ "import": "./dist/perverpervertmonkey.core.es.js",
29
+ "require": "./dist/perverpervertmonkey.core.umd.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "src/**/*",
34
+ "dist/**/*"
35
+ ],
36
+ "scripts": {
37
+ "dev": "vite",
38
+ "build": "tsc && vite build",
39
+ "build:userscripts": "node vite.build.ts"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^25.2.3",
43
+ "jabroni-outfit": "^2.1.1",
44
+ "typescript": "^5.9.3",
45
+ "vite": "^6.4.1",
46
+ "vite-plugin-dts": "^4.0.3"
47
+ },
48
+ "dependencies": {
49
+ "ix": "^7.0.0",
50
+ "lskdb": "^1.0.2",
51
+ "urlpattern-polyfill": "^10.1.0",
52
+ "vite-plugin-monkey": "^7.1.9"
53
+ }
54
+ }
@@ -0,0 +1,143 @@
1
+ import type { StoreState } from 'jabroni-outfit';
2
+ import { GM_addStyle } from '$';
3
+ import { RegexFilter } from '../../utils/strings/regexes';
4
+ import type { RulesGlobal } from '../rules';
5
+ import type { DataElement } from './data-manager';
6
+
7
+ export type DataSelectorFnShort = (e: DataElement, state: StoreState) => boolean;
8
+
9
+ export type DataSelectorFnAdvanced<R> = {
10
+ handle: (el: DataElement, state: StoreState, $preDefineResult?: R) => boolean;
11
+ $preDefine?: (state: StoreState) => R;
12
+ deps?: string[];
13
+ };
14
+
15
+ export type DataSelectorFn<R> = DataSelectorFnAdvanced<R> | DataSelectorFnShort;
16
+
17
+ interface DataFilterResult {
18
+ tag: string;
19
+ condition: boolean;
20
+ }
21
+
22
+ export type DataFilterFn = (v: DataElement) => DataFilterResult;
23
+
24
+ export class DataFilter {
25
+ public filters = new Map<string, () => DataFilterFn>();
26
+
27
+ constructor(private rules: RulesGlobal) {
28
+ this.registerFilters(rules.customDataSelectorFns);
29
+ this.applyCSSFilters();
30
+ }
31
+
32
+ public static isFiltered(el: HTMLElement): boolean {
33
+ return el.className.includes('filter-');
34
+ }
35
+
36
+ public applyCSSFilters(wrapper?: (cssRule: string) => string) {
37
+ this.filters.forEach((_, name) => {
38
+ const cssRule = `.filter-${name} { display: none !important; }`;
39
+ // this.appliedStyle = GM_addStyle();
40
+ if (wrapper) {
41
+ GM_addStyle(wrapper(cssRule));
42
+ } else {
43
+ GM_addStyle(cssRule);
44
+ }
45
+ });
46
+ }
47
+
48
+ public customDataSelectorFns: Record<string, DataSelectorFn<any>> = {};
49
+
50
+ public registerFilters(customFilters: (Record<string, DataSelectorFn<any>> | string)[]) {
51
+ customFilters.forEach((o) => {
52
+ if (typeof o === 'string') {
53
+ this.customDataSelectorFns[o] = DataFilter.customDataSelectorFnsDefault[o];
54
+ this.registerFilter(o);
55
+ } else {
56
+ const k = Object.keys(o)[0];
57
+ this.customDataSelectorFns[k] = o[k];
58
+ this.registerFilter(k);
59
+ }
60
+ });
61
+ }
62
+
63
+ private customSelectorParser<T>(
64
+ name: string,
65
+ selector: DataSelectorFn<T>,
66
+ ): DataSelectorFnAdvanced<T> {
67
+ if ('handle' in selector) {
68
+ return selector as DataSelectorFnAdvanced<T>;
69
+ } else {
70
+ return { handle: selector, deps: [name] } as DataSelectorFnAdvanced<T>;
71
+ }
72
+ }
73
+
74
+ public registerFilter(customSelectorName: string) {
75
+ const handler = this.customSelectorParser(
76
+ customSelectorName,
77
+ this.customDataSelectorFns[customSelectorName],
78
+ );
79
+ const tag = `filter-${customSelectorName}`;
80
+
81
+ [customSelectorName, ...(handler.deps || [])]?.forEach((name) => {
82
+ Object.assign(this.filterMapping, { [name]: customSelectorName });
83
+ });
84
+
85
+ const fn = (): DataFilterFn => {
86
+ const preDefined = handler.$preDefine?.(this.rules.store.state);
87
+
88
+ return (v: DataElement) => {
89
+ const condition = handler.handle(v, this.rules.store.state, preDefined);
90
+
91
+ return {
92
+ condition,
93
+ tag,
94
+ };
95
+ };
96
+ };
97
+
98
+ this.filters.set(customSelectorName, fn);
99
+ }
100
+
101
+ public filterMapping: Record<string, string> = {};
102
+
103
+ public selectFilters(filters: { [key: string]: boolean }) {
104
+ const selectedFilters = Object.keys(filters)
105
+ .filter((k) => k in this.filterMapping)
106
+ .map((k) => this.filterMapping[k])
107
+ .map((k) => this.filters.get(k) as () => DataFilterFn);
108
+ return selectedFilters;
109
+ }
110
+
111
+ static customDataSelectorFnsDefault: Record<string, DataSelectorFn<any>> = {
112
+ filterDuration: {
113
+ handle(el, state, notInRange) {
114
+ return (state.filterDuration as boolean) && notInRange(el.duration);
115
+ },
116
+ $preDefine: (state) => {
117
+ const from = state.filterDurationFrom as number;
118
+ const to = state.filterDurationTo as number;
119
+ function notInRange(d: number) {
120
+ return d < from || d > to;
121
+ }
122
+ return notInRange;
123
+ },
124
+ deps: ['filterDurationFrom', 'filterDurationTo'],
125
+ },
126
+ filterExclude: {
127
+ handle(el, state, searchFilter) {
128
+ if (!state.filterExclude) return false;
129
+ return !(searchFilter as RegexFilter).hasNone(el.title as string);
130
+ },
131
+ $preDefine: (state) => new RegexFilter(state.filterExcludeWords as string),
132
+ deps: ['filterExcludeWords'],
133
+ },
134
+ filterInclude: {
135
+ handle(el, state, searchFilter) {
136
+ if (!state.filterInclude) return false;
137
+ return !(searchFilter as RegexFilter).hasEvery(el.title as string);
138
+ },
139
+ $preDefine: (state) => new RegexFilter(state.filterIncludeWords as string),
140
+ deps: ['filterIncludeWords'],
141
+ },
142
+ };
143
+ }
@@ -0,0 +1,144 @@
1
+ import type { StoreState } from 'jabroni-outfit';
2
+ import { checkHomogenity } from '../../utils/dom';
3
+ import { LazyImgLoader } from '../../utils/observers';
4
+ import type { RulesGlobal } from '../rules';
5
+ import { DataFilter } from './data-filter';
6
+
7
+ export type DataElement = Record<string, string | number | boolean | HTMLElement>;
8
+
9
+ export class DataManager {
10
+ public data = new Map<string, DataElement>();
11
+ private lazyImgLoader = new LazyImgLoader(
12
+ (target: Element) => !DataFilter.isFiltered(target as HTMLElement),
13
+ );
14
+ public dataFilter: DataFilter;
15
+
16
+ constructor(private rules: RulesGlobal) {
17
+ this.dataFilter = new DataFilter(this.rules);
18
+ }
19
+
20
+ public applyFilters = async (
21
+ filters: Record<string, boolean> = {},
22
+ offset = 0,
23
+ ): Promise<void> => {
24
+ const filtersToApply = this.dataFilter.selectFilters(filters);
25
+ if (filtersToApply.length === 0) return;
26
+
27
+ const iterator = this.data.values().drop(offset);
28
+ let finished = false;
29
+
30
+ await new Promise((resolve) => {
31
+ function runBatch(deadline: IdleDeadline) {
32
+ const updates: { e: HTMLElement; tag: string; condition: boolean }[] = [];
33
+
34
+ while (deadline.timeRemaining() > 0) {
35
+ const { value, done } = iterator.next();
36
+ finished = !!done;
37
+ if (done) break;
38
+
39
+ for (const f of filtersToApply) {
40
+ const { tag, condition } = f()(value);
41
+ updates.push({ e: value.element as HTMLElement, tag, condition });
42
+ }
43
+ }
44
+
45
+ if (updates.length > 0) {
46
+ requestAnimationFrame(() => {
47
+ updates.forEach((u) => {
48
+ u.e.classList.toggle(u.tag, u.condition);
49
+ });
50
+ });
51
+ }
52
+
53
+ if (!finished) {
54
+ requestIdleCallback(runBatch);
55
+ } else {
56
+ resolve(true);
57
+ }
58
+ }
59
+
60
+ requestIdleCallback(runBatch);
61
+ });
62
+ };
63
+
64
+ public filterAll = async (offset?: number): Promise<void> => {
65
+ const keys = Array.from(this.dataFilter.filters.keys());
66
+ const filters = Object.fromEntries(
67
+ keys.map((k) => [k, this.rules.store.state[k as keyof StoreState]]),
68
+ ) as Record<string, boolean>;
69
+
70
+ await this.applyFilters(filters, offset);
71
+ };
72
+
73
+ public parseDataParentHomogenity?: Parameters<typeof checkHomogenity>[2];
74
+
75
+ public parseData = (
76
+ html: HTMLElement,
77
+ container?: HTMLElement,
78
+ removeDuplicates = false,
79
+ shouldLazify = true,
80
+ ): void => {
81
+ const thumbs = this.rules.getThumbs(html);
82
+ const dataOffset = this.data.size;
83
+ const fragment = document.createDocumentFragment();
84
+ const parent = container || this.rules.container;
85
+ const homogenity = !!this.parseDataParentHomogenity;
86
+
87
+ for (const thumbElement of thumbs) {
88
+ const url = this.rules.getThumbUrl(thumbElement);
89
+ if (
90
+ !url ||
91
+ this.data.has(url) ||
92
+ (parent !== container && parent?.contains(thumbElement)) ||
93
+ (homogenity &&
94
+ !checkHomogenity(
95
+ parent,
96
+ thumbElement.parentElement as HTMLElement,
97
+ this.parseDataParentHomogenity as object,
98
+ ))
99
+ ) {
100
+ if (removeDuplicates) thumbElement.remove();
101
+ continue;
102
+ }
103
+
104
+ const data = this.rules.getThumbData(thumbElement);
105
+ this.data.set(url, { element: thumbElement, ...data });
106
+
107
+ if (shouldLazify) {
108
+ const { img, imgSrc } = this.rules.getThumbImgData(thumbElement);
109
+ this.lazyImgLoader.lazify(thumbElement, img, imgSrc);
110
+ }
111
+
112
+ fragment.append(thumbElement);
113
+ }
114
+
115
+ this.filterAll(dataOffset).then(() => {
116
+ requestAnimationFrame(() => {
117
+ parent.appendChild(fragment);
118
+ });
119
+ });
120
+ };
121
+
122
+ public sortBy<K extends keyof DataElement>(key: K, direction = true): void {
123
+ if (this.data.size < 2) return;
124
+
125
+ let sorted: DataElement[] = this.data
126
+ .values()
127
+ .toArray()
128
+ .sort((a: DataElement, b: DataElement) => {
129
+ return (a[key] as number) - (b[key] as number);
130
+ });
131
+
132
+ if (!direction) sorted = sorted.reverse();
133
+
134
+ const container = (sorted[0].element as HTMLElement).parentElement as HTMLElement;
135
+
136
+ container.style.visibility = 'hidden';
137
+
138
+ sorted.forEach((s) => {
139
+ container.append(s.element as HTMLElement);
140
+ });
141
+
142
+ container.style.visibility = 'visible';
143
+ }
144
+ }
@@ -0,0 +1,2 @@
1
+ export { DataFilter } from './data-filter';
2
+ export { DataManager } from './data-manager';
@@ -0,0 +1,143 @@
1
+ import { wait } from '../../utils/async';
2
+ import { fetchHtml } from '../../utils/fetch';
3
+ import { Observer } from '../../utils/observers';
4
+ import type { PaginationStrategy } from '../pagination-parsing/pagination-strategies';
5
+ import type { RulesGlobal } from '../rules';
6
+
7
+ type InfiniteScrollerOptions = Pick<InfiniteScroller, 'rules'> & Partial<InfiniteScroller>;
8
+ type GeneratorResult = { url: string; offset: number };
9
+ export type OffsetGenerator<T = GeneratorResult> = Generator<T> | AsyncGenerator<T>;
10
+
11
+ export class InfiniteScroller {
12
+ public enabled = true;
13
+ public paginationOffset = 1;
14
+ public parseData?: (document: HTMLElement) => void;
15
+ public rules: RulesGlobal;
16
+
17
+ private observer?: Observer;
18
+ private paginationGenerator: OffsetGenerator;
19
+
20
+ constructor(options: InfiniteScrollerOptions) {
21
+ this.rules = options.rules;
22
+ this.paginationOffset = this.rules.paginationStrategy.getPaginationOffset();
23
+ Object.assign(this, options);
24
+
25
+ if (this.rules.getPaginationData) {
26
+ this.getPaginationData = this.rules.getPaginationData;
27
+ }
28
+
29
+ this.paginationGenerator =
30
+ this.rules.customGenerator ||
31
+ InfiniteScroller.generatorForPaginationStrategy(this.rules.paginationStrategy);
32
+ this.setObserver(this.rules.observable);
33
+ this.setAutoScroll();
34
+ }
35
+
36
+ public dispose() {
37
+ if (this.observer) this.observer.dispose();
38
+ }
39
+
40
+ public setObserver(observable: HTMLElement) {
41
+ if (this.observer) this.observer.dispose();
42
+ this.observer = Observer.observeWhile(
43
+ observable,
44
+ this.generatorConsumer,
45
+ this.rules.store.state.delay as number,
46
+ );
47
+ return this;
48
+ }
49
+
50
+ private onScrollCBs: Array<(scroller: InfiniteScroller) => void> = [];
51
+
52
+ public onScroll(callback: (scroller: InfiniteScroller) => void, initCall = false) {
53
+ if (initCall) callback(this);
54
+ this.onScrollCBs.push(callback);
55
+ return this;
56
+ }
57
+
58
+ private _onScroll() {
59
+ this.onScrollCBs.forEach((cb) => {
60
+ cb(this);
61
+ });
62
+ }
63
+
64
+ private setAutoScroll() {
65
+ const autoScrollWrapper = async () => {
66
+ if (this.rules.store.state.autoScroll) {
67
+ await wait(this.rules.store.state.delay as number);
68
+ await this.generatorConsumer();
69
+ await autoScrollWrapper();
70
+ }
71
+ };
72
+
73
+ autoScrollWrapper();
74
+
75
+ this.rules.store.stateSubject.subscribe((type) => {
76
+ if (type?.autoScroll) {
77
+ autoScrollWrapper();
78
+ }
79
+ });
80
+ }
81
+
82
+ generatorConsumer = async () => {
83
+ if (!this.enabled) return false;
84
+ const {
85
+ value: { url, offset },
86
+ done,
87
+ } = await this.paginationGenerator.next();
88
+ if (!done && url) {
89
+ await this.doScroll(url, offset);
90
+ }
91
+ return !done;
92
+ };
93
+
94
+ // consume api strategy
95
+ private async getPaginationData(url: string): Promise<HTMLElement> {
96
+ return await fetchHtml(url);
97
+ }
98
+
99
+ async doScroll(url: string, offset: number) {
100
+ const nextPageHtml = await this.getPaginationData(url);
101
+ const prevScrollPos = document.documentElement.scrollTop;
102
+ this.paginationOffset = Math.max(this.paginationOffset, offset);
103
+ this.parseData?.(nextPageHtml);
104
+ this._onScroll();
105
+ window.scrollTo(0, prevScrollPos);
106
+ if (this.rules.store.state.writeHistory) {
107
+ history.replaceState({}, '', url);
108
+ }
109
+ }
110
+
111
+ static async *generatorForPaginationStrategy(
112
+ pstrategy: PaginationStrategy,
113
+ ): OffsetGenerator {
114
+ const _offset = pstrategy.getPaginationOffset();
115
+ const end = pstrategy.getPaginationLast();
116
+ const urlGenerator = pstrategy.getPaginationUrlGenerator();
117
+
118
+ for (let offset = _offset; offset <= end; offset++) {
119
+ const url = await urlGenerator(offset);
120
+ yield { url, offset };
121
+ }
122
+ }
123
+
124
+ static create(rules: RulesGlobal) {
125
+ const enabled = rules.store.state.infiniteScrollEnabled as boolean;
126
+
127
+ rules.store.state.$paginationLast = rules.paginationStrategy.getPaginationLast();
128
+
129
+ const infiniteScroller = new InfiniteScroller({
130
+ enabled,
131
+ parseData: rules.dataManager.parseData,
132
+ rules,
133
+ }).onScroll(({ paginationOffset }) => {
134
+ rules.store.state.$paginationOffset = paginationOffset;
135
+ }, true);
136
+
137
+ rules.store.stateSubject.subscribe(() => {
138
+ infiniteScroller.enabled = rules.store.state.infiniteScrollEnabled as boolean;
139
+ });
140
+
141
+ return infiniteScroller;
142
+ }
143
+ }
@@ -0,0 +1,97 @@
1
+ import type { JabroniTypes, SchemeInput, setupScheme } from 'jabroni-outfit';
2
+
3
+ export const DefaultScheme = [
4
+ {
5
+ title: 'Text Filter',
6
+ collapsed: true,
7
+ content: [
8
+ { filterExclude: false, label: 'exclude' },
9
+ {
10
+ filterExcludeWords: '',
11
+ label: 'keywords',
12
+ watch: 'filterExclude',
13
+ placeholder: 'word, f:full_word, r:RegEx...',
14
+ },
15
+ { filterInclude: false, label: 'include' },
16
+ {
17
+ filterIncludeWords: '',
18
+ label: 'keywords',
19
+ watch: 'filterInclude',
20
+ placeholder: 'word, f:full_word, r:RegEx...',
21
+ },
22
+ ],
23
+ },
24
+ {
25
+ title: 'Duration Filter',
26
+ collapsed: true,
27
+ content: [
28
+ { filterDuration: false, label: 'enable' },
29
+ {
30
+ filterDurationFrom: 0,
31
+ watch: 'filterDuration',
32
+ label: 'from',
33
+ type: 'time',
34
+ },
35
+ {
36
+ filterDurationTo: 600,
37
+ watch: 'filterDuration',
38
+ label: 'to',
39
+ type: 'time',
40
+ },
41
+ ],
42
+ },
43
+ {
44
+ title: 'Sort By',
45
+ content: [
46
+ {
47
+ 'sort by views': () => {},
48
+ },
49
+ {
50
+ 'sort by duration': () => {},
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ title: 'Privacy Filter',
56
+ content: [
57
+ { filterPrivate: false, label: 'private' },
58
+ { filterPublic: false, label: 'public' },
59
+ { 'check access 🔓': () => {} },
60
+ ],
61
+ },
62
+ {
63
+ title: 'Advanced',
64
+ content: [
65
+ {
66
+ infiniteScrollEnabled: true,
67
+ label: 'infinite scroll',
68
+ },
69
+ {
70
+ autoScroll: false,
71
+ label: 'auto scroll',
72
+ },
73
+ {
74
+ delay: 250,
75
+ label: 'scroll delay',
76
+ },
77
+ {
78
+ writeHistory: false,
79
+ label: 'write history',
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ title: 'Badge',
85
+ content: [
86
+ {
87
+ text: 'return `${state.$paginationOffset}/${state.$paginationLast}`',
88
+ vif: 'return state.$paginationLast > 1',
89
+ },
90
+ ],
91
+ },
92
+ ] as const satisfies SchemeInput;
93
+
94
+ export type SchemeOptions = (
95
+ | Parameters<typeof setupScheme>[0][0]
96
+ | JabroniTypes.ExtractValuesByKey<typeof DefaultScheme, 'title'>
97
+ )[];
@@ -0,0 +1,9 @@
1
+ import type { JabroniTypes } from 'jabroni-outfit';
2
+
3
+ export const StoreStateDefault: JabroniTypes.StoreStateOptions = {
4
+ enabled: true,
5
+ collapsed: false,
6
+ darkmode: true,
7
+ $paginationLast: 1,
8
+ $paginationOffset: 1,
9
+ };
@@ -0,0 +1,55 @@
1
+ import {
2
+ PaginationStrategy,
3
+ PaginationStrategyDataParams,
4
+ PaginationStrategyPathnameParams,
5
+ PaginationStrategySearchParams,
6
+ } from './pagination-strategies';
7
+ import { getPaginationLinks } from './pagination-utils';
8
+
9
+ export function getPaginationStrategy(
10
+ options: Partial<PaginationStrategy>,
11
+ ): PaginationStrategy {
12
+ const _paginationStrategy = new PaginationStrategy(options);
13
+ const pagination = _paginationStrategy.getPaginationElement();
14
+
15
+ Object.assign(options, { ..._paginationStrategy });
16
+ const { url, searchParamSelector } = options;
17
+
18
+ if (!pagination) {
19
+ // console.error('Found No Pagination');
20
+ return _paginationStrategy;
21
+ }
22
+
23
+ if (typeof options.getPaginationUrlGenerator === 'function') {
24
+ // console.log(PaginationStrategy.name);
25
+ return new PaginationStrategy(options);
26
+ }
27
+
28
+ const pageLinks = getPaginationLinks(pagination, url).map((l) => new URL(l));
29
+
30
+ // console.log({ pageLinks: pageLinks.map((l) => l.href) });
31
+
32
+ const selectStrategy = (): typeof PaginationStrategy => {
33
+ if (PaginationStrategyDataParams.testLinks(pagination)) {
34
+ return PaginationStrategyDataParams;
35
+ }
36
+
37
+ if (PaginationStrategySearchParams.testLinks(pageLinks, searchParamSelector)) {
38
+ return PaginationStrategySearchParams;
39
+ }
40
+
41
+ if (PaginationStrategyPathnameParams.testLinks(pageLinks, options)) {
42
+ return PaginationStrategyPathnameParams;
43
+ }
44
+
45
+ console.error('Found No Strategy');
46
+ return PaginationStrategy;
47
+ };
48
+
49
+ const PaginationStrategyConstructor = selectStrategy();
50
+ const paginationStrategy = new PaginationStrategyConstructor(options);
51
+
52
+ // console.log({ [PaginationStrategyConstructor.name]: paginationStrategy });
53
+
54
+ return paginationStrategy;
55
+ }
@@ -0,0 +1,44 @@
1
+ import { parseUrl } from '../pagination-utils';
2
+
3
+ export class PaginationStrategy {
4
+ public doc = document;
5
+ public url: URL;
6
+ public paginationSelector = '.pagination';
7
+ public searchParamSelector = 'page';
8
+ public static _pathnameSelector = /\/(page\/)?\d+\/?$/;
9
+ public pathnameSelector = /\/(\d+)\/?$/;
10
+ public dataparamSelector = '[data-parameters *= from]';
11
+ public overwritePaginationLast?: (n: number, offset?: number) => number;
12
+ public offsetMin = 1;
13
+
14
+ constructor(options?: Partial<PaginationStrategy>) {
15
+ if (options) {
16
+ Object.entries(options).forEach(([k, v]) => {
17
+ Object.assign(this, { [k]: v });
18
+ });
19
+ }
20
+
21
+ this.url = parseUrl(options?.url || this.doc.URL);
22
+ }
23
+
24
+ getPaginationElement() {
25
+ return this.doc.querySelector<HTMLElement>(this.paginationSelector);
26
+ }
27
+
28
+ get hasPagination() {
29
+ return !!this.getPaginationElement();
30
+ }
31
+
32
+ getPaginationOffset() {
33
+ return this.offsetMin;
34
+ }
35
+
36
+ getPaginationLast() {
37
+ if (this.overwritePaginationLast) return this.overwritePaginationLast(1);
38
+ return 1;
39
+ }
40
+
41
+ getPaginationUrlGenerator(): (offset: number) => string | Promise<string> {
42
+ return (_: number) => this.url.href;
43
+ }
44
+ }