diff --git a/qbittorrent_performer_sorter.html b/qbittorrent_performer_sorter.html
index 23129b4..ed9c553 100644
--- a/qbittorrent_performer_sorter.html
+++ b/qbittorrent_performer_sorter.html
@@ -377,6 +377,7 @@
display: flex;
align-items: center;
gap: 0.5rem;
+ flex-wrap: wrap;
}
.search-input {
@@ -386,7 +387,30 @@
color: var(--text-main);
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
- min-width: 240px;
+ min-width: 210px;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+ }
+
+ .search-input:focus {
+ outline: none;
+ border-color: var(--accent-cyan);
+ box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.2);
+ }
+
+ .not-filter-input {
+ border-color: rgba(244, 63, 94, 0.4);
+ background: rgba(244, 63, 94, 0.04);
+ color: #fda4af;
+ min-width: 200px;
+ }
+
+ .not-filter-input::placeholder {
+ color: rgba(253, 164, 175, 0.55);
+ }
+
+ .not-filter-input:focus {
+ border-color: var(--accent-rose);
+ box-shadow: 0 0 0 2px rgba(244, 63, 94, 0.25);
}
.torrent-table {
@@ -1349,7 +1373,8 @@
-
+
+
@@ -1455,7 +1480,8 @@
-
+
+
@@ -1567,7 +1593,8 @@
-
+
+
@@ -1708,6 +1735,34 @@
.replace(/'/g, ''');
}
+ // Unified Positive and Negative (NOT) Filter Matcher
+ function matchFilterCriteria(textToSearch, positiveQuery, notQuery) {
+ const text = (textToSearch || '').toLowerCase();
+
+ // 1. Process explicit NOT filter (comma or semicolon separated terms)
+ if (notQuery) {
+ const notTerms = notQuery.toLowerCase().split(/[,|;]+/).map(s => s.trim()).filter(s => s.length > 0);
+ for (const term of notTerms) {
+ if (text.includes(term)) return false;
+ }
+ }
+
+ // 2. Process positive search input (also supports inline -term or !term exclusions)
+ if (positiveQuery) {
+ const tokens = positiveQuery.toLowerCase().split(/\s+/).filter(t => t.length > 0);
+ for (const tok of tokens) {
+ if (tok.startsWith('-') || tok.startsWith('!')) {
+ const negTerm = tok.slice(1);
+ if (negTerm && text.includes(negTerm)) return false;
+ } else {
+ if (!text.includes(tok)) return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
// ==================== Performer Image Hover Card Controller ====================
const hoverCard = document.getElementById('performer-hover-card');
const hoverCardImg = document.getElementById('hover-card-img');
@@ -1941,6 +1996,7 @@
const selectAllCheckbox = document.getElementById('th-select-all');
const selectionCountLabel = document.getElementById('table-selection-count');
const searchInput = document.getElementById('search-input');
+ const notFilterInput = document.getElementById('not-filter-input');
const btnFetch = document.getElementById('btn-fetch-torrents');
const btnAnalyzeSelected = document.getElementById('btn-ai-analyze-selected');
const btnAnalyzeAll = document.getElementById('btn-ai-analyze-all');
@@ -1959,6 +2015,7 @@
const smbSelectAllCheckbox = document.getElementById('th-smb-select-all');
const smbSelectionCountLabel = document.getElementById('smb-table-selection-count');
const smbSearchInput = document.getElementById('smb-search-input');
+ const smbNotFilterInput = document.getElementById('smb-not-filter-input');
const btnFetchSmb = document.getElementById('btn-fetch-smb');
const btnSmbAnalyzeSelected = document.getElementById('btn-smb-analyze-selected');
const btnSmbAnalyzeAll = document.getElementById('btn-smb-analyze-all');
@@ -3483,12 +3540,11 @@ Return ONLY valid JSON matching:
}
function renderTable() {
- const query = searchInput.value.trim().toLowerCase();
+ const query = searchInput ? searchInput.value.trim() : '';
+ const notQuery = notFilterInput ? notFilterInput.value.trim() : '';
const filtered = qbState.torrents.filter(t => {
- if (!query) return true;
- return t.name.toLowerCase().includes(query) ||
- (t.selectedPerformer && t.selectedPerformer.toLowerCase().includes(query)) ||
- (t.save_path && t.save_path.toLowerCase().includes(query));
+ const fullText = `${t.name} ${t.selectedPerformer || ''} ${t.save_path || ''} ${t.category || ''}`;
+ return matchFilterCriteria(fullText, query, notQuery);
});
if (filtered.length === 0) {
@@ -3780,10 +3836,11 @@ Return ONLY valid JSON matching:
selectAllCheckbox.addEventListener('change', (e) => {
const isChecked = e.target.checked;
- const query = searchInput.value.trim().toLowerCase();
+ const query = searchInput ? searchInput.value.trim() : '';
+ const notQuery = notFilterInput ? notFilterInput.value.trim() : '';
qbState.torrents.forEach(t => {
- const matches = !query || t.name.toLowerCase().includes(query) || (t.selectedPerformer && t.selectedPerformer.toLowerCase().includes(query));
- if (matches) {
+ const fullText = `${t.name} ${t.selectedPerformer || ''} ${t.save_path || ''} ${t.category || ''}`;
+ if (matchFilterCriteria(fullText, query, notQuery)) {
if (isChecked) qbState.selectedHashes.add(t.hash);
else qbState.selectedHashes.delete(t.hash);
}
@@ -3793,6 +3850,7 @@ Return ONLY valid JSON matching:
});
searchInput.addEventListener('input', () => renderTable());
+ if (notFilterInput) notFilterInput.addEventListener('input', () => renderTable());
// ==================== TAB 2: SMB Video Downloader Engine ====================
async function scanSmbFiles() {
@@ -4355,11 +4413,11 @@ Return ONLY a valid JSON object with an array named "results":
}
function renderSmbTable() {
- const query = smbSearchInput.value.trim().toLowerCase();
+ const query = smbSearchInput ? smbSearchInput.value.trim() : '';
+ const notQuery = smbNotFilterInput ? smbNotFilterInput.value.trim() : '';
const filtered = smbState.files.filter(f => {
- if (!query) return true;
- return f.name.toLowerCase().includes(query) ||
- (f.selectedPerformer && f.selectedPerformer.toLowerCase().includes(query));
+ const fullText = `${f.name} ${f.selectedPerformer || ''} ${f.path || ''}`;
+ return matchFilterCriteria(fullText, query, notQuery);
});
if (filtered.length === 0) {
@@ -4588,10 +4646,11 @@ Return ONLY a valid JSON object with an array named "results":
smbSelectAllCheckbox.addEventListener('change', (e) => {
const isChecked = e.target.checked;
- const query = smbSearchInput.value.trim().toLowerCase();
+ const query = smbSearchInput ? smbSearchInput.value.trim() : '';
+ const notQuery = smbNotFilterInput ? smbNotFilterInput.value.trim() : '';
smbState.files.forEach(f => {
- const matches = !query || f.name.toLowerCase().includes(query) || (f.selectedPerformer && f.selectedPerformer.toLowerCase().includes(query));
- if (matches) {
+ const fullText = `${f.name} ${f.selectedPerformer || ''} ${f.path || ''}`;
+ if (matchFilterCriteria(fullText, query, notQuery)) {
if (isChecked) smbState.selectedPaths.add(f.path);
else smbState.selectedPaths.delete(f.path);
}
@@ -4601,6 +4660,7 @@ Return ONLY a valid JSON object with an array named "results":
});
smbSearchInput.addEventListener('input', () => renderSmbTable());
+ if (smbNotFilterInput) smbNotFilterInput.addEventListener('input', () => renderSmbTable());
// Modal Close
document.getElementById('modal-close').addEventListener('click', () => {
@@ -5032,6 +5092,7 @@ Return ONLY a valid JSON object with an array named "results":
const toggleStashFullName = document.getElementById('toggle-stash-full-name');
const toggleStashHasScenes = document.getElementById('toggle-stash-has-scenes');
const stashImagesSearchInput = document.getElementById('stash-images-search-input');
+ const stashImagesNotFilterInput = document.getElementById('stash-images-not-filter-input');
const thStashImgSelectAll = document.getElementById('th-stash-img-select-all');
const stashImgSelectionCount = document.getElementById('stash-img-selection-count');
const stashPerformerCardsContainer = document.getElementById('stash-performer-cards-container');
@@ -5464,10 +5525,11 @@ Return ONLY a valid JSON object with an array named "results":
function renderStashPerformerCards() {
if (!stashPerformerCardsContainer) return;
- const query = (stashImagesSearchInput ? stashImagesSearchInput.value.trim().toLowerCase() : '');
+ const query = (stashImagesSearchInput ? stashImagesSearchInput.value.trim() : '');
+ const notQuery = (stashImagesNotFilterInput ? stashImagesNotFilterInput.value.trim() : '');
const filtered = stashImgState.performers.filter(p => {
- if (!query) return true;
- return (p.name || '').toLowerCase().includes(query);
+ const fullText = `${p.name || ''} ${p.disambiguation || ''} ${p.country || ''} ${(p.alias_list || []).join(' ')}`;
+ return matchFilterCriteria(fullText, query, notQuery);
});
if (filtered.length === 0) {
@@ -5475,7 +5537,7 @@ Return ONLY a valid JSON object with an array named "results":
🔍
No matching performers found
-
${query ? 'Try adjusting your search filter' : 'Click "Scan Stash Missing Images" to load performers'}
+
${query || notQuery ? 'Try adjusting your search or exclude filters' : 'Click "Scan Stash Missing Images" to load performers'}
`;
updateStashImagesStats();
@@ -5529,6 +5591,12 @@ Return ONLY a valid JSON object with an array named "results":
});
}
+ if (stashImagesNotFilterInput) {
+ stashImagesNotFilterInput.addEventListener('input', () => {
+ renderStashPerformerCards();
+ });
+ }
+
if (thStashImgSelectAll) {
thStashImgSelectAll.addEventListener('change', () => {
const checked = thStashImgSelectAll.checked;