webui: always show RSS Add button + live rule match preview
- Article Add button now shows whenever the item has any source (magnet, enclosure, or a plain <link>/Atom href), and the manual download falls back to the link URL. Fixes missing + on feeds that only provide a <link> to the torrent. - The rule editor now shows a live "current matches" list that updates as you type the filters / toggle regex / pick feeds, mirroring the daemon's matcher, with grabbed items dimmed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0aeb039586
commit
b1f33d4194
2 changed files with 50 additions and 3 deletions
|
|
@ -980,7 +980,7 @@ async function renderRssView(host, seq) {
|
|||
${articles.map((a, i) => `<tr style="${a.isRead ? 'opacity:.5' : ''}">
|
||||
<td class="dim">${f.esc(a.feed)}</td><td>${f.esc(a.title)}</td>
|
||||
<td class="num">${a.size ? f.bytes(a.size) : '—'}</td><td class="num dim">${f.esc(a.pubDate || '')}</td>
|
||||
<td>${(a.magnet || a.torrentUrl)
|
||||
<td>${(a.magnet || a.torrentUrl || a.link)
|
||||
? `<button class="btn dl-art" data-art="${i}"${a.grabbed ? ' disabled' : ''}>${a.grabbed ? '✓ Added' : '+ Add'}</button>`
|
||||
: '<span class="faint">—</span>'}</td></tr>`).join('')}
|
||||
</tbody></table>` : '<div class="dim">No articles yet.</div>'}
|
||||
|
|
@ -1010,7 +1010,7 @@ async function renderRssView(host, seq) {
|
|||
}));
|
||||
host.querySelectorAll('.dl-art').forEach((b) => b.addEventListener('click', () => guard(async () => {
|
||||
const a = articles[+b.dataset.art];
|
||||
await api.rssDownload({ magnet: a.magnet || '', torrentUrl: a.torrentUrl || '', key: a.key || '' });
|
||||
await api.rssDownload({ magnet: a.magnet || '', torrentUrl: a.torrentUrl || a.link || '', key: a.key || '' });
|
||||
b.textContent = '✓ Added'; b.disabled = true;
|
||||
}, 'Failed to add torrent')));
|
||||
host.querySelectorAll('[data-rule-edit]').forEach((b) => b.addEventListener('click', () =>
|
||||
|
|
@ -1064,7 +1064,9 @@ function openRuleEditor(rule, feeds) {
|
|||
<label><input type="checkbox" id="rEnabled" ${r.enabled ? 'checked' : ''}> Enabled</label>
|
||||
<label><input type="checkbox" id="rRegex" ${r.useRegex ? 'checked' : ''}> Use regex</label>
|
||||
<label><input type="checkbox" id="rPaused" ${r.addPaused ? 'checked' : ''}> Add paused</label>
|
||||
</div>`,
|
||||
</div>
|
||||
<div class="field" style="margin-top:6px"><label>Current matches <span id="rMatchCount" class="dim"></span></label>
|
||||
<div id="rMatches" class="rule-matches"></div></div>`,
|
||||
[{ label: 'Cancel', act: closeModal }, {
|
||||
label: 'Save', primary: true, act: () => guard(async () => {
|
||||
const name = document.getElementById('rName').value.trim();
|
||||
|
|
@ -1084,9 +1086,48 @@ function openRuleEditor(rule, feeds) {
|
|||
renderView();
|
||||
}, 'Failed to save rule'),
|
||||
}]);
|
||||
|
||||
// Live preview of which current articles this rule matches.
|
||||
const allArticles = (feeds || []).flatMap((fd) => fd.articles.map((a) => ({ title: a.title, feed: fd.name, grabbed: a.grabbed })));
|
||||
const updateMatches = () => {
|
||||
const opts = {
|
||||
must: document.getElementById('rMust').value.trim(),
|
||||
mustNot: document.getElementById('rMustNot').value.trim(),
|
||||
regex: document.getElementById('rRegex').checked,
|
||||
feeds: [...document.querySelectorAll('.rule-feeds input:checked')].map((c) => c.value),
|
||||
};
|
||||
const matches = allArticles.filter((a) => ruleMatchesArticle(opts, a.feed, a.title));
|
||||
document.getElementById('rMatchCount').textContent = `— ${matches.length} of ${allArticles.length} article(s)`;
|
||||
const box = document.getElementById('rMatches');
|
||||
box.innerHTML = matches.length
|
||||
? matches.slice(0, 50).map((a) => `<div class="match-row${a.grabbed ? ' grabbed' : ''}">
|
||||
<span class="dim">${f.esc(a.feed)}</span> ${f.esc(a.title)}${a.grabbed ? ' <span class="faint">(grabbed)</span>' : ''}</div>`).join('')
|
||||
+ (matches.length > 50 ? `<div class="dim" style="padding:4px">…and ${matches.length - 50} more</div>` : '')
|
||||
: '<div class="dim" style="padding:4px">No current articles match.</div>';
|
||||
};
|
||||
['rMust', 'rMustNot'].forEach((id) => document.getElementById(id).addEventListener('input', updateMatches));
|
||||
document.getElementById('rRegex').addEventListener('change', updateMatches);
|
||||
document.querySelectorAll('.rule-feeds input').forEach((c) => c.addEventListener('change', updateMatches));
|
||||
updateMatches();
|
||||
setTimeout(() => document.getElementById(rule ? 'rMust' : 'rName')?.focus(), 0);
|
||||
}
|
||||
|
||||
// Client-side mirror of the daemon's rule matcher (for live preview).
|
||||
function ruleMatchesArticle(opts, feedName, title) {
|
||||
if (opts.feeds && opts.feeds.length && !opts.feeds.includes(feedName)) return false;
|
||||
if (opts.regex) {
|
||||
try {
|
||||
if (opts.must && !new RegExp(opts.must, 'i').test(title)) return false;
|
||||
if (opts.mustNot && new RegExp(opts.mustNot, 'i').test(title)) return false;
|
||||
} catch (e) { return false; } // invalid regex matches nothing
|
||||
} else {
|
||||
const t = title.toLowerCase();
|
||||
if (opts.must && !t.includes(opts.must.toLowerCase())) return false;
|
||||
if (opts.mustNot && t.includes(opts.mustNot.toLowerCase())) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderRule(r) {
|
||||
return `<div class="rule ${r.enabled ? '' : 'off'}">
|
||||
<div class="rule-head">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue