feat: Add dedicated NOT (Exclude) filter bars and inline negative keyword (-word / !word) search support across all tabs
This commit is contained in:
@@ -377,6 +377,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
@@ -386,7 +387,30 @@
|
|||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
padding: 0.4rem 0.75rem;
|
padding: 0.4rem 0.75rem;
|
||||||
font-size: 0.8rem;
|
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 {
|
.torrent-table {
|
||||||
@@ -1349,7 +1373,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-filter">
|
<div class="search-filter">
|
||||||
<input type="text" class="search-input" id="search-input" placeholder="Filter torrents by name, performer..." />
|
<input type="text" class="search-input" id="search-input" placeholder="🔍 Filter torrents by name, performer..." />
|
||||||
|
<input type="text" class="search-input not-filter-input" id="not-filter-input" placeholder="⛔ Exclude / NOT (e.g. 720p, VR)..." title="Exclude torrents matching these comma/space-separated terms" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1455,7 +1480,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-filter">
|
<div class="search-filter">
|
||||||
<input type="text" class="search-input" id="smb-search-input" placeholder="Filter SMB files by name, performer..." />
|
<input type="text" class="search-input" id="smb-search-input" placeholder="🔍 Filter SMB files by name, performer..." />
|
||||||
|
<input type="text" class="search-input not-filter-input" id="smb-not-filter-input" placeholder="⛔ Exclude / NOT (e.g. 720p, VR)..." title="Exclude SMB files matching these comma/space-separated terms" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1567,7 +1593,8 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="search-filter" style="margin-left: auto;">
|
<div class="search-filter" style="margin-left: auto;">
|
||||||
<input type="text" class="search-input" id="stash-images-search-input" placeholder="Filter performers by name..." />
|
<input type="text" class="search-input" id="stash-images-search-input" placeholder="🔍 Filter performers by name..." />
|
||||||
|
<input type="text" class="search-input not-filter-input" id="stash-images-not-filter-input" placeholder="⛔ Exclude / NOT..." title="Exclude performers matching these terms" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1708,6 +1735,34 @@
|
|||||||
.replace(/'/g, ''');
|
.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 ====================
|
// ==================== Performer Image Hover Card Controller ====================
|
||||||
const hoverCard = document.getElementById('performer-hover-card');
|
const hoverCard = document.getElementById('performer-hover-card');
|
||||||
const hoverCardImg = document.getElementById('hover-card-img');
|
const hoverCardImg = document.getElementById('hover-card-img');
|
||||||
@@ -1941,6 +1996,7 @@
|
|||||||
const selectAllCheckbox = document.getElementById('th-select-all');
|
const selectAllCheckbox = document.getElementById('th-select-all');
|
||||||
const selectionCountLabel = document.getElementById('table-selection-count');
|
const selectionCountLabel = document.getElementById('table-selection-count');
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
|
const notFilterInput = document.getElementById('not-filter-input');
|
||||||
const btnFetch = document.getElementById('btn-fetch-torrents');
|
const btnFetch = document.getElementById('btn-fetch-torrents');
|
||||||
const btnAnalyzeSelected = document.getElementById('btn-ai-analyze-selected');
|
const btnAnalyzeSelected = document.getElementById('btn-ai-analyze-selected');
|
||||||
const btnAnalyzeAll = document.getElementById('btn-ai-analyze-all');
|
const btnAnalyzeAll = document.getElementById('btn-ai-analyze-all');
|
||||||
@@ -1959,6 +2015,7 @@
|
|||||||
const smbSelectAllCheckbox = document.getElementById('th-smb-select-all');
|
const smbSelectAllCheckbox = document.getElementById('th-smb-select-all');
|
||||||
const smbSelectionCountLabel = document.getElementById('smb-table-selection-count');
|
const smbSelectionCountLabel = document.getElementById('smb-table-selection-count');
|
||||||
const smbSearchInput = document.getElementById('smb-search-input');
|
const smbSearchInput = document.getElementById('smb-search-input');
|
||||||
|
const smbNotFilterInput = document.getElementById('smb-not-filter-input');
|
||||||
const btnFetchSmb = document.getElementById('btn-fetch-smb');
|
const btnFetchSmb = document.getElementById('btn-fetch-smb');
|
||||||
const btnSmbAnalyzeSelected = document.getElementById('btn-smb-analyze-selected');
|
const btnSmbAnalyzeSelected = document.getElementById('btn-smb-analyze-selected');
|
||||||
const btnSmbAnalyzeAll = document.getElementById('btn-smb-analyze-all');
|
const btnSmbAnalyzeAll = document.getElementById('btn-smb-analyze-all');
|
||||||
@@ -3483,12 +3540,11 @@ Return ONLY valid JSON matching:
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTable() {
|
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 => {
|
const filtered = qbState.torrents.filter(t => {
|
||||||
if (!query) return true;
|
const fullText = `${t.name} ${t.selectedPerformer || ''} ${t.save_path || ''} ${t.category || ''}`;
|
||||||
return t.name.toLowerCase().includes(query) ||
|
return matchFilterCriteria(fullText, query, notQuery);
|
||||||
(t.selectedPerformer && t.selectedPerformer.toLowerCase().includes(query)) ||
|
|
||||||
(t.save_path && t.save_path.toLowerCase().includes(query));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
@@ -3780,10 +3836,11 @@ Return ONLY valid JSON matching:
|
|||||||
|
|
||||||
selectAllCheckbox.addEventListener('change', (e) => {
|
selectAllCheckbox.addEventListener('change', (e) => {
|
||||||
const isChecked = e.target.checked;
|
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 => {
|
qbState.torrents.forEach(t => {
|
||||||
const matches = !query || t.name.toLowerCase().includes(query) || (t.selectedPerformer && t.selectedPerformer.toLowerCase().includes(query));
|
const fullText = `${t.name} ${t.selectedPerformer || ''} ${t.save_path || ''} ${t.category || ''}`;
|
||||||
if (matches) {
|
if (matchFilterCriteria(fullText, query, notQuery)) {
|
||||||
if (isChecked) qbState.selectedHashes.add(t.hash);
|
if (isChecked) qbState.selectedHashes.add(t.hash);
|
||||||
else qbState.selectedHashes.delete(t.hash);
|
else qbState.selectedHashes.delete(t.hash);
|
||||||
}
|
}
|
||||||
@@ -3793,6 +3850,7 @@ Return ONLY valid JSON matching:
|
|||||||
});
|
});
|
||||||
|
|
||||||
searchInput.addEventListener('input', () => renderTable());
|
searchInput.addEventListener('input', () => renderTable());
|
||||||
|
if (notFilterInput) notFilterInput.addEventListener('input', () => renderTable());
|
||||||
|
|
||||||
// ==================== TAB 2: SMB Video Downloader Engine ====================
|
// ==================== TAB 2: SMB Video Downloader Engine ====================
|
||||||
async function scanSmbFiles() {
|
async function scanSmbFiles() {
|
||||||
@@ -4355,11 +4413,11 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSmbTable() {
|
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 => {
|
const filtered = smbState.files.filter(f => {
|
||||||
if (!query) return true;
|
const fullText = `${f.name} ${f.selectedPerformer || ''} ${f.path || ''}`;
|
||||||
return f.name.toLowerCase().includes(query) ||
|
return matchFilterCriteria(fullText, query, notQuery);
|
||||||
(f.selectedPerformer && f.selectedPerformer.toLowerCase().includes(query));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
@@ -4588,10 +4646,11 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
|
|
||||||
smbSelectAllCheckbox.addEventListener('change', (e) => {
|
smbSelectAllCheckbox.addEventListener('change', (e) => {
|
||||||
const isChecked = e.target.checked;
|
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 => {
|
smbState.files.forEach(f => {
|
||||||
const matches = !query || f.name.toLowerCase().includes(query) || (f.selectedPerformer && f.selectedPerformer.toLowerCase().includes(query));
|
const fullText = `${f.name} ${f.selectedPerformer || ''} ${f.path || ''}`;
|
||||||
if (matches) {
|
if (matchFilterCriteria(fullText, query, notQuery)) {
|
||||||
if (isChecked) smbState.selectedPaths.add(f.path);
|
if (isChecked) smbState.selectedPaths.add(f.path);
|
||||||
else smbState.selectedPaths.delete(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());
|
smbSearchInput.addEventListener('input', () => renderSmbTable());
|
||||||
|
if (smbNotFilterInput) smbNotFilterInput.addEventListener('input', () => renderSmbTable());
|
||||||
|
|
||||||
// Modal Close
|
// Modal Close
|
||||||
document.getElementById('modal-close').addEventListener('click', () => {
|
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 toggleStashFullName = document.getElementById('toggle-stash-full-name');
|
||||||
const toggleStashHasScenes = document.getElementById('toggle-stash-has-scenes');
|
const toggleStashHasScenes = document.getElementById('toggle-stash-has-scenes');
|
||||||
const stashImagesSearchInput = document.getElementById('stash-images-search-input');
|
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 thStashImgSelectAll = document.getElementById('th-stash-img-select-all');
|
||||||
const stashImgSelectionCount = document.getElementById('stash-img-selection-count');
|
const stashImgSelectionCount = document.getElementById('stash-img-selection-count');
|
||||||
const stashPerformerCardsContainer = document.getElementById('stash-performer-cards-container');
|
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() {
|
function renderStashPerformerCards() {
|
||||||
if (!stashPerformerCardsContainer) return;
|
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 => {
|
const filtered = stashImgState.performers.filter(p => {
|
||||||
if (!query) return true;
|
const fullText = `${p.name || ''} ${p.disambiguation || ''} ${p.country || ''} ${(p.alias_list || []).join(' ')}`;
|
||||||
return (p.name || '').toLowerCase().includes(query);
|
return matchFilterCriteria(fullText, query, notQuery);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
@@ -5475,7 +5537,7 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
<div class="empty-state" style="grid-column: 1 / -1;">
|
<div class="empty-state" style="grid-column: 1 / -1;">
|
||||||
<div class="empty-state-icon">🔍</div>
|
<div class="empty-state-icon">🔍</div>
|
||||||
<h3>No matching performers found</h3>
|
<h3>No matching performers found</h3>
|
||||||
<p>${query ? 'Try adjusting your search filter' : 'Click "Scan Stash Missing Images" to load performers'}</p>
|
<p>${query || notQuery ? 'Try adjusting your search or exclude filters' : 'Click "Scan Stash Missing Images" to load performers'}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
updateStashImagesStats();
|
updateStashImagesStats();
|
||||||
@@ -5529,6 +5591,12 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stashImagesNotFilterInput) {
|
||||||
|
stashImagesNotFilterInput.addEventListener('input', () => {
|
||||||
|
renderStashPerformerCards();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (thStashImgSelectAll) {
|
if (thStashImgSelectAll) {
|
||||||
thStashImgSelectAll.addEventListener('change', () => {
|
thStashImgSelectAll.addEventListener('change', () => {
|
||||||
const checked = thStashImgSelectAll.checked;
|
const checked = thStashImgSelectAll.checked;
|
||||||
|
|||||||
Reference in New Issue
Block a user