nodebb-plugin-ezoic-infinite 0.8.9 → 0.9.1
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 +20 -80
- package/package.json +2 -5
- package/plugin.json +2 -2
- package/public/admin.js +45 -23
- package/public/client.js +287 -336
- package/public/style.css +4 -2
- package/public/templates/admin/plugins/ezoic-infinite.tpl +35 -35
package/library.js
CHANGED
|
@@ -2,96 +2,36 @@
|
|
|
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
|
|
8
|
-
const plugin = {};
|
|
6
|
+
const Plugin = {};
|
|
9
7
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
if (Array.isArray(value)) return value;
|
|
13
|
-
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
14
|
-
}
|
|
15
|
-
|
|
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
|
-
}
|
|
22
|
-
|
|
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),
|
|
38
|
-
|
|
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),
|
|
8
|
+
Plugin.init = async function (params) {
|
|
9
|
+
const { router, middleware } = params;
|
|
43
10
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
11
|
+
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
|
|
12
|
+
router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
|
|
13
|
+
};
|
|
47
14
|
|
|
48
|
-
async function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
15
|
+
async function renderAdmin(req, res) {
|
|
16
|
+
const settings = await meta.settings.get('ezoic-infinite');
|
|
17
|
+
// groups list for exclusions
|
|
18
|
+
const groupList = await groups.getGroups('groups:visible:createtime', 0, -1);
|
|
19
|
+
groupList.sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
|
|
20
|
+
res.render('admin/plugins/ezoic-infinite', {
|
|
21
|
+
title: 'Ezoic Infinite',
|
|
22
|
+
settings,
|
|
23
|
+
groups: groupList,
|
|
24
|
+
});
|
|
52
25
|
}
|
|
53
26
|
|
|
54
|
-
|
|
27
|
+
Plugin.addAdminNavigation = async function (header) {
|
|
55
28
|
header.plugins = header.plugins || [];
|
|
56
29
|
header.plugins.push({
|
|
57
30
|
route: '/plugins/ezoic-infinite',
|
|
58
|
-
icon: 'fa-
|
|
59
|
-
name: 'Ezoic Infinite
|
|
31
|
+
icon: 'fa-bullhorn',
|
|
32
|
+
name: 'Ezoic Infinite',
|
|
60
33
|
});
|
|
61
34
|
return header;
|
|
62
35
|
};
|
|
63
36
|
|
|
64
|
-
|
|
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;
|
|
37
|
+
module.exports = Plugin;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Ezoic ads
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Ezoic ads injection for NodeBB infinite scroll (topics list + topic posts) using a pool of placeholder IDs.",
|
|
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
package/public/admin.js
CHANGED
|
@@ -1,29 +1,51 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* global $, app, socket */
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
(function () {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
$('
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
form.find('[name="enableBetweenAds"]').prop('checked', data.enableBetweenAds === true || data.enableBetweenAds === 'on');
|
|
14
|
+
form.find('[name="intervalTopics"]').val(parseInt(data.intervalTopics, 10) || 6);
|
|
15
|
+
form.find('[name="placeholderIds"]').val(data.placeholderIds || '');
|
|
16
|
+
|
|
17
|
+
form.find('[name="enableMessageAds"]').prop('checked', data.enableMessageAds === true || data.enableMessageAds === 'on');
|
|
18
|
+
form.find('[name="messageIntervalPosts"]').val(parseInt(data.messageIntervalPosts, 10) || 3);
|
|
19
|
+
form.find('[name="messagePlaceholderIds"]').val(data.messagePlaceholderIds || '');
|
|
20
|
+
|
|
21
|
+
const selected = (data.excludedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
22
|
+
form.find('[name="excludedGroups"] option').each(function () {
|
|
23
|
+
$(this).prop('selected', selected.includes($(this).val()));
|
|
23
24
|
});
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
function save() {
|
|
29
|
+
const form = $('.ezoic-infinite-settings');
|
|
30
|
+
const payload = {
|
|
31
|
+
enableBetweenAds: form.find('[name="enableBetweenAds"]').is(':checked'),
|
|
32
|
+
intervalTopics: parseInt(form.find('[name="intervalTopics"]').val(), 10) || 6,
|
|
33
|
+
placeholderIds: form.find('[name="placeholderIds"]').val() || '',
|
|
34
|
+
enableMessageAds: form.find('[name="enableMessageAds"]').is(':checked'),
|
|
35
|
+
messageIntervalPosts: parseInt(form.find('[name="messageIntervalPosts"]').val(), 10) || 3,
|
|
36
|
+
messagePlaceholderIds: form.find('[name="messagePlaceholderIds"]').val() || '',
|
|
37
|
+
excludedGroups: (form.find('[name="excludedGroups"]').val() || []).join(','),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
socket.emit('admin.settings.set', { hash: namespace, values: payload }, function (err) {
|
|
41
|
+
if (err) {
|
|
42
|
+
app.alertError(err.message || err);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
app.alertSuccess('Settings saved');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$('.ezoic-infinite-save').on('click', save);
|
|
50
|
+
load();
|
|
51
|
+
});
|
package/public/client.js
CHANGED
|
@@ -1,380 +1,331 @@
|
|
|
1
|
+
/* global $, ajaxify, app */
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
window.ezoicInfiniteLoaded
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let
|
|
11
|
-
let rerunRequested = false;
|
|
12
|
-
|
|
13
|
-
// Per-page state (keyed by tid/cid)
|
|
14
|
-
let pageKey = null;
|
|
15
|
-
|
|
16
|
-
// Topic page state: anchor ads to absolute post number (not DOM index)
|
|
17
|
-
let seenAfterPostNo = new Set(); // post numbers we've already inserted an ad after
|
|
18
|
-
let usedIds = new Set(); // ids currently in DOM
|
|
19
|
-
let fifo = []; // [{afterPostNo, id}]
|
|
20
|
-
|
|
21
|
-
// Category page state: anchor to absolute topic position if available
|
|
22
|
-
let seenAfterTopicPos = new Set(); // topic positions we've inserted after
|
|
23
|
-
let fifoCat = []; // [{afterPos, id}]
|
|
24
|
-
|
|
25
|
-
function resetState() {
|
|
26
|
-
seenAfterPostNo = new Set();
|
|
27
|
-
seenAfterTopicPos = new Set();
|
|
28
|
-
usedIds = new Set();
|
|
29
|
-
fifo = [];
|
|
30
|
-
fifoCat = [];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getPageKey() {
|
|
34
|
-
try {
|
|
35
|
-
if (ajaxify && ajaxify.data) {
|
|
36
|
-
if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
|
|
37
|
-
if (ajaxify.data.cid) return 'category:' + ajaxify.data.cid;
|
|
38
|
-
}
|
|
39
|
-
} catch (e) {}
|
|
40
|
-
return window.location.pathname;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function parsePool(raw) {
|
|
44
|
-
if (!raw) return [];
|
|
45
|
-
return Array.from(new Set(
|
|
46
|
-
String(raw).split(/[\n,;\s]+/)
|
|
47
|
-
.map(x => parseInt(x, 10))
|
|
48
|
-
.filter(n => Number.isFinite(n) && n > 0)
|
|
49
|
-
));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function fetchConfig() {
|
|
53
|
-
if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
|
|
54
|
-
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
55
|
-
cachedConfig = await res.json();
|
|
56
|
-
lastFetch = Date.now();
|
|
57
|
-
return cachedConfig;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function isTopicPage() {
|
|
61
|
-
return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isCategoryTopicListPage() {
|
|
65
|
-
return $('li[component="category/topic"]').length > 0;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getTopicPosts() {
|
|
69
|
-
const $primary = $('[component="post"][data-pid]');
|
|
70
|
-
if ($primary.length) return $primary;
|
|
71
|
-
|
|
72
|
-
// fallback: top-level with post/content
|
|
73
|
-
return $('[data-pid]').filter(function () {
|
|
74
|
-
const $el = $(this);
|
|
75
|
-
const hasContent = $el.find('[component="post/content"]').length > 0;
|
|
76
|
-
const nested = $el.parents('[data-pid]').length > 0;
|
|
77
|
-
return hasContent && !nested;
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getCategoryTopicItems() {
|
|
82
|
-
return $('li[component="category/topic"]');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// If target's parent is UL/OL, wrapper MUST be LI (otherwise browser may move it to top)
|
|
86
|
-
function wrapperTagFor($target) {
|
|
87
|
-
if (!$target || !$target.length) return 'div';
|
|
88
|
-
const parentTag = ($target.parent().prop('tagName') || '').toUpperCase();
|
|
89
|
-
if (parentTag === 'UL' || parentTag === 'OL') return 'li';
|
|
90
|
-
const selfTag = ($target.prop('tagName') || '').toUpperCase();
|
|
91
|
-
if (selfTag === 'LI') return 'li';
|
|
92
|
-
return 'div';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function makeWrapperLike($target, classes, innerHtml, attrs) {
|
|
96
|
-
const tag = wrapperTagFor($target);
|
|
97
|
-
const attrStr = attrs ? ' ' + attrs : '';
|
|
98
|
-
if (tag === 'li') {
|
|
99
|
-
return '<li class="' + classes + ' list-unstyled"' + attrStr + '>' + innerHtml + '</li>';
|
|
100
|
-
}
|
|
101
|
-
return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
|
|
102
|
-
}
|
|
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;
|
|
103
12
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
13
|
+
// Separate pools for category/topic
|
|
14
|
+
let usedTopic = new Set();
|
|
15
|
+
let usedCat = new Set();
|
|
16
|
+
let fifoTopic = []; // [{id, afterNo}]
|
|
17
|
+
let fifoCat = []; // [{id, afterPos}]
|
|
107
18
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
19
|
+
function parsePool(text) {
|
|
20
|
+
return String(text || '')
|
|
21
|
+
.split(/\r?\n/)
|
|
22
|
+
.map(s => s.trim())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.map(s => parseInt(s, 10))
|
|
25
|
+
.filter(n => Number.isFinite(n) && n > 0);
|
|
111
26
|
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function removeOldestTopicAd() {
|
|
116
|
-
fifo.sort((a, b) => a.afterPostNo - b.afterPostNo);
|
|
117
|
-
const old = fifo.shift();
|
|
118
|
-
if (!old) return false;
|
|
119
|
-
|
|
120
|
-
const sel = '.ezoic-ad-post[data-ezoic-after="' + old.afterPostNo + '"][data-ezoic-id="' + old.id + '"]';
|
|
121
|
-
const $el = $(sel);
|
|
122
|
-
if ($el.length) $el.remove();
|
|
123
|
-
|
|
124
|
-
usedIds.delete(old.id);
|
|
125
|
-
// DO NOT delete seenAfterPostNo to prevent re-insertion in the top area
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function removeOldestCategoryAd() {
|
|
130
|
-
fifoCat.sort((a, b) => a.afterPos - b.afterPos);
|
|
131
|
-
const old = fifoCat.shift();
|
|
132
|
-
if (!old) return false;
|
|
133
|
-
|
|
134
|
-
const sel = '.ezoic-ad-topic[data-ezoic-after="' + old.afterPos + '"][data-ezoic-id="' + old.id + '"]';
|
|
135
|
-
const $el = $(sel);
|
|
136
|
-
if ($el.length) $el.remove();
|
|
137
|
-
|
|
138
|
-
usedIds.delete(old.id);
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function callEzoic(ids) {
|
|
143
|
-
// ids optional; if omitted, we will scan DOM for unrendered placeholders
|
|
144
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
145
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
146
|
-
|
|
147
|
-
const collect = function () {
|
|
148
|
-
const list = [];
|
|
149
|
-
document.querySelectorAll('.ezoic-ad [id^="ezoic-pub-ad-placeholder-"]').forEach(function (ph) {
|
|
150
|
-
const idStr = ph.id.replace('ezoic-pub-ad-placeholder-', '');
|
|
151
|
-
const id = parseInt(idStr, 10);
|
|
152
|
-
if (!Number.isFinite(id) || id <= 0) return;
|
|
153
|
-
|
|
154
|
-
const wrap = ph.closest('.ezoic-ad');
|
|
155
|
-
if (!wrap) return;
|
|
156
|
-
if (wrap.getAttribute('data-ezoic-rendered') === '1') return;
|
|
157
27
|
|
|
158
|
-
|
|
159
|
-
|
|
28
|
+
function loadSettings(cb) {
|
|
29
|
+
socket.emit('admin.settings.get', { hash: SETTINGS_NS }, function (err, data) {
|
|
30
|
+
settings = data || {};
|
|
31
|
+
cb && cb();
|
|
160
32
|
});
|
|
161
|
-
|
|
162
|
-
return Array.from(new Set(list));
|
|
163
|
-
};
|
|
33
|
+
}
|
|
164
34
|
|
|
165
|
-
|
|
166
|
-
|
|
35
|
+
function userExcluded() {
|
|
36
|
+
try {
|
|
37
|
+
const raw = (settings && settings.excludedGroups) ? String(settings.excludedGroups) : '';
|
|
38
|
+
if (!raw) return false;
|
|
39
|
+
const excluded = raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
40
|
+
if (!excluded.length) return false;
|
|
41
|
+
const myGroups = (app.user && app.user.groups) ? app.user.groups : [];
|
|
42
|
+
return excluded.some(g => myGroups.includes(g));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
167
47
|
|
|
168
|
-
|
|
48
|
+
function getPageKey() {
|
|
169
49
|
try {
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
return
|
|
50
|
+
if (ajaxify && ajaxify.data) {
|
|
51
|
+
if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
|
|
52
|
+
if (ajaxify.data.cid) return 'cid:' + ajaxify.data.cid + ':' + window.location.pathname;
|
|
173
53
|
}
|
|
174
54
|
} catch (e) {}
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
55
|
+
return window.location.pathname;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTopicPage() {
|
|
59
|
+
try { if (ajaxify && ajaxify.data && ajaxify.data.tid) return true; } catch (e) {}
|
|
60
|
+
return /^\/topic\//.test(window.location.pathname);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isCategoryTopicList() {
|
|
64
|
+
return $('li[component="category/topic"]').length > 0 && !isTopicPage();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cleanupForNewPage() {
|
|
68
|
+
$('.ezoic-ad').remove();
|
|
69
|
+
usedTopic = new Set();
|
|
70
|
+
usedCat = new Set();
|
|
71
|
+
fifoTopic = [];
|
|
72
|
+
fifoCat = [];
|
|
73
|
+
}
|
|
193
74
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
75
|
+
function pickNextId(pool, usedSet) {
|
|
76
|
+
for (const id of pool) {
|
|
77
|
+
if (!usedSet.has(id)) return id;
|
|
197
78
|
}
|
|
198
|
-
|
|
199
|
-
setTimeout(retry, 800);
|
|
79
|
+
return null;
|
|
200
80
|
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function getPostNumber($post) {
|
|
204
|
-
const di = parseInt($post.attr('data-index'), 10);
|
|
205
|
-
if (Number.isFinite(di) && di > 0) return di;
|
|
206
|
-
|
|
207
|
-
const txt = ($post.find('a.post-index').first().text() || '').trim();
|
|
208
|
-
const m = txt.match(/#\s*(\d+)/);
|
|
209
|
-
if (m) return parseInt(m[1], 10);
|
|
210
|
-
|
|
211
|
-
return NaN;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function getTopicPos($item) {
|
|
215
|
-
const pos = parseInt($item.attr('data-index'), 10);
|
|
216
|
-
if (Number.isFinite(pos) && pos >= 0) return pos + 1;
|
|
217
|
-
const schemaPos = parseInt($item.find('meta[itemprop="position"]').attr('content'), 10);
|
|
218
|
-
if (Number.isFinite(schemaPos) && schemaPos > 0) return schemaPos;
|
|
219
|
-
return NaN;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function injectTopicMessageAds($posts, pool, interval) {
|
|
223
|
-
const newIds = [];
|
|
224
|
-
|
|
225
|
-
$posts.each(function () {
|
|
226
|
-
const $p = $(this);
|
|
227
|
-
// Never insert after the last real post: it can break NodeBB infinite scroll
|
|
228
|
-
if ($p.is($posts.last())) return;
|
|
229
|
-
const postNo = getPostNumber($p);
|
|
230
|
-
if (!Number.isFinite(postNo) || postNo <= 0) return;
|
|
231
|
-
|
|
232
|
-
if (postNo % interval !== 0) return;
|
|
233
|
-
if (seenAfterPostNo.has(postNo)) return;
|
|
234
|
-
|
|
235
|
-
let id = pickNextId(pool);
|
|
236
|
-
if (!id) { return; }
|
|
237
|
-
|
|
238
|
-
const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
|
|
239
|
-
const html = makeWrapperLike(
|
|
240
|
-
$p,
|
|
241
|
-
'ezoic-ad-post ezoic-ad',
|
|
242
|
-
inner,
|
|
243
|
-
'data-ezoic-after="' + postNo + '" data-ezoic-id="' + id + '"'
|
|
244
|
-
);
|
|
245
81
|
|
|
246
|
-
|
|
82
|
+
function destroyPlaceholder(id) {
|
|
83
|
+
try {
|
|
84
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
85
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
86
|
+
if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
87
|
+
window.ezstandalone.destroyPlaceholders(id);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
window.ezstandalone.cmd.push(function () {
|
|
91
|
+
try { window.ezstandalone.destroyPlaceholders(id); } catch (e) {}
|
|
92
|
+
});
|
|
93
|
+
} catch (e) {}
|
|
94
|
+
}
|
|
247
95
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
96
|
+
function callEzoic(ids) {
|
|
97
|
+
if (!ids || !ids.length) return;
|
|
98
|
+
try {
|
|
99
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
100
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
101
|
+
const run = function () {
|
|
102
|
+
try {
|
|
103
|
+
if (typeof window.ezstandalone.showAds === 'function') {
|
|
104
|
+
window.ezstandalone.showAds.apply(window.ezstandalone, ids);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {}
|
|
108
|
+
return false;
|
|
109
|
+
};
|
|
110
|
+
window.ezstandalone.cmd.push(function () { run(); });
|
|
111
|
+
// retries in case ez loads late
|
|
112
|
+
let tries = 0;
|
|
113
|
+
const tick = function () {
|
|
114
|
+
tries++;
|
|
115
|
+
if (run() || tries >= 10) return;
|
|
116
|
+
setTimeout(tick, 800);
|
|
117
|
+
};
|
|
118
|
+
setTimeout(tick, 800);
|
|
119
|
+
} catch (e) {}
|
|
120
|
+
}
|
|
253
121
|
|
|
254
|
-
|
|
255
|
-
|
|
122
|
+
// Auto height: show wrapper only when placeholder gets children
|
|
123
|
+
function setupAutoHeight() {
|
|
124
|
+
if (window.__ezoicAutoHeight) return;
|
|
125
|
+
window.__ezoicAutoHeight = true;
|
|
256
126
|
|
|
257
|
-
function
|
|
258
|
-
|
|
127
|
+
const mark = function (wrap) {
|
|
128
|
+
if (!wrap) return;
|
|
129
|
+
const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
|
|
130
|
+
if (ph && ph.children && ph.children.length) {
|
|
131
|
+
wrap.classList.add('ezoic-filled');
|
|
132
|
+
}
|
|
133
|
+
};
|
|
259
134
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if ($it.is($items.last())) return;
|
|
264
|
-
const pos = getTopicPos($it);
|
|
265
|
-
if (!Number.isFinite(pos) || pos <= 0) return;
|
|
135
|
+
const scan = function () {
|
|
136
|
+
document.querySelectorAll('.ezoic-ad').forEach(mark);
|
|
137
|
+
};
|
|
266
138
|
|
|
267
|
-
|
|
268
|
-
|
|
139
|
+
scan();
|
|
140
|
+
setInterval(scan, 1000);
|
|
269
141
|
|
|
270
|
-
|
|
271
|
-
|
|
142
|
+
try {
|
|
143
|
+
const mo = new MutationObserver(scan);
|
|
144
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
145
|
+
} catch (e) {}
|
|
146
|
+
}
|
|
272
147
|
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
'data-ezoic-after="' + pos + '" data-ezoic-id="' + id + '"'
|
|
148
|
+
function insertAfter($target, id, kind, afterVal) {
|
|
149
|
+
const wrap = $(
|
|
150
|
+
'<div class="ezoic-ad ezoic-ad-' + kind + '" data-ezoic-id="' + id + '" data-ezoic-after="' + afterVal + '">' +
|
|
151
|
+
'<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>' +
|
|
152
|
+
'</div>'
|
|
279
153
|
);
|
|
154
|
+
$target.after(wrap);
|
|
155
|
+
return wrap;
|
|
156
|
+
}
|
|
280
157
|
|
|
281
|
-
|
|
158
|
+
function recycleTopic($posts) {
|
|
159
|
+
fifoTopic.sort((a,b) => a.afterNo - b.afterNo);
|
|
160
|
+
while (fifoTopic.length) {
|
|
161
|
+
const old = fifoTopic.shift();
|
|
162
|
+
const sel = '.ezoic-ad-topic[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterNo + '"]';
|
|
163
|
+
const $el = $(sel);
|
|
164
|
+
if (!$el.length) continue;
|
|
165
|
+
|
|
166
|
+
// don't recycle if this is right before the sentinel (after last post)
|
|
167
|
+
try {
|
|
168
|
+
const $last = $posts.last();
|
|
169
|
+
if ($last.length && $el.prev().is($last)) {
|
|
170
|
+
fifoTopic.push(old);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {}
|
|
282
174
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
175
|
+
$el.remove();
|
|
176
|
+
usedTopic.delete(old.id);
|
|
177
|
+
destroyPlaceholder(old.id);
|
|
178
|
+
return old.id;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
288
182
|
|
|
289
|
-
|
|
290
|
-
|
|
183
|
+
function recycleCat($items) {
|
|
184
|
+
fifoCat.sort((a,b) => a.afterPos - b.afterPos);
|
|
185
|
+
while (fifoCat.length) {
|
|
186
|
+
const old = fifoCat.shift();
|
|
187
|
+
const sel = '.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterPos + '"]';
|
|
188
|
+
const $el = $(sel);
|
|
189
|
+
if (!$el.length) continue;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const $last = $items.last();
|
|
193
|
+
if ($last.length && $el.prev().is($last)) {
|
|
194
|
+
fifoCat.push(old);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {}
|
|
291
198
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
199
|
+
$el.remove();
|
|
200
|
+
usedCat.delete(old.id);
|
|
201
|
+
destroyPlaceholder(old.id);
|
|
202
|
+
return old.id;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
298
205
|
}
|
|
299
206
|
|
|
300
|
-
|
|
301
|
-
|
|
207
|
+
function injectInTopic() {
|
|
208
|
+
if (!(settings && (settings.enableMessageAds === true || settings.enableMessageAds === 'on'))) return;
|
|
209
|
+
const interval = parseInt(settings.messageIntervalPosts, 10) || 3;
|
|
210
|
+
const pool = parsePool(settings.messagePlaceholderIds);
|
|
211
|
+
if (!pool.length) return;
|
|
212
|
+
|
|
213
|
+
const $posts = $('[component="post"][data-pid]');
|
|
214
|
+
if (!$posts.length) return;
|
|
302
215
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
216
|
+
const newIds = [];
|
|
217
|
+
$posts.each(function (idx) {
|
|
218
|
+
const postNo = idx + 1; // 1-based within loaded set
|
|
219
|
+
if (postNo % interval !== 0) return;
|
|
306
220
|
|
|
307
|
-
|
|
308
|
-
|
|
221
|
+
// Do not insert after the last post in DOM (to keep infinite scroll sentinel stable)
|
|
222
|
+
if (idx === $posts.length - 1) return;
|
|
309
223
|
|
|
310
|
-
|
|
311
|
-
|
|
224
|
+
const $post = $(this);
|
|
225
|
+
const existing = $post.next('.ezoic-ad-topic');
|
|
226
|
+
if (existing.length) return;
|
|
312
227
|
|
|
313
|
-
|
|
314
|
-
|
|
228
|
+
let id = pickNextId(pool, usedTopic);
|
|
229
|
+
if (!id) {
|
|
230
|
+
id = recycleTopic($posts);
|
|
231
|
+
if (!id) return;
|
|
232
|
+
}
|
|
233
|
+
usedTopic.add(id);
|
|
234
|
+
fifoTopic.push({ id, afterNo: postNo });
|
|
235
|
+
|
|
236
|
+
insertAfter($post, id, 'topic', postNo).addClass('ezoic-ad-topic');
|
|
237
|
+
newIds.push(id);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (newIds.length) callEzoic(newIds);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function injectInCategory() {
|
|
244
|
+
if (!(settings && (settings.enableBetweenAds === true || settings.enableBetweenAds === 'on'))) return;
|
|
245
|
+
const interval = parseInt(settings.intervalTopics, 10) || 6;
|
|
246
|
+
const pool = parsePool(settings.placeholderIds);
|
|
247
|
+
if (!pool.length) return;
|
|
248
|
+
|
|
249
|
+
const $items = $('li[component="category/topic"]');
|
|
250
|
+
if (!$items.length) return;
|
|
315
251
|
|
|
316
252
|
const newIds = [];
|
|
253
|
+
$items.each(function (idx) {
|
|
254
|
+
const pos = idx + 1;
|
|
255
|
+
if (pos % interval !== 0) return;
|
|
317
256
|
|
|
318
|
-
|
|
319
|
-
const $items = getCategoryTopicItems();
|
|
320
|
-
if (cfg.enableBetweenAds && betweenPool.length && $items.length) {
|
|
321
|
-
newIds.push(...injectCategoryBetweenAds($items, betweenPool, betweenInterval));
|
|
322
|
-
}
|
|
323
|
-
callEzoic(newIds);
|
|
324
|
-
callEzoic();
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
257
|
+
if (idx === $items.length - 1) return;
|
|
327
258
|
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
|
|
259
|
+
const $li = $(this);
|
|
260
|
+
const existing = $li.next('.ezoic-ad-between');
|
|
261
|
+
if (existing.length) return;
|
|
262
|
+
|
|
263
|
+
let id = pickNextId(pool, usedCat);
|
|
264
|
+
if (!id) {
|
|
265
|
+
id = recycleCat($items);
|
|
266
|
+
if (!id) return;
|
|
332
267
|
}
|
|
333
|
-
|
|
334
|
-
|
|
268
|
+
usedCat.add(id);
|
|
269
|
+
fifoCat.push({ id, afterPos: pos });
|
|
270
|
+
|
|
271
|
+
insertAfter($li, id, 'between', pos).addClass('ezoic-ad-between');
|
|
272
|
+
newIds.push(id);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (newIds.length) callEzoic(newIds);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function refresh() {
|
|
279
|
+
if (!settings) return;
|
|
280
|
+
if (userExcluded()) return;
|
|
281
|
+
|
|
282
|
+
const key = getPageKey();
|
|
283
|
+
if (pageKey !== key) {
|
|
284
|
+
pageKey = key;
|
|
285
|
+
cleanupForNewPage();
|
|
335
286
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
287
|
+
|
|
288
|
+
setupAutoHeight();
|
|
289
|
+
|
|
290
|
+
if (isTopicPage()) {
|
|
291
|
+
injectInTopic();
|
|
292
|
+
} else if (isCategoryTopicList()) {
|
|
293
|
+
injectInCategory();
|
|
341
294
|
}
|
|
342
295
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
setTimeout(debounceRefresh, 2200);
|
|
353
|
-
|
|
354
|
-
// Fallback: some themes/devices don't emit the expected events for infinite scroll.
|
|
355
|
-
// Observe DOM additions and trigger a refresh when new posts/topics are appended/prepended.
|
|
356
|
-
(function setupEzoicObserver() {
|
|
357
|
-
if (window.__ezoicInfiniteObserver) return;
|
|
358
|
-
try {
|
|
359
|
-
const obs = new MutationObserver(function (mutations) {
|
|
360
|
-
for (const m of mutations) {
|
|
361
|
-
if (!m.addedNodes || !m.addedNodes.length) continue;
|
|
362
|
-
for (const n of m.addedNodes) {
|
|
363
|
-
if (!n || n.nodeType !== 1) continue;
|
|
364
|
-
// direct match
|
|
365
|
-
if (n.matches && (n.matches('[component="post"][data-pid]') || n.matches('li[component="category/topic"]'))) {
|
|
366
|
-
debounceRefresh();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
// descendant match
|
|
370
|
-
if (n.querySelector && (n.querySelector('[component="post"][data-pid]') || n.querySelector('li[component="category/topic"]'))) {
|
|
371
|
-
debounceRefresh();
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
296
|
+
|
|
297
|
+
// triggers: hard load + ajaxify + infinite scroll events
|
|
298
|
+
function boot() {
|
|
299
|
+
loadSettings(function () {
|
|
300
|
+
refresh();
|
|
301
|
+
// extra delayed refresh for late ez init
|
|
302
|
+
setTimeout(refresh, 1500);
|
|
303
|
+
setTimeout(refresh, 5000);
|
|
304
|
+
setTimeout(refresh, 10000);
|
|
376
305
|
});
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Hard load
|
|
309
|
+
$(document).ready(boot);
|
|
310
|
+
// Ajaxify nav end
|
|
311
|
+
$(window).on('action:ajaxify.end', boot);
|
|
312
|
+
// Infinite scroll loads (varies by view)
|
|
313
|
+
$(window).on('action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', function () {
|
|
314
|
+
refresh();
|
|
315
|
+
setTimeout(refresh, 800);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Clean only on real navigation start (not infinite loads)
|
|
319
|
+
$(window).on('action:ajaxify.start', function (ev, data) {
|
|
320
|
+
try {
|
|
321
|
+
const url = data && (data.url || data.href);
|
|
322
|
+
if (!url) return;
|
|
323
|
+
const a = document.createElement('a');
|
|
324
|
+
a.href = url;
|
|
325
|
+
if (a.pathname && a.pathname === window.location.pathname) return;
|
|
326
|
+
} catch (e) {}
|
|
327
|
+
pageKey = null;
|
|
328
|
+
cleanupForNewPage();
|
|
329
|
+
});
|
|
330
|
+
|
|
380
331
|
})();
|
package/public/style.css
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
.ezoic-ad-
|
|
2
|
-
.ezoic-ad-
|
|
1
|
+
.ezoic-ad{min-height:0 !important;height:auto !important;padding:0 !important;margin:0.5rem 0;}
|
|
2
|
+
.ezoic-ad:not(.ezoic-filled){display:none !important;}
|
|
3
|
+
.ezoic-ad .ezoic-ad-inner{padding:0;margin:0;}
|
|
4
|
+
.ezoic-ad .ezoic-ad-inner > div{margin:0;padding:0;}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
1
|
<div class="acp-page-container">
|
|
2
|
-
<
|
|
2
|
+
<h1 class="mb-3">Ezoic Infinite</h1>
|
|
3
3
|
|
|
4
|
-
<
|
|
5
|
-
<
|
|
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
|
+
Placeholders format: <code><div id="ezoic-pub-ad-placeholder-XXX"></div></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
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
<input type="number" id="intervalPosts" name="intervalPosts" class="form-control" value="{intervalPosts}" min="1">
|
|
10
|
+
<label class="form-label">Exclude groups (ads disabled for members of these groups)</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">Hold Ctrl/Cmd to select multiple. Groups are sorted alphabetically.</div>
|
|
21
17
|
</div>
|
|
22
18
|
|
|
23
19
|
<hr/>
|
|
24
20
|
|
|
25
|
-
<
|
|
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>Between topics in category (topic list)</h3>
|
|
27
22
|
|
|
28
|
-
<div class="form-check mb-
|
|
29
|
-
<input class="form-check-input" type="checkbox"
|
|
30
|
-
<label class="form-check-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">Enable between-topic ads</label>
|
|
31
26
|
</div>
|
|
32
27
|
|
|
33
28
|
<div class="mb-3">
|
|
34
|
-
<label class="form-label"
|
|
35
|
-
<
|
|
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">Interval (insert after every 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"
|
|
41
|
-
<
|
|
34
|
+
<label class="form-label">Placeholder ID pool (one per line)</label>
|
|
35
|
+
<textarea class="form-control" name="placeholderIds" rows="6"></textarea>
|
|
42
36
|
</div>
|
|
43
37
|
|
|
44
38
|
<hr/>
|
|
45
39
|
|
|
46
|
-
<
|
|
40
|
+
<h3>Inside topics (between posts)</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">Enable between-post ads</label>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
47
|
<div class="mb-3">
|
|
48
|
-
<label class="form-label"
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</
|
|
54
|
-
<
|
|
48
|
+
<label class="form-label">Interval (insert after every N posts)</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">Message placeholder ID pool (one per line)</label>
|
|
54
|
+
<textarea class="form-control" name="messagePlaceholderIds" rows="6"></textarea>
|
|
55
55
|
</div>
|
|
56
56
|
|
|
57
|
-
<button
|
|
57
|
+
<button type="button" class="btn btn-primary ezoic-infinite-save">Save</button>
|
|
58
58
|
</form>
|
|
59
59
|
</div>
|