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;