nodebb-plugin-ezoic-infinite 0.9.10 → 0.9.11

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,96 +1,33 @@
1
1
  'use strict';
2
2
 
3
3
  const meta = require.main.require('./src/meta');
4
- const db = require.main.require('./src/database');
5
4
 
6
- const SETTINGS_KEY = 'ezoic-infinite';
7
- const plugin = {};
5
+ const Plugin = {};
8
6
 
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
- }
14
-
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
- }
21
-
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),
7
+ Plugin.init = async function (params) {
8
+ const { router, middleware } = params;
37
9
 
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),
10
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
11
+ router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
12
+ };
42
13
 
43
- excludedGroups: normalizeExcludedGroups(s.excludedGroups),
44
- };
45
- }
14
+ async function renderAdmin(req, res) {
15
+ const settings = await meta.settings.get('ezoic-infinite');
46
16
 
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));
17
+ res.render('admin/plugins/ezoic-infinite', {
18
+ title: 'Ezoic - Publicités Infinite Scroll',
19
+ settings,
20
+ });
51
21
  }
52
22
 
53
- plugin.addAdminNavigation = async (header) => {
23
+ Plugin.addAdminNavigation = async function (header) {
54
24
  header.plugins = header.plugins || [];
55
25
  header.plugins.push({
56
26
  route: '/plugins/ezoic-infinite',
57
- icon: 'fa-ad',
58
- name: 'Ezoic Infinite Ads'
27
+ icon: 'fa-bullhorn',
28
+ name: 'Ezoic Infinite',
59
29
  });
60
30
  return header;
61
31
  };
62
32
 
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;
33
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.9.10",
4
- "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
3
+ "version": "0.9.11",
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
@@ -19,7 +19,7 @@ $(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
- // NodeBB 4.x: charger les groupes via socket côté ACP
22
+ // Load groups dynamically (NodeBB 4.x)
23
23
  socket.emit('groups.getGroups', {}, function (err2, groups) {
24
24
  const $select = form.find('[name="excludedGroups"]');
25
25
  $select.empty();
@@ -65,11 +65,6 @@ $(document).ready(function () {
65
65
  });
66
66
  }
67
67
 
68
- $(document).off('click.ezoicInfiniteSave', '.ezoic-infinite-save')
69
- .on('click.ezoicInfiniteSave', '.ezoic-infinite-save', function (e) {
70
- e.preventDefault();
71
- save();
72
- });
73
-
68
+ $('.ezoic-infinite-save').on('click', save);
74
69
  load();
75
70
  });
package/public/client.js CHANGED
@@ -1,248 +1,344 @@
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
- }
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 () {
43
+ }
44
+
45
+ function getPageKey() {
103
46
  try {
104
- if (typeof window.ezstandalone.showAds === 'function') {
105
- window.ezstandalone.showAds.apply(window.ezstandalone, ids);
106
- return true;
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;
107
50
  }
108
51
  } catch (e) {}
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
- for (let slot = 1; slot <= maxSlot; slot++) {
129
- if (injectedSlots.has(slot)) continue;
130
-
131
- const index = slot * interval - 1;
132
- const $target = $items.eq(index);
133
- if (!$target.length) continue;
134
-
135
- const id = pickNextId(pool);
136
- if (!id) {
137
- // pool exhausted: stop injecting further to avoid reusing ids and "jumping"
138
- break;
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;
139
75
  }
76
+ return null;
77
+ }
140
78
 
141
- const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
142
- const html = makeWrapperLike($target, wrapperClass, placeholder, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
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
92
 
144
- $target.after(html);
93
+ function ensureUniquePlaceholder(id) {
94
+ const existing = document.getElementById('ezoic-pub-ad-placeholder-' + id);
95
+ if (!existing) return;
145
96
 
146
- injectedSlots.add(slot);
147
- usedIds.add(id);
148
- newIds.push(id);
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
+ destroyPlaceholder(id);
149
104
  }
150
105
 
151
- return newIds;
152
- }
106
+ // IMPORTANT: showAds is called ONLY for a single newly injected placeholder id (never a batch)
107
+ function callEzoicSingle(id) {
108
+ if (!id) return;
153
109
 
154
- function injectMessageIncremental($posts, pool, interval) {
155
- const total = $posts.length;
156
- const maxSlot = Math.floor(total / interval);
157
- if (maxSlot <= 0) return [];
110
+ const now = Date.now();
111
+ window.__ezoicLastSingle = window.__ezoicLastSingle || {};
112
+ const last = window.__ezoicLastSingle[id] || 0;
113
+ if (now - last < 800) return;
114
+ window.__ezoicLastSingle[id] = now;
158
115
 
159
- for (let slot = 1; slot <= maxSlot; slot++) {
160
- if (injectedSlots.has(slot)) continue;
116
+ try {
117
+ window.ezstandalone = window.ezstandalone || {};
118
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
119
+
120
+ const run = function () {
121
+ try {
122
+ if (typeof window.ezstandalone.showAds === 'function') {
123
+ window.ezstandalone.showAds(id);
124
+ return true;
125
+ }
126
+ } catch (e) {}
127
+ return false;
128
+ };
129
+
130
+ window.ezstandalone.cmd.push(function () { run(); });
131
+
132
+ // Retry a few times if ez loads late
133
+ let tries = 0;
134
+ const tick = function () {
135
+ tries++;
136
+ if (run() || tries >= 8) return;
137
+ setTimeout(tick, 800);
138
+ };
139
+ setTimeout(tick, 800);
140
+ } catch (e) {}
141
+ }
161
142
 
162
- const index = slot * interval - 1;
163
- const $target = $posts.eq(index);
164
- if (!$target.length) continue;
143
+ // Auto height: wrapper visible only when placeholder gets children
144
+ function setupAutoHeightOnce() {
145
+ if (window.__ezoicAutoHeight) return;
146
+ window.__ezoicAutoHeight = true;
165
147
 
166
- const id = pickNextId(pool);
167
- if (!id) break;
148
+ const mark = function (wrap) {
149
+ if (!wrap) return;
150
+ const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
151
+ if (ph && ph.children && ph.children.length) {
152
+ wrap.classList.add('ezoic-filled');
153
+ }
154
+ };
168
155
 
169
- const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
170
- const html = makeWrapperLike($target, 'post ezoic-ad-post', inner, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
156
+ const scan = function () {
157
+ document.querySelectorAll('.ezoic-ad').forEach(mark);
158
+ };
171
159
 
172
- $target.after(html);
160
+ scan();
161
+ setInterval(scan, 1000);
173
162
 
174
- injectedSlots.add(slot);
175
- usedIds.add(id);
176
- newIds.push(id);
163
+ try {
164
+ const mo = new MutationObserver(scan);
165
+ mo.observe(document.body, { childList: true, subtree: true });
166
+ } catch (e) {}
177
167
  }
178
168
 
179
- return newIds;
180
- }
169
+ function insertAfter($target, id, cls, afterVal) {
170
+ ensureUniquePlaceholder(id);
171
+ const wrap = $(
172
+ '<div class="ezoic-ad ' + cls + '" data-ezoic-id="' + id + '" data-ezoic-after="' + afterVal + '">' +
173
+ '<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>' +
174
+ '</div>'
175
+ );
176
+ $target.after(wrap);
177
+ return wrap;
178
+ }
181
179
 
182
- async function refreshAds() {
183
- // reset state when navigating (ajaxify)
184
- const key = currentPageKey();
185
- if (pageKey !== key) {
186
- pageKey = key;
187
- resetPageState();
188
- // also cleanup any injected wrappers that may have been left by browser bfcache
189
- $('.ezoic-ad-post, .ezoic-ad-between, .ezoic-ad-topic').remove();
180
+ function recycleTopic($posts) {
181
+ fifoTopic.sort((a, b) => a.afterNo - b.afterNo);
182
+ while (fifoTopic.length) {
183
+ const old = fifoTopic.shift();
184
+ const sel = '.ezoic-ad-topic[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterNo + '"]';
185
+ const $el = $(sel);
186
+ if (!$el.length) continue;
187
+
188
+ try {
189
+ const $last = $posts.last();
190
+ if ($last.length && $el.prev().is($last)) {
191
+ fifoTopic.push(old);
192
+ return null;
193
+ }
194
+ } catch (e) {}
195
+
196
+ $el.remove();
197
+ usedTopic.delete(old.id);
198
+ destroyPlaceholder(old.id);
199
+ return old.id;
200
+ }
201
+ return null;
190
202
  }
191
203
 
192
- if (inFlight) { rerunRequested = true; return; }
193
- inFlight = true;
204
+ function recycleCat($items) {
205
+ fifoCat.sort((a, b) => a.afterPos - b.afterPos);
206
+ while (fifoCat.length) {
207
+ const old = fifoCat.shift();
208
+ const sel = '.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterPos + '"]';
209
+ const $el = $(sel);
210
+ if (!$el.length) continue;
211
+
212
+ try {
213
+ const $last = $items.last();
214
+ if ($last.length && $el.prev().is($last)) {
215
+ fifoCat.push(old);
216
+ return null;
217
+ }
218
+ } catch (e) {}
219
+
220
+ $el.remove();
221
+ usedCat.delete(old.id);
222
+ destroyPlaceholder(old.id);
223
+ return old.id;
224
+ }
225
+ return null;
226
+ }
227
+
228
+ function injectInTopic() {
229
+ if (!(settings && (settings.enableMessageAds === true || settings.enableMessageAds === 'on'))) return;
230
+
231
+ const interval = parseInt(settings.messageIntervalPosts, 10) || 3;
232
+ const pool = parsePool(settings.messagePlaceholderIds);
233
+ if (!pool.length) return;
194
234
 
195
- try {
196
- const cfg = await fetchConfig();
197
- if (!cfg || cfg.excluded) return;
235
+ const $posts = $('[component="post"][data-pid]');
236
+ if (!$posts.length) return;
198
237
 
199
- const betweenPool = parsePool(cfg.placeholderIds);
200
- const betweenInterval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
238
+ $posts.each(function (idx) {
239
+ const postNo = idx + 1;
240
+ if (postNo % interval !== 0) return;
241
+ if (idx === $posts.length - 1) return;
201
242
 
202
- const messagePool = parsePool(cfg.messagePlaceholderIds);
203
- const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
243
+ const $post = $(this);
244
+ const existing = $post.next('.ezoic-ad-topic');
245
+ if (existing.length) return;
204
246
 
205
- const onTopic = isTopicPage();
206
- const onCategory = !onTopic && isCategoryTopicListPage();
247
+ let id = pickNextId(pool, usedTopic);
248
+ if (!id) {
249
+ id = recycleTopic($posts);
250
+ if (!id) return;
251
+ }
252
+
253
+ usedTopic.add(id);
254
+ fifoTopic.push({ id, afterNo: postNo });
255
+
256
+ insertAfter($post, id, 'ezoic-ad-topic', postNo);
257
+ callEzoicSingle(id);
258
+ });
259
+ }
207
260
 
208
- const $posts = onTopic ? getTopicPosts() : $();
209
- const $topicItems = onCategory ? getCategoryTopicItems() : $();
261
+ function injectInCategory() {
262
+ if (!(settings && (settings.enableBetweenAds === true || settings.enableBetweenAds === 'on'))) return;
210
263
 
211
- if (!$posts.length && !$topicItems.length) return;
264
+ const interval = parseInt(settings.intervalTopics, 10) || 6;
265
+ const pool = parsePool(settings.placeholderIds);
266
+ if (!pool.length) return;
212
267
 
213
- const newIds = [];
268
+ const $items = $('li[component="category/topic"]');
269
+ if (!$items.length) return;
214
270
 
215
- // Your rule:
216
- // - Category topic list: BETWEEN only
217
- // - Topic page: MESSAGE only
218
- if ($topicItems.length) {
219
- if (cfg.enableBetweenAds && betweenPool.length) {
220
- newIds.push(...injectBetweenIncremental($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
271
+ $items.each(function (idx) {
272
+ const pos = idx + 1;
273
+ if (pos % interval !== 0) return;
274
+ if (idx === $items.length - 1) return;
275
+
276
+ const $li = $(this);
277
+ const existing = $li.next('.ezoic-ad-between');
278
+ if (existing.length) return;
279
+
280
+ let id = pickNextId(pool, usedCat);
281
+ if (!id) {
282
+ id = recycleCat($items);
283
+ if (!id) return;
221
284
  }
222
- callEzoic(newIds);
223
- return;
224
- }
225
285
 
226
- if ($posts.length) {
227
- if (cfg.enableMessageAds && messagePool.length) {
228
- newIds.push(...injectMessageIncremental($posts, messagePool, messageInterval));
286
+ usedCat.add(id);
287
+ fifoCat.push({ id, afterPos: pos });
288
+
289
+ insertAfter($li, id, 'ezoic-ad-between', pos);
290
+ callEzoicSingle(id);
291
+ });
292
+ }
293
+
294
+ function refresh() {
295
+ if (!settings) return;
296
+ if (userExcluded()) return;
297
+
298
+ if (refreshInFlight) { refreshQueued = true; return; }
299
+ refreshInFlight = true;
300
+ try {
301
+ const key = getPageKey();
302
+ if (pageKey !== key) {
303
+ pageKey = key;
304
+ cleanupForNewPage();
229
305
  }
230
- callEzoic(newIds);
231
- }
232
- } finally {
233
- inFlight = false;
234
- if (rerunRequested) {
235
- rerunRequested = false;
236
- setTimeout(refreshAds, 120);
306
+
307
+ setupAutoHeightOnce();
308
+
309
+ if (isTopicPage()) injectInTopic();
310
+ else if (isCategoryTopicList()) injectInCategory();
311
+ } finally {
312
+ refreshInFlight = false;
313
+ if (refreshQueued) { refreshQueued = false; setTimeout(refresh, 50); }
237
314
  }
238
315
  }
239
- }
240
316
 
241
- function debounceRefresh() {
242
- clearTimeout(debounceTimer);
243
- debounceTimer = setTimeout(refreshAds, 180);
244
- }
317
+ function loadSettings(cb) {
318
+ socket.emit('admin.settings.get', { hash: SETTINGS_NS }, function (err, data) {
319
+ settings = data || {};
320
+ cb && cb();
321
+ });
322
+ }
323
+
324
+ function boot() {
325
+ loadSettings(function () {
326
+ refresh();
327
+ setTimeout(refresh, 1200);
328
+ });
329
+ }
330
+
331
+ $(document).ready(boot);
332
+ $(window).on('action:ajaxify.end', boot);
333
+
334
+ $(window).on('action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', function () {
335
+ refresh();
336
+ setTimeout(refresh, 600);
337
+ });
338
+
339
+ $(window).on('action:ajaxify.start', function () {
340
+ pageKey = null;
341
+ cleanupForNewPage();
342
+ });
245
343
 
246
- $(document).ready(debounceRefresh);
247
- $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded', debounceRefresh);
248
- setTimeout(debounceRefresh, 1800);
344
+ })();
@@ -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,55 @@
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>
11
-
12
- <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>
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>
17
7
 
8
+ <form role="form" class="ezoic-infinite-settings">
18
9
  <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"></select>
12
+ <div class="form-text">Maintenez Ctrl/Cmd pour sélectionner plusieurs groupes. Liste triée par ordre alphabétique.</div>
21
13
  </div>
22
14
 
23
15
  <hr/>
24
16
 
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>
17
+ <h3>Entre les topics dans une catégorie (liste des sujets)</h3>
27
18
 
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>
19
+ <div class="form-check form-switch mb-2">
20
+ <input class="form-check-input" type="checkbox" name="enableBetweenAds">
21
+ <label class="form-check-label">Activer les pubs entre les topics</label>
31
22
  </div>
32
23
 
33
24
  <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>
25
+ <label class="form-label">Intervalle (insérer après chaque N topics)</label>
26
+ <input type="number" class="form-control" name="intervalTopics" min="1" step="1">
37
27
  </div>
38
28
 
39
29
  <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">
30
+ <label class="form-label">Pool d'IDs de placeholder (un par ligne)</label>
31
+ <textarea class="form-control" name="placeholderIds" rows="6"></textarea>
42
32
  </div>
43
33
 
44
34
  <hr/>
45
35
 
46
- <h4 class="mt-3">Exclusions</h4>
36
+ <h3>Dans les topics (entre les messages)</h3>
37
+
38
+ <div class="form-check form-switch mb-2">
39
+ <input class="form-check-input" type="checkbox" name="enableMessageAds">
40
+ <label class="form-check-label">Activer les pubs entre les messages</label>
41
+ </div>
42
+
43
+ <div class="mb-3">
44
+ <label class="form-label">Intervalle (insérer après chaque N messages)</label>
45
+ <input type="number" class="form-control" name="messageIntervalPosts" min="1" step="1">
46
+ </div>
47
+
47
48
  <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>
49
+ <label class="form-label">Pool d'IDs de placeholder pour messages (un par ligne)</label>
50
+ <textarea class="form-control" name="messagePlaceholderIds" rows="6"></textarea>
55
51
  </div>
56
52
 
57
- <button id="save" class="btn btn-primary">Enregistrer</button>
53
+ <button type="button" class="btn btn-primary ezoic-infinite-save">Enregistrer</button>
58
54
  </form>
59
55
  </div>