nodebb-plugin-ezoic-infinite 0.9.7 → 0.9.8

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 CHANGED
@@ -2,96 +2,56 @@
2
2
 
3
3
  const meta = require.main.require('./src/meta');
4
4
  const groups = require.main.require('./src/groups');
5
- const db = require.main.require('./src/database');
6
5
 
7
- const SETTINGS_KEY = 'ezoic-infinite';
8
- const plugin = {};
6
+ const Plugin = {};
9
7
 
10
- function normalizeExcludedGroups(value) {
11
- if (!value) return [];
12
- if (Array.isArray(value)) return value;
13
- return String(value).split(',').map(s => s.trim()).filter(Boolean);
14
- }
8
+ Plugin.init = async function (params) {
9
+ const { router, middleware } = params;
15
10
 
16
- function parseBool(v, def = false) {
17
- if (v === undefined || v === null || v === '') return def;
18
- if (typeof v === 'boolean') return v;
19
- const s = String(v).toLowerCase();
20
- return s === '1' || s === 'true' || s === 'on' || s === 'yes';
21
- }
11
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
12
+ router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
13
+ };
22
14
 
23
- async function getAllGroups() {
24
- const names = await db.getSortedSetRange('groups:createtime', 0, -1);
25
- const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
26
- const data = await groups.getGroupsData(filtered);
27
- // Sort alphabetically for ACP usability
28
- data.sort((a, b) => String(a.name).localeCompare(String(b.name), 'fr', { sensitivity: 'base' }));
29
- return data;
30
- }
31
- async function getSettings() {
32
- const s = await meta.settings.get(SETTINGS_KEY);
33
- return {
34
- // Between-post ads (simple blocks)
35
- enableBetweenAds: parseBool(s.enableBetweenAds, true),
36
- placeholderIds: (s.placeholderIds || '').trim(),
37
- intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
15
+ async function renderAdmin(req, res) {
16
+ const settings = await meta.settings.get('ezoic-infinite');
17
+
18
+ let groupNames = [];
19
+ try {
20
+ groupNames = await groups.getGroupsFromSet('groups:createtime', 0, -1);
21
+ } catch (e) {
22
+ try {
23
+ groupNames = await groups.getGroupsFromSet('groups:visible:createtime', 0, -1);
24
+ } catch (e2) {
25
+ groupNames = [];
26
+ }
27
+ }
38
28
 
39
- // "Ad message" between replies (looks like a post)
40
- enableMessageAds: parseBool(s.enableMessageAds, false),
41
- messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
42
- messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
29
+ let groupList = [];
30
+ try {
31
+ groupList = await groups.getGroupsData(groupNames);
32
+ } catch (e) {
33
+ groupList = groupNames.map((name) => ({ name }));
34
+ }
43
35
 
44
- excludedGroups: normalizeExcludedGroups(s.excludedGroups),
45
- };
46
- }
36
+ groupList = (groupList || [])
37
+ .filter(g => g && g.name)
38
+ .sort((a, b) => (a.name || '').localeCompare(b.name || '', 'fr', { sensitivity: 'base' }));
47
39
 
48
- async function isUserExcluded(uid, excludedGroups) {
49
- if (!uid || !excludedGroups.length) return false;
50
- const userGroups = await groups.getUserGroups([uid]);
51
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
40
+ res.render('admin/plugins/ezoic-infinite', {
41
+ title: 'Ezoic - Publicités Infinite Scroll',
42
+ settings,
43
+ groups: groupList,
44
+ });
52
45
  }
53
46
 
54
- plugin.addAdminNavigation = async (header) => {
47
+ Plugin.addAdminNavigation = async function (header) {
55
48
  header.plugins = header.plugins || [];
56
49
  header.plugins.push({
57
50
  route: '/plugins/ezoic-infinite',
58
- icon: 'fa-ad',
59
- name: 'Ezoic Infinite Ads'
51
+ icon: 'fa-bullhorn',
52
+ name: 'Ezoic Infinite',
60
53
  });
61
54
  return header;
62
55
  };
63
56
 
64
- plugin.init = async ({ router, middleware }) => {
65
- async function render(req, res) {
66
- const settings = await getSettings();
67
- const allGroups = await getAllGroups();
68
-
69
- res.render('admin/plugins/ezoic-infinite', {
70
- title: 'Ezoic Infinite Ads',
71
- ...settings,
72
- enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
73
- enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
74
- allGroups,
75
- });
76
- }
77
-
78
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
79
- router.get('/api/admin/plugins/ezoic-infinite', render);
80
-
81
- router.get('/api/plugins/ezoic-infinite/config', middleware.buildHeader, async (req, res) => {
82
- const settings = await getSettings();
83
- const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
84
-
85
- res.json({
86
- excluded,
87
- enableBetweenAds: settings.enableBetweenAds,
88
- placeholderIds: settings.placeholderIds,
89
- intervalPosts: settings.intervalPosts,
90
- enableMessageAds: settings.enableMessageAds,
91
- messagePlaceholderIds: settings.messagePlaceholderIds,
92
- messageIntervalPosts: settings.messageIntervalPosts,
93
- });
94
- });
95
- };
96
-
97
- module.exports = plugin;
57
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.9.7",
4
- "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
3
+ "version": "0.9.8",
4
+ "description": "Injection de publicités Ezoic entre les topics et entre les messages avec infinite scroll (NodeBB 4.x).",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -11,9 +11,6 @@
11
11
  "ads",
12
12
  "infinite-scroll"
13
13
  ],
14
- "engines": {
15
- "node": ">=18"
16
- },
17
14
  "nbbpm": {
18
15
  "compatibility": "^4.0.0"
19
16
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "nodebb-plugin-ezoic-infinite",
3
- "name": "NodeBB Ezoic Infinite Ads",
4
- "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
3
+ "name": "Ezoic Infinite",
4
+ "description": "Ezoic ads injection with infinite scroll (topics list + topic posts)",
5
5
  "library": "./library.js",
6
6
  "hooks": [
7
7
  {
@@ -22,5 +22,8 @@
22
22
  "scripts": [
23
23
  "public/client.js"
24
24
  ],
25
+ "css": [
26
+ "public/style.css"
27
+ ],
25
28
  "templates": "public/templates"
26
29
  }
package/public/admin.js CHANGED
@@ -1,29 +1,54 @@
1
- /* globals ajaxify */
1
+ /* global $, app, socket */
2
2
  'use strict';
3
3
 
4
- (function () {
5
- function init() {
6
- const $form = $('.ezoic-infinite-settings');
7
- if (!$form.length) return;
8
-
9
- require(['settings', 'alerts'], function (Settings, alerts) {
10
- Settings.load('ezoic-infinite', $form);
11
-
12
- $('#save').off('click.ezoicInfinite').on('click.ezoicInfinite', function (e) {
13
- e.preventDefault();
14
-
15
- Settings.save('ezoic-infinite', $form, function () {
16
- // Toast vert (NodeBB core)
17
- if (alerts && typeof alerts.success === 'function') {
18
- alerts.success('Enregistré');
19
- } else if (window.app && typeof window.app.alertSuccess === 'function') {
20
- window.app.alertSuccess('Enregistré');
21
- }
22
- });
4
+ $(document).ready(function () {
5
+ const namespace = 'ezoic-infinite';
6
+
7
+ function load() {
8
+ socket.emit('admin.settings.get', { hash: namespace }, function (err, data) {
9
+ if (err) return;
10
+ data = data || {};
11
+
12
+ const form = $('.ezoic-infinite-settings');
13
+
14
+ form.find('[name="enableBetweenAds"]').prop('checked', data.enableBetweenAds === true || data.enableBetweenAds === 'on');
15
+ form.find('[name="intervalTopics"]').val(parseInt(data.intervalTopics, 10) || 6);
16
+ form.find('[name="placeholderIds"]').val(data.placeholderIds || '');
17
+
18
+ form.find('[name="enableMessageAds"]').prop('checked', data.enableMessageAds === true || data.enableMessageAds === 'on');
19
+ form.find('[name="messageIntervalPosts"]').val(parseInt(data.messageIntervalPosts, 10) || 3);
20
+ form.find('[name="messagePlaceholderIds"]').val(data.messagePlaceholderIds || '');
21
+
22
+ const selected = (data.excludedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
23
+ form.find('[name="excludedGroups"] option').each(function () {
24
+ $(this).prop('selected', selected.includes($(this).val()));
23
25
  });
24
26
  });
25
27
  }
26
28
 
27
- $(document).ready(init);
28
- $(window).on('action:ajaxify.end', init);
29
- })();
29
+ function save() {
30
+ const form = $('.ezoic-infinite-settings');
31
+ const payload = {
32
+ enableBetweenAds: form.find('[name="enableBetweenAds"]').is(':checked'),
33
+ intervalTopics: parseInt(form.find('[name="intervalTopics"]').val(), 10) || 6,
34
+ placeholderIds: form.find('[name="placeholderIds"]').val() || '',
35
+
36
+ enableMessageAds: form.find('[name="enableMessageAds"]').is(':checked'),
37
+ messageIntervalPosts: parseInt(form.find('[name="messageIntervalPosts"]').val(), 10) || 3,
38
+ messagePlaceholderIds: form.find('[name="messagePlaceholderIds"]').val() || '',
39
+
40
+ excludedGroups: (form.find('[name="excludedGroups"]').val() || []).join(','),
41
+ };
42
+
43
+ socket.emit('admin.settings.set', { hash: namespace, values: payload }, function (err) {
44
+ if (err) {
45
+ app.alertError(err.message || err);
46
+ return;
47
+ }
48
+ app.alertSuccess('Paramètres enregistrés');
49
+ });
50
+ }
51
+
52
+ $('.ezoic-infinite-save').on('click', save);
53
+ load();
54
+ });
package/public/client.js CHANGED
@@ -1,100 +1,114 @@
1
+ /* global $, ajaxify, app, socket */
1
2
  'use strict';
2
3
 
3
- /* globals ajaxify */
4
- window.ezoicInfiniteLoaded = true;
5
-
6
- let cachedConfig;
7
- let lastFetch = 0;
8
- let debounceTimer;
9
-
10
- let inFlight = false;
11
- let rerunRequested = false;
12
-
13
- // Incremental state (prevents ads "jumping to the top")
14
- let pageKey = null;
15
- let injectedSlots = new Set(); // slotNumber per page
16
- let usedIds = new Set(); // ids currently injected per page
17
-
18
- function resetPageState() {
19
- injectedSlots = new Set();
20
- usedIds = new Set();
21
- }
22
-
23
- function currentPageKey() {
24
- // Stable key per ajaxified page
25
- try {
26
- if (ajaxify && ajaxify.data) {
27
- if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
28
- if (ajaxify.data.cid) return 'category:' + ajaxify.data.cid;
29
- if (ajaxify.data.template) return 'tpl:' + ajaxify.data.template + ':' + (ajaxify.data.url || window.location.pathname);
4
+ (function () {
5
+ if (window.ezoicInfiniteLoaded) return;
6
+ window.ezoicInfiniteLoaded = true;
7
+
8
+ const SETTINGS_NS = 'ezoic-infinite';
9
+
10
+ let settings = null;
11
+ let pageKey = null;
12
+
13
+ // State per page
14
+ let usedTopic = new Set(); // ids currently in DOM (topic)
15
+ let usedCat = new Set(); // ids currently in DOM (category)
16
+ let fifoTopic = []; // [{id, afterNo}]
17
+ let fifoCat = []; // [{id, afterPos}]
18
+
19
+ // Refresh single-flight
20
+ let refreshInFlight = false;
21
+ let refreshQueued = false;
22
+
23
+ function parsePool(text) {
24
+ return String(text || '')
25
+ .split(/\r?\n/)
26
+ .map(s => s.trim())
27
+ .filter(Boolean)
28
+ .map(s => parseInt(s, 10))
29
+ .filter(n => Number.isFinite(n) && n > 0);
30
+ }
31
+
32
+ function userExcluded() {
33
+ try {
34
+ const raw = (settings && settings.excludedGroups) ? String(settings.excludedGroups) : '';
35
+ if (!raw) return false;
36
+ const excluded = raw.split(',').map(s => s.trim()).filter(Boolean);
37
+ if (!excluded.length) return false;
38
+ const myGroups = (app.user && app.user.groups) ? app.user.groups : [];
39
+ return excluded.some(g => myGroups.includes(g));
40
+ } catch (e) {
41
+ return false;
30
42
  }
31
- } catch (e) {}
32
- return window.location.pathname;
33
- }
34
-
35
- function parsePool(raw) {
36
- if (!raw) return [];
37
- return Array.from(new Set(
38
- String(raw).split(/[\n,;\s]+/)
39
- .map(x => parseInt(x, 10))
40
- .filter(n => Number.isFinite(n) && n > 0)
41
- ));
42
- }
43
-
44
- async function fetchConfig() {
45
- if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
46
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
47
- cachedConfig = await res.json();
48
- lastFetch = Date.now();
49
- return cachedConfig;
50
- }
51
-
52
- function isTopicPage() {
53
- return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
54
- }
55
-
56
- function isCategoryTopicListPage() {
57
- return $('li[component="category/topic"]').length > 0;
58
- }
59
-
60
- function getTopicPosts() {
61
- const $primary = $('[component="post"][data-pid]');
62
- if ($primary.length) return $primary.not('.ezoic-ad-post');
63
-
64
- return $('[data-pid]').filter(function () {
65
- const $el = $(this);
66
- const hasContent = $el.find('[component="post/content"]').length > 0;
67
- const nested = $el.parents('[data-pid]').length > 0;
68
- return hasContent && !nested;
69
- }).not('.ezoic-ad-post');
70
- }
71
-
72
- function getCategoryTopicItems() {
73
- return $('li[component="category/topic"]').not('.ezoic-ad-topic');
74
- }
75
-
76
- function tagName($el) {
77
- return ($el && $el.length ? (($el.prop('tagName') || '').toUpperCase()) : '');
78
- }
79
-
80
- function makeWrapperLike($target, classes, innerHtml, attrs) {
81
- const t = tagName($target);
82
- const attrStr = attrs ? ' ' + attrs : '';
83
- if (t === 'LI') {
84
- return '<li class="' + classes + ' list-unstyled"' + attrStr + '>' + innerHtml + '</li>';
85
43
  }
86
- return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
87
- }
88
44
 
89
- function pickNextId(pool) {
90
- for (const id of pool) {
91
- if (!usedIds.has(id)) return id;
45
+ function getPageKey() {
46
+ try {
47
+ if (ajaxify && ajaxify.data) {
48
+ if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
49
+ if (ajaxify.data.cid) return 'cid:' + ajaxify.data.cid + ':' + window.location.pathname;
50
+ }
51
+ } catch (e) {}
52
+ return window.location.pathname;
53
+ }
54
+
55
+ function isTopicPage() {
56
+ try { if (ajaxify && ajaxify.data && ajaxify.data.tid) return true; } catch (e) {}
57
+ return /^\/topic\//.test(window.location.pathname);
58
+ }
59
+
60
+ function isCategoryTopicList() {
61
+ return $('li[component="category/topic"]').length > 0 && !isTopicPage();
62
+ }
63
+
64
+ function cleanupForNewPage() {
65
+ $('.ezoic-ad').remove();
66
+ usedTopic = new Set();
67
+ usedCat = new Set();
68
+ fifoTopic = [];
69
+ fifoCat = [];
70
+ }
71
+
72
+ function pickNextId(pool, usedSet) {
73
+ for (const id of pool) {
74
+ if (!usedSet.has(id)) return id;
75
+ }
76
+ return null;
92
77
  }
93
- return null;
94
- }
78
+
79
+ function destroyPlaceholder(id) {
80
+ try {
81
+ window.ezstandalone = window.ezstandalone || {};
82
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
83
+ if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
84
+ window.ezstandalone.destroyPlaceholders(id);
85
+ return;
86
+ }
87
+ window.ezstandalone.cmd.push(function () {
88
+ try { window.ezstandalone.destroyPlaceholders(id); } catch (e) {}
89
+ });
90
+ } catch (e) {}
91
+ }
92
+
93
+ function ensureUniquePlaceholder(id) {
94
+ const existing = document.getElementById('ezoic-pub-ad-placeholder-' + id);
95
+ if (!existing) return;
96
+
97
+ const wrap = existing.closest('.ezoic-ad');
98
+ if (wrap) {
99
+ try { $(wrap).remove(); } catch (e) { wrap.remove(); }
100
+ } else {
101
+ existing.remove();
102
+ }
103
+ // On détruit le slot correspondant pour éviter les comportements imprévisibles
104
+ destroyPlaceholder(id);
105
+ }
106
+
107
+ // IMPORTANT: showAds is called ONLY with newly injected ids (never the entire pool)
95
108
  function callEzoic(ids) {
96
109
  if (!ids || !ids.length) return;
97
110
 
111
+ // Anti double-call: same ids within 1.2s
98
112
  const key = ids.slice().sort((a, b) => a - b).join(',');
99
113
  const now = Date.now();
100
114
  if (window.__ezoicLastShowKey === key && now - (window.__ezoicLastShowAt || 0) < 1200) return;
@@ -108,6 +122,7 @@ function pickNextId(pool) {
108
122
  const run = function () {
109
123
  try {
110
124
  if (typeof window.ezstandalone.showAds === 'function') {
125
+ // Ezoic accepte plusieurs args: showAds(1,2,3)
111
126
  window.ezstandalone.showAds.apply(window.ezstandalone, ids);
112
127
  return true;
113
128
  }
@@ -117,179 +132,229 @@ function pickNextId(pool) {
117
132
 
118
133
  window.ezstandalone.cmd.push(function () { run(); });
119
134
 
135
+ // Retry a few times if ez loads late
120
136
  let tries = 0;
121
137
  const tick = function () {
122
138
  tries++;
123
- if (run() || tries >= 10) return;
139
+ if (run() || tries >= 8) return;
124
140
  setTimeout(tick, 800);
125
141
  };
126
142
  setTimeout(tick, 800);
127
143
  } catch (e) {}
128
144
  }
129
145
 
130
- if (typeof window.ezstandalone.showAds === 'function') {
131
- window.ezstandalone.showAds.apply(window.ezstandalone, ids);
132
- return true;
133
- }
134
- } catch (e) {}
135
- return false;
136
- };
146
+ // Auto height: wrapper visible only when placeholder gets children
147
+ function setupAutoHeightOnce() {
148
+ if (window.__ezoicAutoHeight) return;
149
+ window.__ezoicAutoHeight = true;
137
150
 
138
- // Ensure destroy->show runs AFTER ezstandalone is ready
139
- window.ezstandalone.cmd.push(function () { run(); });
140
-
141
- // retries in case ez loads late
142
- let tries = 0;
143
- const tick = function () {
144
- tries++;
145
- if (run() || tries >= 10) return;
146
- setTimeout(tick, 800);
147
- };
148
- setTimeout(tick, 800);
149
- } catch (e) {}
150
- }
151
- } catch (e) {}
152
- return false;
153
- };
154
-
155
- window.ezstandalone.cmd.push(function () { run(); });
156
-
157
- // retry a few times (Ezoic can load late)
158
- let tries = 0;
159
- const maxTries = 6;
160
- const timer = setInterval(function () {
161
- tries++;
162
- if (run() || tries >= maxTries) clearInterval(timer);
163
- }, 800);
164
- }
165
-
166
- function injectBetweenIncremental($items, pool, interval, wrapperClass) {
167
- const total = $items.length;
168
- const maxSlot = Math.floor(total / interval);
169
- if (maxSlot <= 0) return [];
170
-
171
- const newIds = [];
172
-
173
- for (let slot = 1; slot <= maxSlot; slot++) {
174
- if (injectedSlots.has(slot)) continue;
175
-
176
- const index = slot * interval - 1;
177
- const $target = $items.eq(index);
178
- if (!$target.length) continue;
179
-
180
- const id = pickNextId(pool);
181
- if (!id) {
182
- // pool exhausted: stop injecting further to avoid reusing ids and "jumping"
183
- break;
184
- }
151
+ const mark = function (wrap) {
152
+ if (!wrap) return;
153
+ const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
154
+ if (ph && ph.children && ph.children.length) {
155
+ wrap.classList.add('ezoic-filled');
156
+ }
157
+ };
185
158
 
186
- const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
187
- const html = makeWrapperLike($target, wrapperClass, placeholder, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
159
+ const scan = function () {
160
+ document.querySelectorAll('.ezoic-ad').forEach(mark);
161
+ };
188
162
 
189
- $target.after(html);
163
+ scan();
164
+ setInterval(scan, 1000);
190
165
 
191
- injectedSlots.add(slot);
192
- usedIds.add(id);
193
- newIds.push(id);
166
+ try {
167
+ const mo = new MutationObserver(scan);
168
+ mo.observe(document.body, { childList: true, subtree: true });
169
+ } catch (e) {}
194
170
  }
195
171
 
196
- return newIds;
197
- }
172
+ function insertAfter($target, id, kind, afterVal, cls) {
173
+ ensureUniquePlaceholder(id);
174
+ const wrap = $(
175
+ '<div class="ezoic-ad ' + cls + '" data-ezoic-id="' + id + '" data-ezoic-after="' + afterVal + '">' +
176
+ '<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>' +
177
+ '</div>'
178
+ );
179
+ $target.after(wrap);
180
+ return wrap;
181
+ }
198
182
 
199
- function injectMessageIncremental($posts, pool, interval) {
200
- const total = $posts.length;
201
- const maxSlot = Math.floor(total / interval);
202
- if (maxSlot <= 0) return [];
183
+ function recycleTopic($posts) {
184
+ fifoTopic.sort((a, b) => a.afterNo - b.afterNo);
185
+ while (fifoTopic.length) {
186
+ const old = fifoTopic.shift();
187
+ const sel = '.ezoic-ad-topic[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterNo + '"]';
188
+ const $el = $(sel);
189
+ if (!$el.length) continue;
190
+
191
+ // Ne pas recycler si collé juste avant le dernier post (sentinel / scroll)
192
+ try {
193
+ const $last = $posts.last();
194
+ if ($last.length && $el.prev().is($last)) {
195
+ fifoTopic.push(old);
196
+ return null;
197
+ }
198
+ } catch (e) {}
199
+
200
+ $el.remove();
201
+ usedTopic.delete(old.id);
202
+ destroyPlaceholder(old.id);
203
+ return old.id;
204
+ }
205
+ return null;
206
+ }
203
207
 
204
- const newIds = [];
208
+ function recycleCat($items) {
209
+ fifoCat.sort((a, b) => a.afterPos - b.afterPos);
210
+ while (fifoCat.length) {
211
+ const old = fifoCat.shift();
212
+ const sel = '.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterPos + '"]';
213
+ const $el = $(sel);
214
+ if (!$el.length) continue;
215
+
216
+ try {
217
+ const $last = $items.last();
218
+ if ($last.length && $el.prev().is($last)) {
219
+ fifoCat.push(old);
220
+ return null;
221
+ }
222
+ } catch (e) {}
223
+
224
+ $el.remove();
225
+ usedCat.delete(old.id);
226
+ destroyPlaceholder(old.id);
227
+ return old.id;
228
+ }
229
+ return null;
230
+ }
205
231
 
206
- for (let slot = 1; slot <= maxSlot; slot++) {
207
- if (injectedSlots.has(slot)) continue;
232
+ function injectInTopic() {
233
+ if (!(settings && (settings.enableMessageAds === true || settings.enableMessageAds === 'on'))) return;
208
234
 
209
- const index = slot * interval - 1;
210
- const $target = $posts.eq(index);
211
- if (!$target.length) continue;
235
+ const interval = parseInt(settings.messageIntervalPosts, 10) || 3;
236
+ const pool = parsePool(settings.messagePlaceholderIds);
237
+ if (!pool.length) return;
212
238
 
213
- const id = pickNextId(pool);
214
- if (!id) break;
239
+ const $posts = $('[component="post"][data-pid]');
240
+ if (!$posts.length) return;
215
241
 
216
- const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
217
- const html = makeWrapperLike($target, 'post ezoic-ad-post', inner, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
242
+ const newIds = [];
243
+ $posts.each(function (idx) {
244
+ const postNo = idx + 1;
245
+ if (postNo % interval !== 0) return;
246
+ if (idx === $posts.length - 1) return; // pas après le dernier
247
+
248
+ const $post = $(this);
249
+ const existing = $post.next('.ezoic-ad-topic');
250
+ if (existing.length) return;
251
+
252
+ let id = pickNextId(pool, usedTopic);
253
+ if (!id) {
254
+ id = recycleTopic($posts);
255
+ if (!id) return;
256
+ }
218
257
 
219
- $target.after(html);
258
+ usedTopic.add(id);
259
+ fifoTopic.push({ id, afterNo: postNo });
220
260
 
221
- injectedSlots.add(slot);
222
- usedIds.add(id);
223
- newIds.push(id);
224
- }
261
+ insertAfter($post, id, 'topic', postNo, 'ezoic-ad-topic');
262
+ newIds.push(id);
263
+ });
225
264
 
226
- return newIds;
227
- }
228
-
229
- async function refreshAds() {
230
- // reset state when navigating (ajaxify)
231
- const key = currentPageKey();
232
- if (pageKey !== key) {
233
- pageKey = key;
234
- resetPageState();
235
- // also cleanup any injected wrappers that may have been left by browser bfcache
236
- $('.ezoic-ad-post, .ezoic-ad-between, .ezoic-ad-topic').remove();
265
+ if (newIds.length) callEzoic(newIds);
237
266
  }
238
267
 
239
- if (inFlight) { rerunRequested = true; return; }
240
- inFlight = true;
268
+ function injectInCategory() {
269
+ if (!(settings && (settings.enableBetweenAds === true || settings.enableBetweenAds === 'on'))) return;
241
270
 
242
- try {
243
- const cfg = await fetchConfig();
244
- if (!cfg || cfg.excluded) return;
271
+ const interval = parseInt(settings.intervalTopics, 10) || 6;
272
+ const pool = parsePool(settings.placeholderIds);
273
+ if (!pool.length) return;
245
274
 
246
- const betweenPool = parsePool(cfg.placeholderIds);
247
- const betweenInterval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
275
+ const $items = $('li[component="category/topic"]');
276
+ if (!$items.length) return;
248
277
 
249
- const messagePool = parsePool(cfg.messagePlaceholderIds);
250
- const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
278
+ const newIds = [];
279
+ $items.each(function (idx) {
280
+ const pos = idx + 1;
281
+ if (pos % interval !== 0) return;
282
+ if (idx === $items.length - 1) return;
283
+
284
+ const $li = $(this);
285
+ const existing = $li.next('.ezoic-ad-between');
286
+ if (existing.length) return;
287
+
288
+ let id = pickNextId(pool, usedCat);
289
+ if (!id) {
290
+ id = recycleCat($items);
291
+ if (!id) return;
292
+ }
251
293
 
252
- const onTopic = isTopicPage();
253
- const onCategory = !onTopic && isCategoryTopicListPage();
294
+ usedCat.add(id);
295
+ fifoCat.push({ id, afterPos: pos });
254
296
 
255
- const $posts = onTopic ? getTopicPosts() : $();
256
- const $topicItems = onCategory ? getCategoryTopicItems() : $();
297
+ insertAfter($li, id, 'between', pos, 'ezoic-ad-between');
298
+ newIds.push(id);
299
+ });
257
300
 
258
- if (!$posts.length && !$topicItems.length) return;
301
+ if (newIds.length) callEzoic(newIds);
302
+ }
259
303
 
260
- const newIds = [];
304
+ function refresh() {
305
+ if (!settings) return;
306
+ if (userExcluded()) return;
261
307
 
262
- // Your rule:
263
- // - Category topic list: BETWEEN only
264
- // - Topic page: MESSAGE only
265
- if ($topicItems.length) {
266
- if (cfg.enableBetweenAds && betweenPool.length) {
267
- newIds.push(...injectBetweenIncremental($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
308
+ if (refreshInFlight) { refreshQueued = true; return; }
309
+ refreshInFlight = true;
310
+ try {
311
+ const key = getPageKey();
312
+ if (pageKey !== key) {
313
+ pageKey = key;
314
+ cleanupForNewPage();
268
315
  }
269
- callEzoic(newIds);
270
- return;
271
- }
272
316
 
273
- if ($posts.length) {
274
- if (cfg.enableMessageAds && messagePool.length) {
275
- newIds.push(...injectMessageIncremental($posts, messagePool, messageInterval));
276
- }
277
- callEzoic(newIds);
278
- }
279
- } finally {
280
- inFlight = false;
281
- if (rerunRequested) {
282
- rerunRequested = false;
283
- setTimeout(refreshAds, 120);
317
+ setupAutoHeightOnce();
318
+
319
+ if (isTopicPage()) injectInTopic();
320
+ else if (isCategoryTopicList()) injectInCategory();
321
+ } finally {
322
+ refreshInFlight = false;
323
+ if (refreshQueued) { refreshQueued = false; setTimeout(refresh, 50); }
284
324
  }
285
325
  }
286
- }
287
326
 
288
- function debounceRefresh() {
289
- clearTimeout(debounceTimer);
290
- debounceTimer = setTimeout(refreshAds, 180);
291
- }
327
+ function loadSettings(cb) {
328
+ // We read settings via socket so it works in ACP changes without restart
329
+ socket.emit('admin.settings.get', { hash: SETTINGS_NS }, function (err, data) {
330
+ settings = data || {};
331
+ cb && cb();
332
+ });
333
+ }
334
+
335
+ // Boot: load settings then refresh. No showAds batch here.
336
+ function boot() {
337
+ loadSettings(function () {
338
+ refresh();
339
+ // Small delayed refresh (DOM late), harmless: it won't call showAds if no new placeholders were injected
340
+ setTimeout(refresh, 1200);
341
+ });
342
+ }
343
+
344
+ // Hard load + SPA
345
+ $(document).ready(boot);
346
+ $(window).on('action:ajaxify.end', boot);
347
+
348
+ // Infinite scroll related events
349
+ $(window).on('action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', function () {
350
+ refresh();
351
+ setTimeout(refresh, 600);
352
+ });
353
+
354
+ // Navigation start: cleanup to avoid duplicates
355
+ $(window).on('action:ajaxify.start', function () {
356
+ pageKey = null;
357
+ cleanupForNewPage();
358
+ });
292
359
 
293
- $(document).ready(debounceRefresh);
294
- $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded', debounceRefresh);
295
- setTimeout(debounceRefresh, 1800);
360
+ })();
@@ -0,0 +1,5 @@
1
+ /* Le conteneur est caché tant qu'Ezoic n'a pas injecté de contenu */
2
+ .ezoic-ad{min-height:0 !important;height:auto !important;padding:0 !important;margin:0.5rem 0;}
3
+ .ezoic-ad:not(.ezoic-filled){display:none !important;}
4
+ .ezoic-ad .ezoic-ad-inner{padding:0;margin:0;}
5
+ .ezoic-ad .ezoic-ad-inner > div{margin:0;padding:0;}
@@ -1,59 +1,59 @@
1
1
  <div class="acp-page-container">
2
- <h2>Ezoic - Publicités Infinite Scroll Ads</h2>
2
+ <h1 class="mb-3">Ezoic - Publicités Infinite Scroll</h1>
3
3
 
4
- <form class="ezoic-infinite-settings" role="form">
5
- <h4 class="mt-3">Pubs entre les posts (bloc simple)</h4>
6
-
7
- <div class="form-check mb-3">
8
- <input class="form-check-input" type="checkbox" id="enableBetweenAds" name="enableBetweenAds" {enableBetweenAds_checked}>
9
- <label class="form-check-label" for="enableBetweenAds">Activer les pubs entre les posts</label>
10
- </div>
4
+ <div class="alert alert-info">
5
+ Format placeholder&nbsp;: <code>&lt;div id="ezoic-pub-ad-placeholder-XXX"&gt;&lt;/div&gt;</code>
6
+ </div>
11
7
 
8
+ <form role="form" class="ezoic-infinite-settings">
12
9
  <div class="mb-3">
13
- <label class="form-label" for="placeholderIds">Pool d’IDs Ezoic (entre posts)</label>
14
- <textarea id="placeholderIds" name="placeholderIds" class="form-control" rows="4">{placeholderIds}</textarea>
15
- <p class="form-text">Un ID par ligne (ou séparé par virgules/espaces). Le nombre d’IDs = nombre max de pubs simultanées.</p>
16
- </div>
17
-
18
- <div class="mb-3">
19
- <label class="form-label" for="intervalPosts">Afficher une pub tous les N posts</label>
20
- <input type="number" id="intervalPosts" name="intervalPosts" class="form-control" value="{intervalPosts}" min="1">
10
+ <label class="form-label">Groupes exclus (pas de pubs pour ces groupes)</label>
11
+ <select multiple class="form-select" name="excludedGroups">
12
+ <!-- BEGIN groups -->
13
+ <option value="{groups.name}">{groups.name}</option>
14
+ <!-- END groups -->
15
+ </select>
16
+ <div class="form-text">Maintenez Ctrl/Cmd pour sélectionner plusieurs groupes. Liste triée par ordre alphabétique.</div>
21
17
  </div>
22
18
 
23
19
  <hr/>
24
20
 
25
- <h4 class="mt-3">Pubs “message” entre les réponses</h4>
26
- <p class="form-text">Insère un bloc qui ressemble à un post, toutes les N réponses (dans une page topic).</p>
21
+ <h3>Entre les topics dans une catégorie (liste des sujets)</h3>
27
22
 
28
- <div class="form-check mb-3">
29
- <input class="form-check-input" type="checkbox" id="enableMessageAds" name="enableMessageAds" {enableMessageAds_checked}>
30
- <label class="form-check-label" for="enableMessageAds">Activer les pubs “message”</label>
23
+ <div class="form-check form-switch mb-2">
24
+ <input class="form-check-input" type="checkbox" name="enableBetweenAds">
25
+ <label class="form-check-label">Activer les pubs entre les topics</label>
31
26
  </div>
32
27
 
33
28
  <div class="mb-3">
34
- <label class="form-label" for="messagePlaceholderIds">Pool d’IDs Ezoic (message)</label>
35
- <textarea id="messagePlaceholderIds" name="messagePlaceholderIds" class="form-control" rows="4">{messagePlaceholderIds}</textarea>
36
- <p class="form-text">Pool séparé recommandé pour éviter la réutilisation d’IDs. IMPORTANT : ne réutilise pas les mêmes IDs dans les deux pools.</p>
29
+ <label class="form-label">Intervalle (insérer après chaque N topics)</label>
30
+ <input type="number" class="form-control" name="intervalTopics" min="1" step="1">
37
31
  </div>
38
32
 
39
33
  <div class="mb-3">
40
- <label class="form-label" for="messageIntervalPosts">Afficher un “message pub” tous les N messages</label>
41
- <input type="number" id="messageIntervalPosts" name="messageIntervalPosts" class="form-control" value="{messageIntervalPosts}" min="1">
34
+ <label class="form-label">Pool d'IDs de placeholder (un par ligne)</label>
35
+ <textarea class="form-control" name="placeholderIds" rows="6"></textarea>
42
36
  </div>
43
37
 
44
38
  <hr/>
45
39
 
46
- <h4 class="mt-3">Exclusions</h4>
40
+ <h3>Dans les topics (entre les messages)</h3>
41
+
42
+ <div class="form-check form-switch mb-2">
43
+ <input class="form-check-input" type="checkbox" name="enableMessageAds">
44
+ <label class="form-check-label">Activer les pubs entre les messages</label>
45
+ </div>
46
+
47
47
  <div class="mb-3">
48
- <label class="form-label" for="excludedGroups">Groupes exclus</label>
49
- <select id="excludedGroups" name="excludedGroups" class="form-select" multiple>
50
- <!-- BEGIN allGroups -->
51
- <option value="{allGroups.name}">{allGroups.name}</option>
52
- <!-- END allGroups -->
53
- </select>
54
- <p class="form-text">Si l’utilisateur appartient à un de ces groupes, aucune pub n’est injectée.</p>
48
+ <label class="form-label">Intervalle (insérer après chaque N messages)</label>
49
+ <input type="number" class="form-control" name="messageIntervalPosts" min="1" step="1">
50
+ </div>
51
+
52
+ <div class="mb-3">
53
+ <label class="form-label">Pool d'IDs de placeholder pour messages (un par ligne)</label>
54
+ <textarea class="form-control" name="messagePlaceholderIds" rows="6"></textarea>
55
55
  </div>
56
56
 
57
- <button id="save" class="btn btn-primary">Enregistrer</button>
57
+ <button type="button" class="btn btn-primary ezoic-infinite-save">Enregistrer</button>
58
58
  </form>
59
59
  </div>