nodebb-plugin-ezoic-infinite 0.9.8 → 0.9.9

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
@@ -1,57 +1,96 @@
1
1
  'use strict';
2
2
 
3
3
  const meta = require.main.require('./src/meta');
4
- const groups = require.main.require('./src/groups');
4
+ const db = require.main.require('./src/database');
5
5
 
6
- const Plugin = {};
6
+ const SETTINGS_KEY = 'ezoic-infinite';
7
+ const plugin = {};
7
8
 
8
- Plugin.init = async function (params) {
9
- const { router, middleware } = params;
9
+ function normalizeExcludedGroups(value) {
10
+ if (!value) return [];
11
+ if (Array.isArray(value)) return value;
12
+ return String(value).split(',').map(s => s.trim()).filter(Boolean);
13
+ }
10
14
 
11
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
12
- router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
13
- };
15
+ function parseBool(v, def = false) {
16
+ if (v === undefined || v === null || v === '') return def;
17
+ if (typeof v === 'boolean') return v;
18
+ const s = String(v).toLowerCase();
19
+ return s === '1' || s === 'true' || s === 'on' || s === 'yes';
20
+ }
14
21
 
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
- }
22
+ async function getAllGroups() {
23
+ const names = await db.getSortedSetRange('groups:createtime', 0, -1);
24
+ const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
25
+ const data = await groups.getGroupsData(filtered);
26
+ // Sort alphabetically for ACP usability
27
+ data.sort((a, b) => String(a.name).localeCompare(String(b.name), 'fr', { sensitivity: 'base' }));
28
+ return data;
29
+ }
30
+ async function getSettings() {
31
+ const s = await meta.settings.get(SETTINGS_KEY);
32
+ return {
33
+ // Between-post ads (simple blocks)
34
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
35
+ placeholderIds: (s.placeholderIds || '').trim(),
36
+ intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
28
37
 
29
- let groupList = [];
30
- try {
31
- groupList = await groups.getGroupsData(groupNames);
32
- } catch (e) {
33
- groupList = groupNames.map((name) => ({ name }));
34
- }
38
+ // "Ad message" between replies (looks like a post)
39
+ enableMessageAds: parseBool(s.enableMessageAds, false),
40
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
41
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
35
42
 
36
- groupList = (groupList || [])
37
- .filter(g => g && g.name)
38
- .sort((a, b) => (a.name || '').localeCompare(b.name || '', 'fr', { sensitivity: 'base' }));
43
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
44
+ };
45
+ }
39
46
 
40
- res.render('admin/plugins/ezoic-infinite', {
41
- title: 'Ezoic - Publicités Infinite Scroll',
42
- settings,
43
- groups: groupList,
44
- });
47
+ async function isUserExcluded(uid, excludedGroups) {
48
+ if (!uid || !excludedGroups.length) return false;
49
+ const userGroups = await groups.getUserGroups([uid]);
50
+ return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
45
51
  }
46
52
 
47
- Plugin.addAdminNavigation = async function (header) {
53
+ plugin.addAdminNavigation = async (header) => {
48
54
  header.plugins = header.plugins || [];
49
55
  header.plugins.push({
50
56
  route: '/plugins/ezoic-infinite',
51
- icon: 'fa-bullhorn',
52
- name: 'Ezoic Infinite',
57
+ icon: 'fa-ad',
58
+ name: 'Ezoic Infinite Ads'
53
59
  });
54
60
  return header;
55
61
  };
56
62
 
57
- module.exports = Plugin;
63
+ plugin.init = async ({ router, middleware }) => {
64
+ async function render(req, res) {
65
+ const settings = await getSettings();
66
+ const allGroups = await getAllGroups();
67
+
68
+ res.render('admin/plugins/ezoic-infinite', {
69
+ title: 'Ezoic Infinite Ads',
70
+ ...settings,
71
+ enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
72
+ enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
73
+ allGroups,
74
+ });
75
+ }
76
+
77
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
78
+ router.get('/api/admin/plugins/ezoic-infinite', render);
79
+
80
+ router.get('/api/plugins/ezoic-infinite/config', middleware.buildHeader, async (req, res) => {
81
+ const settings = await getSettings();
82
+ const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
83
+
84
+ res.json({
85
+ excluded,
86
+ enableBetweenAds: settings.enableBetweenAds,
87
+ placeholderIds: settings.placeholderIds,
88
+ intervalPosts: settings.intervalPosts,
89
+ enableMessageAds: settings.enableMessageAds,
90
+ messagePlaceholderIds: settings.messagePlaceholderIds,
91
+ messageIntervalPosts: settings.messageIntervalPosts,
92
+ });
93
+ });
94
+ };
95
+
96
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
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).",
3
+ "version": "0.9.9",
4
+ "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -11,6 +11,9 @@
11
11
  "ads",
