Better search indexing, search filters, and starring.
// ==UserScript==
// @name 111477 Enhancements
// @namespace Craaab4
// @author Craaab4
// @version 1.0
// @description Better search indexing, search filters, and starring.
// @match *://a.111477.xyz/*
// @run-at document-end
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const table = document.getElementById('fileTable');
const searchInput = document.getElementById('search');
if (!table || !searchInput) return;
const tbody = table.tBodies[0];
const parentRow = tbody.querySelector('tr[data-parent]');
const entryRows = () => Array.from(tbody.querySelectorAll('tr[data-entry]'));
// ── storage ──────────────────────────────────────────────────────────────
const STAR_KEY = '111477_starred_v1';
const SORT_KEY = '111477_sort_v1';
const SEARCH_KEY = '111477_search_' + location.pathname;
const FILT_KEY = '111477_filters_' + location.pathname;
function getStars() {
try { return new Set(JSON.parse(localStorage.getItem(STAR_KEY) || '[]')); }
catch { return new Set(); }
}
function saveStars(s) { localStorage.setItem(STAR_KEY, JSON.stringify([...s])); }
let starredSet = getStars();
function saveSortState(col, order) { localStorage.setItem(SORT_KEY, JSON.stringify({ col, order })); }
function getSortState() { try { return JSON.parse(localStorage.getItem(SORT_KEY)); } catch { return null; } }
function saveFilterState() {
localStorage.setItem(FILT_KEY, JSON.stringify({
mode: activeMode,
minWords: minWordsVal,
maxWords: maxWordsVal,
starredOnly: showStarredOnly,
}));
}
function loadFilterState() {
try { return JSON.parse(localStorage.getItem(FILT_KEY)) || {}; } catch { return {}; }
}
// ── utils ─────────────────────────────────────────────────────────────────
function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
function escapeHtml(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
function debounce(fn, ms) {
let t;
return (...a) => {
clearTimeout(t);
t = setTimeout(() => fn(...a), ms);
};
}
function wordCount(name) { return name.trim().split(/[\s\-_.]+/).filter(Boolean).length; }
// ── ranking (used in normal mode only) ───────────────────────────────────
function getRank(lname, tokens, fullQuery) {
if (!fullQuery) return 0;
if (lname === fullQuery) return 0;
if (lname.startsWith(fullQuery)) return 1;
const wb = new RegExp('(^|[^a-z0-9])' + escapeRegex(fullQuery) + '([^a-z0-9]|$)');
if (wb.test(lname)) return 2;
const allWB = tokens.every(t => new RegExp('(^|[^a-z0-9])' + escapeRegex(t) + '([^a-z0-9]|$)').test(lname));
if (allWB) return 3;
const allSub = tokens.every(t => lname.includes(t));
if (allSub) return 4;
return -1;
}
function highlightTokens(text, tokens) {
if (!tokens.length) return escapeHtml(text);
const pat = tokens.map(escapeRegex).join('|');
return escapeHtml(text).replace(new RegExp('(' + pat + ')', 'ig'), '<mark>$1</mark>');
}
function highlightRegex(text, re) {
return escapeHtml(text).replace(re, m => '<mark>' + escapeHtml(m) + '</mark>');
}
// ── inject styles ─────────────────────────────────────────────────────────
document.head.insertAdjacentHTML('beforeend', `<style>
/* layout */
#enh111477-search-wrap { margin-bottom: 6px; }
.enh111477-row { display: flex; align-items: stretch; gap: 8px; margin-bottom: 6px; }
.enh111477-row input#search { margin-bottom: 0; flex: 1; min-width: 0; }
/* filter bar */
#enh111477-filter-bar {
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
padding: 6px 8px; background: #1a1a1a; border: 1px solid #2a2a2a;
border-radius: 4px; margin-bottom: 6px;
}
#enh111477-filter-bar label { color: #888; font-size: 0.78rem; white-space: nowrap; }
.enh111477-mode-btn {
background: #1e1e1e; border: 1px solid #333; color: #aaa;
padding: 4px 10px; border-radius: 4px; cursor: pointer;
font-size: 0.78rem; white-space: nowrap; font-family: monospace;
transition: all .15s;
}
.enh111477-mode-btn:hover { border-color: #4da6ff; color: #4da6ff; }
.enh111477-mode-btn.active { background: #4da6ff22; border-color: #4da6ff; color: #4da6ff; }
.enh111477-sep { color: #333; user-select: none; }
#enh111477-minwords-wrap { display: flex; align-items: center; gap: 5px; color: #888; font-size: 0.78rem; }
#enh111477-minwords, #enh111477-maxwords {
width: 44px; padding: 3px 6px; background: #1e1e1e; border: 1px solid #333;
color: #fff; border-radius: 4px; font-size: 0.78rem; text-align: center; margin: 0;
}
#enh111477-minwords:focus, #enh111477-maxwords:focus { border-color: #4da6ff; outline: none; }
#enh111477-regex-error { color: #ff6b6b; font-size: 0.75rem; margin-left: 4px; display: none; }
/* star button */
.enh111477-star-col { width: 30px; text-align: center; }
.enh111477-star-btn {
background: none; border: none; cursor: pointer; font-size: 1rem;
color: #555; padding: 0 3px; line-height: 1; transition: color .1s;
}
.enh111477-star-btn:hover { color: #ffd24d; }
.enh111477-star-btn.starred { color: #ffd24d; }
/* starred-only button */
.enh111477-staronlybtn {
background: #1e1e1e; border: 1px solid #333; color: #aaa;
padding: 0 12px; border-radius: 4px; cursor: pointer;
font-size: 0.85rem; white-space: nowrap; transition: all .15s;
}
.enh111477-staronlybtn:hover { border-color: #ffd24d; color: #ffd24d; }
.enh111477-staronlybtn.active { background: #ffd24d22; border-color: #ffd24d; color: #ffd24d; }
/* result count + mark */
#enh111477-count { color: #666; font-size: 0.8rem; margin-bottom: 8px; min-height: 1em; }
mark { background: #4da6ff44; color: inherit; border-radius: 2px; padding: 0 1px; }
</style>`);
// ── build UI ──────────────────────────────────────────────────────────────
const wrap = document.createElement('div');
wrap.id = 'enh111477-search-wrap';
searchInput.parentNode.insertBefore(wrap, searchInput);
// row 1: search input + starred-only btn
const row1 = document.createElement('div');
row1.className = 'enh111477-row';
searchInput.removeAttribute('oninput');
row1.appendChild(searchInput);
const starOnlyBtn = document.createElement('button');
starOnlyBtn.type = 'button';
starOnlyBtn.className = 'enh111477-staronlybtn';
starOnlyBtn.textContent = '☆ Starred';
row1.appendChild(starOnlyBtn);
wrap.appendChild(row1);
// row 2: filter bar
const filterBar = document.createElement('div');
filterBar.id = 'enh111477-filter-bar';
filterBar.innerHTML = `
<label>Mode:</label>
<button type="button" class="enh111477-mode-btn active" data-mode="normal" title="Smart ranked search — all query words must appear">Normal</button>
<button type="button" class="enh111477-mode-btn" data-mode="exact" title="Full name must exactly equal the query (case-insensitive)">Exact</button>
<button type="button" class="enh111477-mode-btn" data-mode="regex" title="Treat query as a regular expression, e.g. ^From$">Regex</button>
<span class="enh111477-sep">|</span>
<div id="enh111477-minwords-wrap">
<label for="enh111477-minwords">Min words:</label>
<input id="enh111477-minwords" type="number" min="0" max="99" value="0" title="Only show entries whose name has at least this many words">
<label for="enh111477-maxwords">Max:</label>
<input id="enh111477-maxwords" type="number" min="0" max="99" value="0" placeholder="∞" title="Only show entries whose name has at most this many words (0 = no limit)">
</div>
<span id="enh111477-regex-error">⚠ invalid regex</span>
`;
wrap.appendChild(filterBar);
// count line
const countEl = document.createElement('div');
countEl.id = 'enh111477-count';
wrap.appendChild(countEl);
// ── filter state ──────────────────────────────────────────────────────────
let activeMode = 'normal'; // 'normal' | 'exact' | 'regex'
let minWordsVal = 0;
let maxWordsVal = 0; // 0 = no limit
let showStarredOnly = false;
let compiledRegex = null;
// restore persisted filter state
const savedFilters = loadFilterState();
if (savedFilters.mode) activeMode = savedFilters.mode;
if (savedFilters.minWords) minWordsVal = savedFilters.minWords;
if (savedFilters.maxWords) maxWordsVal = savedFilters.maxWords;
if (savedFilters.starredOnly) showStarredOnly = savedFilters.starredOnly;
// apply restored states to UI
filterBar.querySelectorAll('.enh111477-mode-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === activeMode);
});
document.getElementById('enh111477-minwords').value = minWordsVal;
document.getElementById('enh111477-maxwords').value = maxWordsVal || '';
if (showStarredOnly) {
starOnlyBtn.classList.add('active');
starOnlyBtn.textContent = '★ Starred';
}
// mode buttons
filterBar.querySelectorAll('.enh111477-mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
activeMode = btn.dataset.mode;
filterBar.querySelectorAll('.enh111477-mode-btn').forEach(b => b.classList.toggle('active', b === btn));
saveFilterState();
applyCurrentView();
});
});
// min words
document.getElementById('enh111477-minwords').addEventListener('input', function () {
minWordsVal = Math.max(0, parseInt(this.value) || 0);
saveFilterState();
debouncedApply();
});
// max words
document.getElementById('enh111477-maxwords').addEventListener('input', function () {
maxWordsVal = Math.max(0, parseInt(this.value) || 0);
saveFilterState();
debouncedApply();
});
// starred only
starOnlyBtn.addEventListener('click', () => {
showStarredOnly = !showStarredOnly;
starOnlyBtn.classList.toggle('active', showStarredOnly);
starOnlyBtn.textContent = showStarredOnly ? '★ Starred' : '☆ Starred';
saveFilterState();
applyCurrentView();
});
// ── star column ───────────────────────────────────────────────────────────
const headerRow = table.tHead.rows[0];
const starTh = document.createElement('th');
starTh.textContent = '★';
starTh.style.cssText = 'width:30px;text-align:center;cursor:default;';
headerRow.insertBefore(starTh, headerRow.cells[1]);
if (parentRow) {
parentRow.insertBefore(document.createElement('td'), parentRow.cells[1]);
}
entryRows().forEach(row => {
const url = row.dataset.url;
const td = document.createElement('td');
td.className = 'enh111477-star-col';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'enh111477-star-btn' + (starredSet.has(url) ? ' starred' : '');
btn.textContent = starredSet.has(url) ? '★' : '☆';
btn.title = 'Star / unstar';
btn.addEventListener('click', e => {
e.preventDefault(); e.stopPropagation();
if (starredSet.has(url)) {
starredSet.delete(url);
btn.textContent = '☆'; btn.classList.remove('starred');
} else {
starredSet.add(url);
btn.textContent = '★'; btn.classList.add('starred');
}
saveStars(starredSet);
if (showStarredOnly) applyCurrentView();
});
td.appendChild(btn);
row.insertBefore(td, row.cells[1]);
});
function updateSelectedCount() {
if (typeof window.updateSelectedCount === 'function') {
window.updateSelectedCount();
}
}
let naturalOrder = entryRows();
function updateNaturalOrder() { naturalOrder = entryRows(); }
function restoreNaturalOrder() {
naturalOrder.forEach(r => tbody.appendChild(r));
if (parentRow) tbody.insertBefore(parentRow, tbody.firstChild);
}
// ── wrap sortTable ────────────────────────────────────────────────────────
const origSort = window.sortTable;
window.sortTable = function (n, isNumeric) {
origSort(n, isNumeric);
updateNaturalOrder();
saveSortState(n, table.dataset.sortOrder || 'asc');
applyCurrentView();
};
// ── main render ───────────────────────────────────────────────────────────
function applyCurrentView() {
const raw = searchInput.value.trim();
const lower = raw.toLowerCase();
const tokens = lower.split(/\s+/).filter(Boolean);
const hasQuery = raw.length > 0;
const regexErr = document.getElementById('enh111477-regex-error');
// compile regex if needed
compiledRegex = null;
regexErr.style.display = 'none';
if (activeMode === 'regex' && hasQuery) {
try {
compiledRegex = new RegExp(raw, 'i');
} catch {
regexErr.style.display = '';
// show all (or starred-only), don't crash
}
}
const filterActive = hasQuery || showStarredOnly || minWordsVal > 0 || maxWordsVal > 0;
const copyBar = document.getElementById('copyAllBar');
if (filterActive) {
table.classList.add('searching');
if (copyBar) copyBar.classList.add('show');
} else {
table.classList.remove('searching');
if (copyBar) copyBar.classList.remove('show');
document.querySelectorAll('#fileTable tbody input[type=checkbox]').forEach(cb => cb.checked = false);
const selAll = document.getElementById('selectAllCheckbox');
if (selAll) selAll.checked = false;
}
let visibleCount = 0;
const scored = [];
entryRows().forEach(row => {
const rawName = row.dataset.name || '';
const lname = rawName.toLowerCase();
const isStarred = starredSet.has(row.dataset.url);
let matches = true;
let rank = 0;
// word count gate
const wc = wordCount(rawName);
if (minWordsVal > 0 && wc < minWordsVal) matches = false;
if (maxWordsVal > 0 && wc > maxWordsVal) matches = false;
// starred gate
if (showStarredOnly && !isStarred) matches = false;
// search gate
if (matches && hasQuery) {
if (activeMode === 'exact') {
matches = lname === lower;
rank = 0;
} else if (activeMode === 'regex') {
matches = compiledRegex ? compiledRegex.test(rawName) : true;
rank = 0;
} else {
// normal ranked
rank = getRank(lname, tokens, lower);
matches = rank >= 0;
}
}
row.style.display = matches ? '' : 'none';
if (matches) visibleCount++;
// highlighting
const anchor = row.querySelector('a[href]');
if (anchor) {
if (!anchor.dataset.orig) anchor.dataset.orig = anchor.textContent;
if (matches && hasQuery && activeMode !== 'exact') {
if (activeMode === 'regex' && compiledRegex) {
anchor.innerHTML = highlightRegex(anchor.dataset.orig, new RegExp(raw, 'ig'));
} else if (activeMode === 'normal') {
anchor.innerHTML = highlightTokens(anchor.dataset.orig, tokens);
} else {
anchor.textContent = anchor.dataset.orig;
}
} else {
anchor.textContent = anchor.dataset.orig;
}
}
if (activeMode === 'normal' && hasQuery) scored.push({ row, rank, name: lname });
});
// re-sort rows by rank in normal mode, otherwise keep natural order
if (activeMode === 'normal' && hasQuery) {
scored.sort((a, b) => a.rank - b.rank || a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
scored.forEach(s => tbody.appendChild(s.row));
if (parentRow) tbody.insertBefore(parentRow, tbody.firstChild);
} else {
restoreNaturalOrder();
}
updateSelectedCount();
// count display
if (filterActive) {
let parts = [];
if (hasQuery) parts.push(`query: "${raw}"`);
if (activeMode !== 'normal') parts.push(activeMode + ' mode');
if (minWordsVal > 0 && maxWordsVal > 0) parts.push(`${minWordsVal}–${maxWordsVal} words`);
else if (minWordsVal > 0) parts.push(`≥${minWordsVal} words`);
else if (maxWordsVal > 0) parts.push(`≤${maxWordsVal} words`);
if (showStarredOnly) parts.push('starred only');
countEl.textContent = `${visibleCount} result${visibleCount === 1 ? '' : 's'} · ${parts.join(' · ')}`;
} else {
countEl.textContent = '';
}
}
window.filterTable = applyCurrentView;
const debouncedApply = debounce(applyCurrentView, 60);
searchInput.addEventListener('input', debouncedApply);
// ── restore state on load ─────────────────────────────────────────────────
localStorage.removeItem(SEARCH_KEY); // don't restore search on reload
const savedSort = getSortState();
if (savedSort && Number.isInteger(savedSort.col)) {
const isNumeric = savedSort.col === 2;
origSort(savedSort.col, isNumeric);
if (savedSort.order === 'desc') origSort(savedSort.col, isNumeric);
updateNaturalOrder();
} else {
updateNaturalOrder();
}
applyCurrentView();
})();