nodebb-plugin-ezoic-infinite 1.8.18 → 1.8.19
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/library.js +4 -43
- package/package.json +2 -2
- package/public/client.js +366 -128
- package/public/style.css +9 -15
package/library.js
CHANGED
|
@@ -75,45 +75,6 @@ async function isUserExcluded(uid, excludedGroups) {
|
|
|
75
75
|
return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// ── Groups cache (5 min TTL) ───────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
let _groupsCache = null;
|
|
83
|
-
let _groupsCacheAt = 0;
|
|
84
|
-
const GROUPS_TTL = 5 * 60_000;
|
|
85
|
-
|
|
86
|
-
async function getAllGroupsCached() {
|
|
87
|
-
const now = Date.now();
|
|
88
|
-
if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
|
|
89
|
-
const g = await getAllGroups();
|
|
90
|
-
_groupsCache = g;
|
|
91
|
-
_groupsCacheAt = Date.now();
|
|
92
|
-
return _groupsCache;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ── Exclusion cache (per uid, 30s TTL) ─────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
const _excludedCache = new Map(); // uid -> { v:boolean, t:number, sig:string }
|
|
98
|
-
const EXCLUDED_TTL = 30_000;
|
|
99
|
-
const EXCLUDED_MAX = 10_000;
|
|
100
|
-
|
|
101
|
-
function excludedSig(excludedGroups) {
|
|
102
|
-
// signature stable to invalidate when groups list changes
|
|
103
|
-
return excludedGroups.join('\u0001');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function isUserExcludedCached(uid, excludedGroups) {
|
|
107
|
-
if (!uid || !excludedGroups.length) return false;
|
|
108
|
-
const now = Date.now();
|
|
109
|
-
const sig = excludedSig(excludedGroups);
|
|
110
|
-
const hit = _excludedCache.get(uid);
|
|
111
|
-
if (hit && hit.sig === sig && (now - hit.t) < EXCLUDED_TTL) return hit.v;
|
|
112
|
-
const v = await isUserExcluded(uid, excludedGroups);
|
|
113
|
-
if (_excludedCache.size > EXCLUDED_MAX) _excludedCache.clear();
|
|
114
|
-
_excludedCache.set(uid, { v, t: now, sig });
|
|
115
|
-
return v;
|
|
116
|
-
}
|
|
117
78
|
// ── Scripts Ezoic ──────────────────────────────────────────────────────────
|
|
118
79
|
|
|
119
80
|
const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
|
|
@@ -127,7 +88,7 @@ ezstandalone.cmd = ezstandalone.cmd || [];
|
|
|
127
88
|
// ── Hooks ──────────────────────────────────────────────────────────────────
|
|
128
89
|
|
|
129
90
|
plugin.onSettingsSet = function (data) {
|
|
130
|
-
if (data && data.hash === SETTINGS_KEY)
|
|
91
|
+
if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
|
|
131
92
|
};
|
|
132
93
|
|
|
133
94
|
plugin.addAdminNavigation = async (header) => {
|
|
@@ -150,7 +111,7 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
150
111
|
try {
|
|
151
112
|
const settings = await getSettings();
|
|
152
113
|
const uid = data.req?.uid ?? 0;
|
|
153
|
-
const excluded = await
|
|
114
|
+
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
154
115
|
if (!excluded) {
|
|
155
116
|
// Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
|
|
156
117
|
data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
|
|
@@ -162,7 +123,7 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
162
123
|
plugin.init = async ({ router, middleware }) => {
|
|
163
124
|
async function render(req, res) {
|
|
164
125
|
const settings = await getSettings();
|
|
165
|
-
const allGroups = await
|
|
126
|
+
const allGroups = await getAllGroups();
|
|
166
127
|
res.render('admin/plugins/ezoic-infinite', {
|
|
167
128
|
title: 'Ezoic Infinite Ads',
|
|
168
129
|
...settings,
|
|
@@ -177,7 +138,7 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
177
138
|
|
|
178
139
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
179
140
|
const settings = await getSettings();
|
|
180
|
-
const excluded = await
|
|
141
|
+
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
181
142
|
res.json({
|
|
182
143
|
excluded,
|
|
183
144
|
enableBetweenAds: settings.enableBetweenAds,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.19",
|
|
4
4
|
"description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
"compatibility": "^4.0.0"
|
|
19
19
|
},
|
|
20
20
|
"private": false
|
|
21
|
-
}
|
|
21
|
+
}
|
package/public/client.js
CHANGED
|
@@ -77,12 +77,18 @@
|
|
|
77
77
|
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
78
78
|
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
// Tunables (stables en prod)
|
|
81
81
|
const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
|
|
82
|
-
const MAX_INSERTS_RUN =
|
|
83
|
-
const MAX_INFLIGHT =
|
|
84
|
-
const
|
|
85
|
-
const
|
|
82
|
+
const MAX_INSERTS_RUN = 10; // plus réactif si NodeBB injecte en rafale
|
|
83
|
+
const MAX_INFLIGHT = 2; // ids max simultanés en vol (garde-fou)
|
|
84
|
+
const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
|
|
85
|
+
const SHOW_THROTTLE_MS = 500; // anti-spam showAds() par id (plus réactif)
|
|
86
|
+
const SHOW_RELEASE_MS = 300; // relâche inflight après showAds() batché
|
|
87
|
+
const SHOW_FAILSAFE_MS = 7000; // relâche forcée si stack pub lente
|
|
88
|
+
const BATCH_FLUSH_MS = 30; // micro-buffer pour regrouper les ids proches
|
|
89
|
+
const MAX_DESTROY_BATCH = 4; // ids max par destroyPlaceholders(...ids)
|
|
90
|
+
const DESTROY_FLUSH_MS = 30; // micro-buffer destroy pour lisser les rafales
|
|
91
|
+
const BURST_COOLDOWN_MS = 100; // délai min entre deux déclenchements de burst
|
|
86
92
|
|
|
87
93
|
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
88
94
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
@@ -126,7 +132,18 @@
|
|
|
126
132
|
inflight: 0, // showAds() en cours
|
|
127
133
|
pending: [], // ids en attente de slot inflight
|
|
128
134
|
pendingSet: new Set(),
|
|
135
|
+
showBatchTimer: 0,
|
|
136
|
+
destroyBatchTimer: 0,
|
|
137
|
+
destroyPending: [],
|
|
138
|
+
destroyPendingSet: new Set(),
|
|
139
|
+
sweepQueued: false,
|
|
129
140
|
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
141
|
+
ezActiveIds: new Set(), // ids actifs côté plugin (wrap présent / récemment show)
|
|
142
|
+
ezShownSinceDestroy: new Set(), // ids déjà show depuis le dernier destroy Ezoic
|
|
143
|
+
scrollDir: 1, // 1=bas, -1=haut
|
|
144
|
+
scrollSpeed: 0, // px/s approx (EMA)
|
|
145
|
+
lastScrollY: 0,
|
|
146
|
+
lastScrollTs: 0,
|
|
130
147
|
runQueued: false,
|
|
131
148
|
burstActive: false,
|
|
132
149
|
burstDeadline: 0,
|
|
@@ -142,10 +159,120 @@
|
|
|
142
159
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
143
160
|
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
144
161
|
|
|
162
|
+
function healFalseEmpty(root = document) {
|
|
163
|
+
try {
|
|
164
|
+
const list = [];
|
|
165
|
+
if (root instanceof Element && root.classList?.contains(WRAP_CLASS)) list.push(root);
|
|
166
|
+
const found = root.querySelectorAll ? root.querySelectorAll(`.${WRAP_CLASS}.is-empty`) : [];
|
|
167
|
+
for (const w of found) list.push(w);
|
|
168
|
+
for (const w of list) {
|
|
169
|
+
if (!w?.classList?.contains('is-empty')) continue;
|
|
170
|
+
if (isFilled(w)) w.classList.remove('is-empty');
|
|
171
|
+
}
|
|
172
|
+
} catch (_) {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function phEl(id) {
|
|
176
|
+
return document.getElementById(`${PH_PREFIX}${id}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function hasSinglePlaceholder(id) {
|
|
180
|
+
try { return document.querySelectorAll(`#${PH_PREFIX}${id}`).length === 1; } catch (_) { return false; }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function canShowPlaceholderId(id, now = ts()) {
|
|
184
|
+
const n = parseInt(id, 10);
|
|
185
|
+
if (!Number.isFinite(n) || n <= 0) return false;
|
|
186
|
+
if (now - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
|
|
187
|
+
const ph = phEl(n);
|
|
188
|
+
if (!ph?.isConnected || isFilled(ph)) return false;
|
|
189
|
+
if (!hasSinglePlaceholder(n)) return false;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function queueSweepDeadWraps() {
|
|
194
|
+
if (S.sweepQueued) return;
|
|
195
|
+
S.sweepQueued = true;
|
|
196
|
+
requestAnimationFrame(() => {
|
|
197
|
+
S.sweepQueued = false;
|
|
198
|
+
sweepDeadWraps();
|
|
199
|
+
healFalseEmpty();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getDynamicShowBatchMax() {
|
|
204
|
+
const speed = S.scrollSpeed || 0;
|
|
205
|
+
const pend = S.pending.length;
|
|
206
|
+
// Scroll très rapide => petits batches (réduit le churn/unused)
|
|
207
|
+
if (speed > 2600) return 2;
|
|
208
|
+
if (speed > 1400) return 3;
|
|
209
|
+
// Peu de candidats => flush plus vite, inutile d'attendre 4
|
|
210
|
+
if (pend <= 1) return 1;
|
|
211
|
+
if (pend <= 3) return 2;
|
|
212
|
+
// Par défaut compromis dynamique
|
|
213
|
+
return 3;
|
|
214
|
+
}
|
|
215
|
+
|
|
145
216
|
function mutate(fn) {
|
|
146
217
|
S.mutGuard++;
|
|
147
218
|
try { fn(); } finally { S.mutGuard--; }
|
|
148
219
|
}
|
|
220
|
+
function scheduleDestroyFlush() {
|
|
221
|
+
if (S.destroyBatchTimer) return;
|
|
222
|
+
S.destroyBatchTimer = setTimeout(() => {
|
|
223
|
+
S.destroyBatchTimer = 0;
|
|
224
|
+
flushDestroyBatch();
|
|
225
|
+
}, DESTROY_FLUSH_MS);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function flushDestroyBatch() {
|
|
229
|
+
if (!S.destroyPending.length) return;
|
|
230
|
+
const ids = [];
|
|
231
|
+
while (S.destroyPending.length && ids.length < MAX_DESTROY_BATCH) {
|
|
232
|
+
const id = S.destroyPending.shift();
|
|
233
|
+
S.destroyPendingSet.delete(id);
|
|
234
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
235
|
+
ids.push(id);
|
|
236
|
+
}
|
|
237
|
+
if (ids.length) {
|
|
238
|
+
try {
|
|
239
|
+
const ez = window.ezstandalone;
|
|
240
|
+
const run = () => { try { ez?.destroyPlaceholders?.(ids); } catch (_) {} };
|
|
241
|
+
try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
|
|
242
|
+
} catch (_) {}
|
|
243
|
+
}
|
|
244
|
+
if (S.destroyPending.length) scheduleDestroyFlush();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function destroyEzoicId(id) {
|
|
248
|
+
if (!Number.isFinite(id) || id <= 0) return;
|
|
249
|
+
if (!S.ezActiveIds.has(id)) return;
|
|
250
|
+
S.ezActiveIds.delete(id);
|
|
251
|
+
if (!S.destroyPendingSet.has(id)) {
|
|
252
|
+
S.destroyPending.push(id);
|
|
253
|
+
S.destroyPendingSet.add(id);
|
|
254
|
+
}
|
|
255
|
+
scheduleDestroyFlush();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function destroyBeforeReuse(ids) {
|
|
259
|
+
const out = [];
|
|
260
|
+
const toDestroy = [];
|
|
261
|
+
const seen = new Set();
|
|
262
|
+
for (const raw of (ids || [])) {
|
|
263
|
+
const id = parseInt(raw, 10);
|
|
264
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
265
|
+
seen.add(id);
|
|
266
|
+
out.push(id);
|
|
267
|
+
if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
|
|
268
|
+
}
|
|
269
|
+
if (toDestroy.length) {
|
|
270
|
+
try { window.ezstandalone?.destroyPlaceholders?.(toDestroy); } catch (_) {}
|
|
271
|
+
for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
149
276
|
|
|
150
277
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
151
278
|
|
|
@@ -160,7 +287,7 @@
|
|
|
160
287
|
|
|
161
288
|
function parseIds(raw) {
|
|
162
289
|
const out = [], seen = new Set();
|
|
163
|
-
for (const v of String(raw || '').split(/
|
|
290
|
+
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
164
291
|
const n = parseInt(v, 10);
|
|
165
292
|
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
166
293
|
}
|
|
@@ -300,6 +427,25 @@
|
|
|
300
427
|
return null;
|
|
301
428
|
}
|
|
302
429
|
|
|
430
|
+
function sweepDeadWraps() {
|
|
431
|
+
// NodeBB peut retirer des nœuds sans passer par dropWrap() (virtualisation / rerender).
|
|
432
|
+
// On libère alors les IDs/keys fantômes pour éviter l'épuisement du pool.
|
|
433
|
+
for (const [key, wrap] of Array.from(S.wrapByKey.entries())) {
|
|
434
|
+
if (wrap?.isConnected) continue;
|
|
435
|
+
S.wrapByKey.delete(key);
|
|
436
|
+
const id = parseInt(wrap?.getAttribute?.(A_WRAPID), 10);
|
|
437
|
+
if (Number.isFinite(id)) {
|
|
438
|
+
S.mountedIds.delete(id);
|
|
439
|
+
S.pendingSet.delete(id);
|
|
440
|
+
S.lastShow.delete(id);
|
|
441
|
+
S.ezActiveIds.delete(id);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (S.pending.length) {
|
|
445
|
+
S.pending = S.pending.filter(id => S.pendingSet.has(id));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
303
449
|
/**
|
|
304
450
|
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
305
451
|
* Séquence avec délais (destroyPlaceholders est asynchrone) :
|
|
@@ -307,60 +453,84 @@
|
|
|
307
453
|
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
308
454
|
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
309
455
|
*/
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
456
|
+
function recycleAndMove(klass, targetEl, newKey) {
|
|
457
|
+
const ez = window.ezstandalone;
|
|
458
|
+
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
459
|
+
typeof ez?.define !== 'function' ||
|
|
460
|
+
typeof ez?.displayMore !== 'function') return null;
|
|
461
|
+
|
|
462
|
+
const vh = window.innerHeight || 800;
|
|
463
|
+
const preferAbove = S.scrollDir >= 0; // scroll bas => recycle en haut
|
|
464
|
+
const farAbove = -vh;
|
|
465
|
+
const farBelow = vh * 2;
|
|
466
|
+
|
|
467
|
+
let bestPrefEmpty = null, bestPrefMetric = Infinity;
|
|
468
|
+
let bestPrefFilled = null, bestPrefFilledMetric = Infinity;
|
|
469
|
+
let bestAnyEmpty = null, bestAnyMetric = Infinity;
|
|
470
|
+
let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
|
|
471
|
+
|
|
472
|
+
for (const wrap of S.wrapByKey.values()) {
|
|
473
|
+
if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
|
|
474
|
+
try {
|
|
475
|
+
const rect = wrap.getBoundingClientRect();
|
|
476
|
+
const isAbove = rect.bottom <= farAbove;
|
|
477
|
+
const isBelow = rect.top >= farBelow;
|
|
478
|
+
const anyFar = isAbove || isBelow;
|
|
479
|
+
if (!anyFar) continue;
|
|
480
|
+
|
|
481
|
+
const qualifies = preferAbove ? isAbove : isBelow;
|
|
482
|
+
const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
|
|
483
|
+
const filled = isFilled(wrap);
|
|
484
|
+
|
|
485
|
+
if (qualifies) {
|
|
486
|
+
if (!filled) {
|
|
487
|
+
if (metric < bestPrefMetric) { bestPrefMetric = metric; bestPrefEmpty = wrap; }
|
|
329
488
|
} else {
|
|
330
|
-
if (
|
|
489
|
+
if (metric < bestPrefFilledMetric) { bestPrefFilledMetric = metric; bestPrefFilled = wrap; }
|
|
331
490
|
}
|
|
332
|
-
}
|
|
333
|
-
|
|
491
|
+
}
|
|
492
|
+
if (!filled) {
|
|
493
|
+
if (metric < bestAnyMetric) { bestAnyMetric = metric; bestAnyEmpty = wrap; }
|
|
494
|
+
} else {
|
|
495
|
+
if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
|
|
496
|
+
}
|
|
497
|
+
} catch (_) {}
|
|
498
|
+
}
|
|
334
499
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
500
|
+
const best = bestPrefEmpty ?? bestPrefFilled ?? bestAnyEmpty ?? bestAnyFilled;
|
|
501
|
+
if (!best) return null;
|
|
502
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
503
|
+
if (!Number.isFinite(id)) return null;
|
|
504
|
+
|
|
505
|
+
const oldKey = best.getAttribute(A_ANCHOR);
|
|
506
|
+
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
507
|
+
mutate(() => {
|
|
508
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
509
|
+
best.setAttribute(A_CREATED, String(ts()));
|
|
510
|
+
best.setAttribute(A_SHOWN, '0');
|
|
511
|
+
best.classList.remove('is-empty');
|
|
512
|
+
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
513
|
+
if (ph) ph.innerHTML = '';
|
|
514
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
515
|
+
});
|
|
516
|
+
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
517
|
+
S.wrapByKey.set(newKey, best);
|
|
518
|
+
|
|
519
|
+
const doDestroy = () => {
|
|
520
|
+
if (S.ezShownSinceDestroy.has(id)) {
|
|
521
|
+
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
522
|
+
S.ezShownSinceDestroy.delete(id);
|
|
523
|
+
}
|
|
524
|
+
S.ezActiveIds.delete(id);
|
|
525
|
+
setTimeout(doDefine, 330);
|
|
526
|
+
};
|
|
527
|
+
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
528
|
+
const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); S.ezShownSinceDestroy.add(id); } catch (_) {} };
|
|
529
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
355
530
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
359
|
-
const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
|
|
360
|
-
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
531
|
+
return { id, wrap: best };
|
|
532
|
+
}
|
|
361
533
|
|
|
362
|
-
return { id, wrap: best };
|
|
363
|
-
}
|
|
364
534
|
|
|
365
535
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
366
536
|
|
|
@@ -396,7 +566,7 @@
|
|
|
396
566
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
397
567
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
398
568
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
399
|
-
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
569
|
+
if (Number.isFinite(id)) { S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
|
|
400
570
|
const key = w.getAttribute(A_ANCHOR);
|
|
401
571
|
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
402
572
|
w.remove();
|
|
@@ -422,13 +592,6 @@
|
|
|
422
592
|
const klass = 'ezoic-ad-between';
|
|
423
593
|
const cfg = KIND[klass];
|
|
424
594
|
|
|
425
|
-
// Build a fast lookup of existing anchors once (avoid querySelector per wrap)
|
|
426
|
-
const anchors = new Set();
|
|
427
|
-
document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`).forEach(el => {
|
|
428
|
-
const v = el.getAttribute(cfg.anchorAttr);
|
|
429
|
-
if (v) anchors.add(String(v));
|
|
430
|
-
});
|
|
431
|
-
|
|
432
595
|
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
433
596
|
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
434
597
|
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
@@ -437,7 +600,8 @@
|
|
|
437
600
|
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
438
601
|
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
439
602
|
|
|
440
|
-
|
|
603
|
+
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
604
|
+
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
441
605
|
});
|
|
442
606
|
}
|
|
443
607
|
|
|
@@ -477,7 +641,8 @@
|
|
|
477
641
|
const key = anchorKey(klass, el);
|
|
478
642
|
if (findWrap(key)) continue;
|
|
479
643
|
|
|
480
|
-
|
|
644
|
+
let id = pickId(poolKey);
|
|
645
|
+
if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
|
|
481
646
|
if (id) {
|
|
482
647
|
const w = insertAfter(el, id, klass, key);
|
|
483
648
|
if (w) { observePh(id); inserted++; }
|
|
@@ -508,76 +673,107 @@
|
|
|
508
673
|
}
|
|
509
674
|
|
|
510
675
|
function observePh(id) {
|
|
511
|
-
const ph =
|
|
676
|
+
const ph = phEl(id);
|
|
512
677
|
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
678
|
+
// Fast-path : si déjà proche viewport, ne pas attendre un callback IO complet
|
|
679
|
+
try {
|
|
680
|
+
if (!ph?.isConnected) return;
|
|
681
|
+
const rect = ph.getBoundingClientRect();
|
|
682
|
+
const vh = window.innerHeight || 800;
|
|
683
|
+
const preload = isMobile() ? 1400 : 1000;
|
|
684
|
+
if (rect.top <= vh + preload && rect.bottom >= -preload) enqueueShow(id);
|
|
685
|
+
} catch (_) {}
|
|
513
686
|
}
|
|
514
687
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
688
|
+
function enqueueShow(id) {
|
|
689
|
+
if (!id || isBlocked()) return;
|
|
690
|
+
const n = parseInt(id, 10);
|
|
691
|
+
if (!Number.isFinite(n) || n <= 0) return;
|
|
692
|
+
if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
693
|
+
if (!S.pendingSet.has(n)) { S.pending.push(n); S.pendingSet.add(n); }
|
|
694
|
+
scheduleDrainQueue();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function scheduleDrainQueue() {
|
|
698
|
+
if (isBlocked()) return;
|
|
699
|
+
if (S.showBatchTimer) return;
|
|
700
|
+
S.showBatchTimer = setTimeout(() => {
|
|
701
|
+
S.showBatchTimer = 0;
|
|
702
|
+
drainQueue();
|
|
703
|
+
}, BATCH_FLUSH_MS);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function drainQueue() {
|
|
707
|
+
if (isBlocked()) return;
|
|
708
|
+
const free = Math.max(0, MAX_INFLIGHT - S.inflight);
|
|
709
|
+
if (!free || !S.pending.length) return;
|
|
710
|
+
|
|
711
|
+
const picked = [];
|
|
712
|
+
const seen = new Set();
|
|
713
|
+
const batchCap = Math.max(1, Math.min(MAX_SHOW_BATCH, free, getDynamicShowBatchMax()));
|
|
714
|
+
while (S.pending.length && picked.length < batchCap) {
|
|
715
|
+
const id = S.pending.shift();
|
|
716
|
+
S.pendingSet.delete(id);
|
|
717
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
718
|
+
if (!phEl(id)?.isConnected) continue;
|
|
719
|
+
seen.add(id);
|
|
720
|
+
picked.push(id);
|
|
523
721
|
}
|
|
722
|
+
if (picked.length) startShowBatch(picked);
|
|
723
|
+
if (S.pending.length && (MAX_INFLIGHT - S.inflight) > 0) scheduleDrainQueue();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function startShowBatch(ids) {
|
|
727
|
+
if (!ids?.length || isBlocked()) return;
|
|
728
|
+
const reserve = ids.length;
|
|
729
|
+
S.inflight += reserve;
|
|
730
|
+
|
|
731
|
+
let done = false;
|
|
732
|
+
const release = () => {
|
|
733
|
+
if (done) return;
|
|
734
|
+
done = true;
|
|
735
|
+
S.inflight = Math.max(0, S.inflight - reserve);
|
|
736
|
+
drainQueue();
|
|
737
|
+
};
|
|
738
|
+
const timer = setTimeout(release, SHOW_FAILSAFE_MS);
|
|
524
739
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const id = S.pending.shift();
|
|
529
|
-
S.pendingSet.delete(id);
|
|
530
|
-
startShow(id);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
740
|
+
requestAnimationFrame(() => {
|
|
741
|
+
try {
|
|
742
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
533
743
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
S.inflight++;
|
|
537
|
-
let done = false;
|
|
538
|
-
const release = () => {
|
|
539
|
-
if (done) return;
|
|
540
|
-
done = true;
|
|
541
|
-
S.inflight = Math.max(0, S.inflight - 1);
|
|
542
|
-
drainQueue();
|
|
543
|
-
};
|
|
544
|
-
const timer = setTimeout(release, 7000);
|
|
744
|
+
const valid = [];
|
|
745
|
+
const t = ts();
|
|
545
746
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (
|
|
549
|
-
const ph =
|
|
550
|
-
if (!
|
|
747
|
+
for (const raw of ids) {
|
|
748
|
+
const id = parseInt(raw, 10);
|
|
749
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
750
|
+
const ph = phEl(id);
|
|
751
|
+
if (!canShowPlaceholderId(id, t)) continue;
|
|
551
752
|
|
|
552
|
-
const t = ts();
|
|
553
|
-
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
554
753
|
S.lastShow.set(id, t);
|
|
555
|
-
|
|
556
754
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
755
|
+
valid.push(id);
|
|
756
|
+
}
|
|
557
757
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
758
|
+
if (!valid.length) { clearTimeout(timer); return release(); }
|
|
759
|
+
|
|
760
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
761
|
+
const ez = window.ezstandalone;
|
|
762
|
+
const doShow = () => {
|
|
763
|
+
const prepared = destroyBeforeReuse(valid);
|
|
764
|
+
if (!prepared.length) { setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS); return; }
|
|
765
|
+
try { ez.showAds(...prepared); } catch (_) {}
|
|
766
|
+
for (const id of prepared) {
|
|
767
|
+
S.ezActiveIds.add(id);
|
|
768
|
+
S.ezShownSinceDestroy.add(id);
|
|
769
|
+
}
|
|
770
|
+
setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
|
|
771
|
+
};
|
|
772
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
773
|
+
} catch (_) { clearTimeout(timer); release(); }
|
|
774
|
+
});
|
|
775
|
+
}
|
|
569
776
|
|
|
570
|
-
function scheduleEmptyCheck(id, showTs) {
|
|
571
|
-
setTimeout(() => {
|
|
572
|
-
try {
|
|
573
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
574
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
575
|
-
if (!wrap || !ph?.isConnected) return;
|
|
576
|
-
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
577
|
-
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
578
|
-
} catch (_) {}
|
|
579
|
-
}, EMPTY_CHECK_MS);
|
|
580
|
-
}
|
|
581
777
|
|
|
582
778
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
583
779
|
//
|
|
@@ -595,14 +791,21 @@
|
|
|
595
791
|
const orig = ez.showAds.bind(ez);
|
|
596
792
|
ez.showAds = function (...args) {
|
|
597
793
|
if (isBlocked()) return;
|
|
598
|
-
const ids
|
|
794
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
795
|
+
const valid = [];
|
|
599
796
|
const seen = new Set();
|
|
600
797
|
for (const v of ids) {
|
|
601
798
|
const id = parseInt(v, 10);
|
|
602
799
|
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
603
|
-
if (!
|
|
800
|
+
if (!phEl(id)?.isConnected || !hasSinglePlaceholder(id)) continue;
|
|
604
801
|
seen.add(id);
|
|
605
|
-
|
|
802
|
+
valid.push(id);
|
|
803
|
+
}
|
|
804
|
+
if (!valid.length) return;
|
|
805
|
+
try { orig(...valid); } catch (_) {
|
|
806
|
+
for (const id of valid) {
|
|
807
|
+
try { orig(id); } catch (_) {}
|
|
808
|
+
}
|
|
606
809
|
}
|
|
607
810
|
};
|
|
608
811
|
} catch (_) {}
|
|
@@ -619,6 +822,7 @@
|
|
|
619
822
|
async function runCore() {
|
|
620
823
|
if (isBlocked()) return 0;
|
|
621
824
|
patchShowAds();
|
|
825
|
+
sweepDeadWraps();
|
|
622
826
|
|
|
623
827
|
const cfg = await fetchConfig();
|
|
624
828
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -685,7 +889,7 @@
|
|
|
685
889
|
S.burstCount++;
|
|
686
890
|
scheduleRun(n => {
|
|
687
891
|
if (!n && !S.pending.length) { S.burstActive = false; return; }
|
|
688
|
-
setTimeout(step, n > 0 ?
|
|
892
|
+
setTimeout(step, n > 0 ? 80 : 180);
|
|
689
893
|
});
|
|
690
894
|
};
|
|
691
895
|
step();
|
|
@@ -703,11 +907,21 @@
|
|
|
703
907
|
S.mountedIds.clear();
|
|
704
908
|
S.lastShow.clear();
|
|
705
909
|
S.wrapByKey.clear();
|
|
910
|
+
S.ezActiveIds.clear();
|
|
911
|
+
S.ezShownSinceDestroy.clear();
|
|
706
912
|
S.inflight = 0;
|
|
707
913
|
S.pending = [];
|
|
708
914
|
S.pendingSet.clear();
|
|
915
|
+
if (S.showBatchTimer) { clearTimeout(S.showBatchTimer); S.showBatchTimer = 0; }
|
|
916
|
+
if (S.destroyBatchTimer) { clearTimeout(S.destroyBatchTimer); S.destroyBatchTimer = 0; }
|
|
917
|
+
S.destroyPending = [];
|
|
918
|
+
S.destroyPendingSet.clear();
|
|
709
919
|
S.burstActive = false;
|
|
710
920
|
S.runQueued = false;
|
|
921
|
+
S.sweepQueued = false;
|
|
922
|
+
S.scrollSpeed = 0;
|
|
923
|
+
S.lastScrollY = 0;
|
|
924
|
+
S.lastScrollTs = 0;
|
|
711
925
|
}
|
|
712
926
|
|
|
713
927
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
@@ -718,8 +932,17 @@
|
|
|
718
932
|
S.domObs = new MutationObserver(muts => {
|
|
719
933
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
720
934
|
for (const m of muts) {
|
|
935
|
+
let sawWrapRemoval = false;
|
|
936
|
+
for (const n of m.removedNodes) {
|
|
937
|
+
if (n.nodeType !== 1) continue;
|
|
938
|
+
if ((n.matches && n.matches(`.${WRAP_CLASS}`)) || (n.querySelector && n.querySelector(`.${WRAP_CLASS}`))) {
|
|
939
|
+
sawWrapRemoval = true;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (sawWrapRemoval) queueSweepDeadWraps();
|
|
721
943
|
for (const n of m.addedNodes) {
|
|
722
944
|
if (n.nodeType !== 1) continue;
|
|
945
|
+
try { healFalseEmpty(n); } catch (_) {}
|
|
723
946
|
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
724
947
|
if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
|
|
725
948
|
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
@@ -805,7 +1028,7 @@
|
|
|
805
1028
|
S.pageKey = pageKey();
|
|
806
1029
|
blockedUntil = 0;
|
|
807
1030
|
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
808
|
-
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
1031
|
+
patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
|
|
809
1032
|
});
|
|
810
1033
|
|
|
811
1034
|
const burstEvts = [
|
|
@@ -827,7 +1050,22 @@
|
|
|
827
1050
|
|
|
828
1051
|
function bindScroll() {
|
|
829
1052
|
let ticking = false;
|
|
1053
|
+
try {
|
|
1054
|
+
S.lastScrollY = window.scrollY || window.pageYOffset || 0;
|
|
1055
|
+
S.lastScrollTs = ts();
|
|
1056
|
+
} catch (_) {}
|
|
830
1057
|
window.addEventListener('scroll', () => {
|
|
1058
|
+
try {
|
|
1059
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
1060
|
+
const t = ts();
|
|
1061
|
+
const dy = y - (S.lastScrollY || 0);
|
|
1062
|
+
const dt = Math.max(1, t - (S.lastScrollTs || t));
|
|
1063
|
+
if (Math.abs(dy) > 1) S.scrollDir = dy >= 0 ? 1 : -1;
|
|
1064
|
+
const inst = Math.abs(dy) * 1000 / dt;
|
|
1065
|
+
S.scrollSpeed = S.scrollSpeed ? (S.scrollSpeed * 0.7 + inst * 0.3) : inst;
|
|
1066
|
+
S.lastScrollY = y;
|
|
1067
|
+
S.lastScrollTs = t;
|
|
1068
|
+
} catch (_) {}
|
|
831
1069
|
if (ticking) return;
|
|
832
1070
|
ticking = true;
|
|
833
1071
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
package/public/style.css
CHANGED
|
@@ -56,23 +56,17 @@
|
|
|
56
56
|
top: auto !important;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/* ── État vide ────────────────────────────────────────────────────────────── */
|
|
60
|
-
/*
|
|
61
|
-
Ajouté 20s après showAds si aucun fill détecté.
|
|
62
|
-
Collapse à 1px (pas 0) : reste observable par l'IO si le fill arrive tard.
|
|
63
|
-
*/
|
|
64
|
-
.nodebb-ezoic-wrap.is-empty {
|
|
65
|
-
display: block !important;
|
|
66
|
-
height: 1px !important;
|
|
67
|
-
min-height: 1px !important;
|
|
68
|
-
max-height: 1px !important;
|
|
69
|
-
margin: 0 !important;
|
|
70
|
-
padding: 0 !important;
|
|
71
|
-
overflow: hidden !important;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
59
|
/* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
|
|
75
60
|
.ezoic-ad {
|
|
76
61
|
margin: 0 !important;
|
|
77
62
|
padding: 0 !important;
|
|
78
63
|
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/* Filet anti faux-empty : si la pub est rendue, ne pas laisser le wrap replié */
|
|
67
|
+
.nodebb-ezoic-wrap.is-empty:has(iframe, [data-google-container-id], [id^="google_ads_iframe_"]) {
|
|
68
|
+
height: auto !important;
|
|
69
|
+
min-height: 1px !important;
|
|
70
|
+
max-height: none !important;
|
|
71
|
+
overflow: visible !important;
|
|
72
|
+
}
|