12
12
  "infinite-scroll"
13
13
  ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
14
17
  "nbbpm": {
15
18
  "compatibility": "^4.0.0"
16
19
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "nodebb-plugin-ezoic-infinite",
3
- "name": "Ezoic Infinite",
4
- "description": "Ezoic ads injection with infinite scroll (topics list + topic posts)",
3
+ "name": "NodeBB Ezoic Infinite Ads",
4
+ "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "library": "./library.js",
6
6
  "hooks": [
7
7
  {
@@ -22,8 +22,5 @@
22
22
  "scripts": [
23
23
  "public/client.js"
24
24
  ],
25
- "css": [
26
- "public/style.css"
27
- ],
28
25
  "templates": "public/templates"
29
26
  }
package/public/admin.js CHANGED
@@ -19,9 +19,25 @@ $(document).ready(function () {
19
19
  form.find('[name="messageIntervalPosts"]').val(parseInt(data.messageIntervalPosts, 10) || 3);
20
20
  form.find('[name="messagePlaceholderIds"]').val(data.messagePlaceholderIds || '');
21
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()));
22
+ // NodeBB 4.x: charger les groupes via socket côté ACP
23
+ socket.emit('groups.getGroups', {}, function (err2, groups) {
24
+ const $select = form.find('[name="excludedGroups"]');
25
+ $select.empty();
26
+
27
+ if (!err2 && Array.isArray(groups)) {
28
+ groups
29
+ .map(g => g && g.name)
30
+ .filter(Boolean)
31
+ .sort((a, b) => a.localeCompare(b, 'fr', { sensitivity: 'base' }))
32
+ .forEach((name) => {
33
+ $select.append($('<option>').val(name).text(name));
34
+ });
35
+ }
36
+
37
+ const selected = (data.excludedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
38
+ $select.find('option').each(function () {
39
+ $(this).prop('selected', selected.includes($(this).val()));
40
+ });
25
41
  });
26
42
  });
27
43
  }
@@ -49,6 +65,11 @@ $(document).ready(function () {
49
65
  });
50
66
  }
51
67
 
52
- $('.ezoic-infinite-save').on('click', save);
68
+ $(document).off('click.ezoicInfiniteSave', '.ezoic-infinite-save')
69
+ .on('click.ezoicInfiniteSave', '.ezoic-infinite-save', function (e) {
70
+ e.preventDefault();
71
+ save();
72
+ });
73
+
53
74
  load();
54
75
  });
package/public/client.js CHANGED
@@ -1,360 +1,252 @@
1
- /* global $, ajaxify, app, socket */
2
1
  'use strict';
3
2
 
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;
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);
42
30
  }
43
- }
44
-
45
- function getPageKey() {
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
+ }
86
+ return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
87
+ }
88
+
89
+ function pickNextId(pool) {
90
+ for (const id of pool) {
91
+ if (!usedIds.has(id)) return id;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ function callEzoic(ids) {
97
+ if (!ids || !ids.length) return;
98
+
99
+ window.ezstandalone = window.ezstandalone || {};
100
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
101
+
102
+ const run = function () {
46
103
  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;
104
+ if (typeof window.ezstandalone.showAds === 'function') {
105
+ window.ezstandalone.showAds.apply(window.ezstandalone, ids);
106
+ return true;
50
107
  }
51
108
  } 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;
109
+ return false;
110
+ };
111
+
112
+ window.ezstandalone.cmd.push(function () { run(); });
113
+
114
+ // retry a few times (Ezoic can load late)
115
+ let tries = 0;
116
+ const maxTries = 6;
117
+ const timer = setInterval(function () {
118
+ tries++;
119
+ if (run() || tries >= maxTries) clearInterval(timer);
120
+ }, 800);
121
+ }
122
+
123
+ function injectBetweenIncremental($items, pool, interval, wrapperClass) {
124
+ const total = $items.length;
125
+ const maxSlot = Math.floor(total / interval);
126
+ if (maxSlot <= 0) return [];
127
+
128
+ const newIds = [];
129
+
130
+ for (let slot = 1; slot <= maxSlot; slot++) {
131
+ if (injectedSlots.has(slot)) continue;
132
+
133
+ const index = slot * interval - 1;
134
+ const $target = $items.eq(index);
135
+ if (!$target.length) continue;
136
+
137
+ const id = pickNextId(pool);
138
+ if (!id) {
139
+ // pool exhausted: stop injecting further to avoid reusing ids and "jumping"
140
+ break;
75
141
  }
76
- return null;
77
- }
78
142
 
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
- }
143
+ const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
144
+ const html = makeWrapperLike($target, wrapperClass, placeholder, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
92
145
 
93
- function ensureUniquePlaceholder(id) {
94
- const existing = document.getElementById('ezoic-pub-ad-placeholder-' + id);
95
- if (!existing) return;
146
+ $target.after(html);
96
147
 
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);
148
+ injectedSlots.add(slot);
149
+ usedIds.add(id);
150
+ newIds.push(id);
105
151
  }
106
152
 
107
- // IMPORTANT: showAds is called ONLY with newly injected ids (never the entire pool)
108
- function callEzoic(ids) {
109
- if (!ids || !ids.length) return;
153
+ return newIds;
154
+ }
110
155
 
111
- // Anti double-call: same ids within 1.2s
112
- const key = ids.slice().sort((a, b) => a - b).join(',');
113
- const now = Date.now();
114
- if (window.__ezoicLastShowKey === key && now - (window.__ezoicLastShowAt || 0) < 1200) return;
115
- window.__ezoicLastShowKey = key;
116
- window.__ezoicLastShowAt = now;
156
+ function injectMessageIncremental($posts, pool, interval) {
157
+ const total = $posts.length;
158
+ const maxSlot = Math.floor(total / interval);
159
+ if (maxSlot <= 0) return [];
117
160
 
118
- try {
119
- window.ezstandalone = window.ezstandalone || {};
120
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
121
-
122
- const run = function () {
123
- try {
124
- if (typeof window.ezstandalone.showAds === 'function') {
125
- // Ezoic accepte plusieurs args: showAds(1,2,3)
126
- window.ezstandalone.showAds.apply(window.ezstandalone, ids);
127
- return true;
128
- }
129
- } catch (e) {}
130
- return false;
131
- };
132
-
133
- window.ezstandalone.cmd.push(function () { run(); });
134
-
135
- // Retry a few times if ez loads late
136
- let tries = 0;
137
- const tick = function () {
138
- tries++;
139
- if (run() || tries >= 8) return;
140
- setTimeout(tick, 800);
141
- };
142
- setTimeout(tick, 800);
143
- } catch (e) {}
144
- }
161
+ const newIds = [];
145
162
 
146
- // Auto height: wrapper visible only when placeholder gets children
147
- function setupAutoHeightOnce() {
148
- if (window.__ezoicAutoHeight) return;
149
- window.__ezoicAutoHeight = true;
163
+ for (let slot = 1; slot <= maxSlot; slot++) {
164
+ if (injectedSlots.has(slot)) continue;
150
165
 
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
- };
166
+ const index = slot * interval - 1;
167
+ const $target = $posts.eq(index);
168
+ if (!$target.length) continue;
158
169
 
159
- const scan = function () {
160
- document.querySelectorAll('.ezoic-ad').forEach(mark);
161
- };
170
+ const id = pickNextId(pool);
171
+ if (!id) break;
162
172
 
163
- scan();
164
- setInterval(scan, 1000);
173
+ const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
174
+ const html = makeWrapperLike($target, 'post ezoic-ad-post', inner, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
165
175
 
166
- try {
167
- const mo = new MutationObserver(scan);
168
- mo.observe(document.body, { childList: true, subtree: true });
169
- } catch (e) {}
170
- }
176
+ $target.after(html);
171
177
 
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;
178
+ injectedSlots.add(slot);
179
+ usedIds.add(id);
180
+ newIds.push(id);
181
181
  }
182
182
 
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
- }
183
+ return newIds;
184
+ }
207
185
 
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;
186
+ async function refreshAds() {
187
+ // reset state when navigating (ajaxify)
188
+ const key = currentPageKey();
189
+ if (pageKey !== key) {
190
+ pageKey = key;
191
+ resetPageState();
192
+ // also cleanup any injected wrappers that may have been left by browser bfcache
193
+ $('.ezoic-ad-post, .ezoic-ad-between, .ezoic-ad-topic').remove();
230
194
  }
231
195
 
232
- function injectInTopic() {
233
- if (!(settings && (settings.enableMessageAds === true || settings.enableMessageAds === 'on'))) return;
234
-
235
- const interval = parseInt(settings.messageIntervalPosts, 10) || 3;
236
- const pool = parsePool(settings.messagePlaceholderIds);
237
- if (!pool.length) return;
238
-
239
- const $posts = $('[component="post"][data-pid]');
240
- if (!$posts.length) return;
196
+ if (inFlight) { rerunRequested = true; return; }
197
+ inFlight = true;
241
198
 
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
- }
199
+ try {
200
+ const cfg = await fetchConfig();
201
+ if (!cfg || cfg.excluded) return;
257
202
 
258
- usedTopic.add(id);
259
- fifoTopic.push({ id, afterNo: postNo });
203
+ const betweenPool = parsePool(cfg.placeholderIds);
204
+ const betweenInterval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
260
205
 
261
- insertAfter($post, id, 'topic', postNo, 'ezoic-ad-topic');
262
- newIds.push(id);
263
- });
264
-
265
- if (newIds.length) callEzoic(newIds);
266
- }
206
+ const messagePool = parsePool(cfg.messagePlaceholderIds);
207
+ const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
267
208
 
