diff --git a/public/css/styles.css b/public/css/styles.css
index a064769..40d1a09 100644
--- a/public/css/styles.css
+++ b/public/css/styles.css
@@ -304,6 +304,12 @@ table.dtbl td { padding: 3px 8px; border-bottom: 1px solid var(--line-soft); whi
.iconbtn:hover { color: var(--err); background: var(--bg-3); }
.rule-feeds { display: flex; flex-wrap: wrap; gap: 4px 12px; max-height: 120px; overflow: auto;
border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; background: var(--bg); }
+.rule-matches { max-height: 160px; overflow: auto; border: 1px solid var(--line);
+ border-radius: 6px; background: var(--bg); font-size: 12px; }
+.match-row { padding: 3px 8px; border-bottom: 1px solid var(--line); white-space: nowrap;
+ overflow: hidden; text-overflow: ellipsis; }
+.match-row:last-child { border-bottom: none; }
+.match-row.grabbed { opacity: .55; }
.rule { border: 1px solid var(--line); border-radius: 6px; padding: 10px 12px; margin-bottom: 8px; background: var(--bg-1); }
.rule.off { opacity: .55; }
.rule-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
diff --git a/public/js/app.js b/public/js/app.js
index 993c113..56d4204 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -980,7 +980,7 @@ async function renderRssView(host, seq) {
${articles.map((a, i) => `
| ${f.esc(a.feed)} | ${f.esc(a.title)} |
${a.size ? f.bytes(a.size) : '—'} | ${f.esc(a.pubDate || '')} |
- ${(a.magnet || a.torrentUrl)
+ | ${(a.magnet || a.torrentUrl || a.link)
? ``
: '—'} |
`).join('')}
` : 'No articles yet.
'}
@@ -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: '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) => `
+ ${f.esc(a.feed)} ${f.esc(a.title)}${a.grabbed ? ' (grabbed)' : ''}
`).join('')
+ + (matches.length > 50 ? `…and ${matches.length - 50} more
` : '')
+ : 'No current articles match.
';
+ };
+ ['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 `