268
- function injectInCategory() {
269
- if (!(settings && (settings.enableBetweenAds === true || settings.enableBetweenAds === 'on'))) return;
209
+ const onTopic = isTopicPage();
210
+ const onCategory = !onTopic && isCategoryTopicListPage();
270
211
 
271
- const interval = parseInt(settings.intervalTopics, 10) || 6;
272
- const pool = parsePool(settings.placeholderIds);
273
- if (!pool.length) return;
212
+ const $posts = onTopic ? getTopicPosts() : $();
213
+ const $topicItems = onCategory ? getCategoryTopicItems() : $();
274
214
 
275
- const $items = $('li[component="category/topic"]');
276
- if (!$items.length) return;
215
+ if (!$posts.length && !$topicItems.length) return;
277
216
 
278
217
  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
- }
293
-
294
- usedCat.add(id);
295
- fifoCat.push({ id, afterPos: pos });
296
-
297
- insertAfter($li, id, 'between', pos, 'ezoic-ad-between');
298
- newIds.push(id);
299
- });
300
218
 
301
- if (newIds.length) callEzoic(newIds);
302
- }
303
-
304
- function refresh() {
305
- if (!settings) return;
306
- if (userExcluded()) return;
307
-
308
- if (refreshInFlight) { refreshQueued = true; return; }
309
- refreshInFlight = true;
310
- try {
311
- const key = getPageKey();
312
- if (pageKey !== key) {
313
- pageKey = key;
314
- cleanupForNewPage();
219
+ // Your rule:
220
+ // - Category topic list: BETWEEN only
221
+ // - Topic page: MESSAGE only
222
+ if ($topicItems.length) {
223
+ if (cfg.enableBetweenAds && betweenPool.length) {
224
+ newIds.push(...injectBetweenIncremental($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
315
225
  }
316
-
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); }
226
+ callEzoic(newIds);
227
+ return;
324
228
  }
325
- }
326
229
 
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
- });
230
+ if ($posts.length) {
231
+ if (cfg.enableMessageAds && messagePool.length) {
232
+ newIds.push(...injectMessageIncremental($posts, messagePool, messageInterval));
233
+ }
234
+ callEzoic(newIds);
235
+ }
236
+ } finally {
237
+ inFlight = false;
238
+ if (rerunRequested) {
239
+ rerunRequested = false;
240
+ setTimeout(refreshAds, 120);
241
+ }
342
242
  }
243
+ }
343
244
 
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
- });
245
+ function debounceRefresh() {
246
+ clearTimeout(debounceTimer);
247
+ debounceTimer = setTimeout(refreshAds, 180);
248
+ }
359
249
 
360
- })();
250
+ $(document).ready(debounceRefresh);
251
+ $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded', debounceRefresh);
252
+ setTimeout(debounceRefresh, 1800);
@@ -1,59 +1,59 @@
1
1
  <div class="acp-page-container">
2
- <h1 class="mb-3">Ezoic - Publicités Infinite Scroll</h1>
2
+ <h2>Ezoic - Publicités Infinite Scroll Ads</h2>
3
3
 
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>
4
+ <form class="ezoic-infinite-settings" role="form">
5
+ <h4 class="mt-3">Pubs entre les posts (bloc simple)</h4>
7
6
 
8
- <form role="form" class="ezoic-infinite-settings">
9
- <div class="mb-3">
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>
17
- </div>
18
-
19
- <hr/>
20
-
21
- <h3>Entre les topics dans une catégorie (liste des sujets)</h3>
22
-
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>
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>
26
10
  </div>
27
11
 
28
12
  <div class="mb-3">
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">
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>
31
16
  </div>
32
17
 
33
18
  <div class="mb-3">
34
- <label class="form-label">Pool d'IDs de placeholder (un par ligne)</label>
35
- <textarea class="form-control" name="placeholderIds" rows="6"></textarea>
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">
36
21
  </div>
37
22
 
38
23
  <hr/>
39
24
 
40
- <h3>Dans les topics (entre les messages)</h3>
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>
41
27
 
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>
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>
45
31
  </div>
46
32
 
47
33
  <div class="mb-3">
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">
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>
50
37
  </div>
51
38
 
52
39
  <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>
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">
42
+ </div>
43
+
44
+ <hr/>
45
+
46
+ <h4 class="mt-3">Exclusions</h4>
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>
55
55
  </div>
56
56
 
57
- <button type="button" class="btn btn-primary ezoic-infinite-save">Enregistrer</button>
57
+ <button id="save" class="btn btn-primary">Enregistrer</button>
58
58
  </form>
59
59
  </div>
package/public/style.css DELETED
@@ -1,5 +0,0 @@
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;}