您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
tool for crwflags.com
当前为
// ==UserScript== // @name crwflags tool // @license MIT // @namespace http://tampermonkey.net/ // @version 1.1 // @description tool for crwflags.com // @match *://www.crwflags.com/fotw/flags/* // @grant GM_addStyle // @require https://unpkg.com/[email protected]/dist/fuse.min.js // @run-at document-idle // ==/UserScript== (async function () { 'use strict'; /* ========== CONFIG ========== */ const BASE = 'https://www.crwflags.com/fotw/flags/'; const DB_NAME = 'crwflags_tool'; const STORE_NAME = 'flags'; const FETCH_TIMEOUT_MS = 12_000; const KEYWORD_THROTTLE_MS = 90; const FAV_KEY = 'fotw_favs_v6'; const LONG_NAME_MIN_LENGTH = 40; /* ========== UI ========== */ GM_addStyle(` #fotwPanel{ position:fixed; top:48px; right:12px; width:360px; max-height:82vh; overflow-y:auto; background:#003399; padding:14px; border-radius:10px; font-family:Arial,sans-serif; color:#fff; box-shadow:0 0 18px rgba(0,0,0,0.7); z-index:2147483647; } #fotwHeader{ font-size:18px; font-weight:700; margin-bottom:10px; text-align:center; user-select:none; } #fotwSearchInput, #fotwCategoryFilter{ width:100%; padding:8px; font-size:14px; border-radius:6px; border:none; margin-bottom:8px; box-sizing:border-box; } #fotwButtonsRow{ display:grid; grid-template-columns:repeat(2,1fr); gap:6px; justify-items:stretch; margin-bottom:8px; } #fotwButtonsRow button{ background:#0055cc; border:none; border-radius:6px; padding:8px 6px; color:#fff; font-size:13px; cursor:pointer; } #fotwButtonsRow button:hover{ background:#0077ff; } #fotwSearchResults{ background:#fff; color:#000; max-height:260px; overflow-y:auto; border-radius:6px; display:none; font-size:13px; margin-bottom:8px; } #fotwSearchResults div{ padding:8px 10px; border-bottom:1px solid #ddd; cursor:pointer; } #fotwSearchResults div:hover{ background:#eee; } #fotwFlagInfo{ background:#002266; border-radius:8px; padding:10px; font-size:13px; min-height:56px; line-height:1.3em; word-break:break-word; } button[disabled]{ opacity:0.55; cursor:not-allowed; } `); const panel = document.createElement('div'); panel.id = 'fotwPanel'; panel.innerHTML = ` <div id="fotwHeader">⚑ crwflags tool</div> <input id="fotwSearchInput" type="search" placeholder="Search flags..." disabled> <select id="fotwCategoryFilter" disabled> <option value="all">All Categories</option> <option value="country">Countries</option> <option value="historical">Historical</option> <option value="regional">Regional</option> <option value="maritime">Maritime</option> <option value="obscure">Obscure</option> </select> <div id="fotwButtonsRow"> <button id="btnRandom" disabled>🎲 Random</button> <button id="btnRandomLongName" disabled>📝 Long Name</button> <button id="btnRandomHistorical" disabled>🏛 Historical</button> <button id="btnRandomObscure" disabled>🏴 Obscure</button> <button id="btnPrev" disabled>⬅ Prev</button> <button id="btnNext" disabled>Next ➡</button> <button id="btnFavToggle" disabled>⭐ Fav</button> <button id="btnViewFavs" disabled>📂 View Favs</button> <button id="btnRandomFav" disabled>🎯 Random Fav</button> <button id="btnRebuild" disabled>🔄 Rebuild Full DB</button> </div> <div id="fotwStatus" style="font-size:13px;min-height:18px;margin-bottom:6px;">Initializing...</div> <div id="fotwSearchResults"></div> <div id="fotwFlagInfo"></div> `; document.body.appendChild(panel); const statusEl = document.getElementById('fotwStatus'); const searchEl = document.getElementById('fotwSearchInput'); const resultsEl = document.getElementById('fotwSearchResults'); const infoEl = document.getElementById('fotwFlagInfo'); const categoryFilterEl = document.getElementById('fotwCategoryFilter'); const btnRandom = document.getElementById('btnRandom'); const btnRandomLongName = document.getElementById('btnRandomLongName'); const btnRandomHistorical = document.getElementById('btnRandomHistorical'); const btnRandomObscure = document.getElementById('btnRandomObscure'); const btnPrev = document.getElementById('btnPrev'); const btnNext = document.getElementById('btnNext'); const btnFavToggle = document.getElementById('btnFavToggle'); const btnViewFavs = document.getElementById('btnViewFavs'); const btnRandomFav = document.getElementById('btnRandomFav'); const btnRebuild = document.getElementById('btnRebuild'); /* ========== UTIL ========== */ const sleep = ms => new Promise(r => setTimeout(r, ms)); function escapeHtml(s) { return (s||'').replace(/[&<>\"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); } async function fetchWithTimeout(url, timeoutMs = FETCH_TIMEOUT_MS) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(url, { signal: controller.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.text(); } finally { clearTimeout(timeout); } } async function waitForFuse(timeoutMs = 5000) { const start = Date.now(); while (typeof window.Fuse === 'undefined') { if (Date.now() - start > timeoutMs) throw new Error("Fuse.js failed to load"); await sleep(50); } } /* ========== IndexedDB helpers ========== */ function openDB() { return new Promise((resolve, reject) => { const r=indexedDB.open(DB_NAME,1); r.onupgradeneeded=e=>{const d=e.target.result;if(!d.objectStoreNames.contains(STORE_NAME))d.createObjectStore(STORE_NAME,{keyPath:'id'})}; r.onsuccess=()=>resolve(r.result); r.onerror=()=>reject(r.error) }); } function idbGetAll(db) { return new Promise((resolve, reject) => { try{const t=db.transaction(STORE_NAME,'readonly').objectStore(STORE_NAME).getAll();t.onsuccess=()=>resolve(t.result||[]); t.onerror=()=>reject(t.error)}catch(e){reject(e)} }); } function idbPutBatch(db, items) { return new Promise((resolve, reject) => { try{const t=db.transaction(STORE_NAME,'readwrite');const s=t.objectStore(STORE_NAME);items.forEach(it=>s.put(it));t.oncomplete=()=>resolve();t.onerror=()=>reject(t.error)}catch(e){reject(e)} }); } /* ========== STATE ========== */ let fuse = null; let masterList = []; let dbInst = null; let rebuilding = false; let currentList = []; let currentIndex = -1; let currentItem = null; function safeFuseCreate() { try { fuse = new Fuse(masterList, { keys: ['name'], threshold: 0.3 }); } catch (e) { console.warn('Fuse creation failed', e); fuse = null; } } function enableUI() { document.querySelectorAll('#fotwPanel button, #fotwPanel input, #fotwPanel select').forEach(el => el.disabled = false); } function disableUI() { document.querySelectorAll('#fotwPanel button, #fotwPanel input, #fotwPanel select').forEach(el => el.disabled = true); } function setStatus(msg) { statusEl.textContent = msg; } /* ========== CORE ID HELPERS (DO NOT CHANGE) ========== */ function filenameFromId(id) { if (!id) return ''; let seg = id.split('/').pop(); return seg.split('?')[0].split('#')[0].toLowerCase(); } function isTwoLetterCountryId(id) { const fn = filenameFromId(id); return /^[a-z]{2}(?:\.html?)?$/.test(fn); } function isIndexPageId(id) { const fn = filenameFromId(id); return /^keyword[a-z]\.html?$/.test(fn); } /* ========== Rebuild masterList ========== */ async function rebuildMasterList(db) { if (rebuilding) return; rebuilding = true; disableUI(); btnRebuild.disabled = true; setStatus('Rebuilding DB...'); const newMap = new Map(); try { const letters = 'abcdefghijklmnopqrstuvwxyz'; for (const ch of letters) { const path = `${BASE}keyword${ch}.html`; let html = ''; try { html = await fetchWithTimeout(path); } catch (e) { console.warn(`letter ${ch} failed`, e); await sleep(KEYWORD_THROTTLE_MS); continue; } const doc = new DOMParser().parseFromString(html, 'text/html'); for (const a of doc.querySelectorAll('a[href]')) { const href = a.getAttribute('href'); if (!href) continue; const url = new URL(href, path).href; if (url.startsWith(BASE)) { const id = url.slice(BASE.length); const name = (a.textContent || '').trim() || id; if (!newMap.has(id)) newMap.set(id, { id, name, url, category: 'misc' }); } } setStatus(`Rebuild: scanned letter '${ch.toUpperCase()}' (${newMap.size} items)`); await sleep(KEYWORD_THROTTLE_MS); } let allItems = Array.from(newMap.values()); setStatus(`Categorizing ${allItems.length} links...`); await sleep(100); const obscureKeywords = /\b(proposed|fictional|micronation|unofficial|club|football|society|organization|personal|local custom|movement|variant|political|party|separatist|ethnic|company|house flag|aspirant|cultural|sports|association|UFE|unidentified)\b/i; for (const item of allItems) { if (isTwoLetterCountryId(item.id)) { item.category = 'country'; continue; } if (isIndexPageId(item.id)) { continue; } // Keep as misc, will be filtered out const name = item.name.toLowerCase(); if (/\b(historical|history|former|formerly|obsolete)\b/i.test(name)) item.category = 'historical'; else if (/\b(ensign|naval|jack|pennant|burgee|yacht|ship|maritime|merchant)\b/i.test(name)) item.category = 'maritime'; else if (/\b(region|province|municipal|city|county|prefecture|oblast|krai|state flag|provincial)\b/i.test(name)) item.category = 'regional'; else if (obscureKeywords.test(name)) item.category = 'obscure'; else if (item.name.split(/\s+/).length <= 3) item.category = 'regional'; else { // --- THIS IS THE KEY CHANGE --- // If it's none of the above, it's obscure by default. item.category = 'obscure'; } } masterList = allItems.filter(it => it.category !== 'misc'); // Remove index pages from final list await idbPutBatch(db, masterList); safeFuseCreate(); setStatus(`Rebuild complete: ${masterList.length} items.`); } catch (e) { setStatus(`Rebuild failed: ${e.message}`); console.error(e); } finally { rebuilding = false; enableUI(); } } /* ========== Navigation and Selection ========== */ function updateCurrentItemDisplay() { if (!currentItem) { infoEl.innerHTML = 'No item selected.'; btnPrev.disabled = btnNext.disabled = btnFavToggle.disabled = true; return; } infoEl.innerHTML = `<b>${escapeHtml(currentItem.name)}</b> <br><small style="opacity:.7;">[${escapeHtml(currentItem.category)}]</small> <br><a href="${escapeHtml(currentItem.url)}" target="_blank" rel="noopener noreferrer">Open page</a>`; updateFavButton(); btnPrev.disabled = currentIndex <= 0; btnNext.disabled = currentIndex >= currentList.length - 1; btnFavToggle.disabled = false; } function goToItem(item) { if (!item) return; location.href = item.url; } // --- BULLETPROOF FUNCTION --- // This function performs a LIVE check and is guaranteed to work. function pickTrulyRandomObscure() { if (!masterList.length) return null; // 1. Start with the entire list of non-country flags. This is our potential pool. let pool = masterList.filter(it => !isTwoLetterCountryId(it.id)); // 2. Further refine by removing any index pages that might have slipped through. pool = pool.filter(it => !isIndexPageId(it.id)); // 3. For the best experience, also remove major, well-defined categories. pool = pool.filter(it => it.category !== 'country' && it.category !== 'historical' && it.category !== 'maritime'); if (!pool.length) return null; return pool[Math.floor(Math.random() * pool.length)]; } /* ========== Favorites ========== */ let favorites = new Set(JSON.parse(localStorage.getItem(FAV_KEY) || '[]')); function saveFavs() { localStorage.setItem(FAV_KEY, JSON.stringify(Array.from(favorites))); } function toggleFav(id) { if(id){ if(favorites.has(id))favorites.delete(id); else favorites.add(id); saveFavs(); updateFavButton(); } } function updateFavButton() { const curId = currentItem ? currentItem.id : null; btnFavToggle.textContent = favorites.has(curId) ? '⭐ Unfav' : '⭐ Fav'; } /* ========== Events ========== */ searchEl.addEventListener('input', e => { const term = (e.target.value||'').trim(); if (!term) { resultsEl.style.display = 'none'; resultsEl.innerHTML = ''; return; } const cat = categoryFilterEl.value; let results = fuse.search(term).slice(0, 80).map(r => r.item); if (cat && cat !== 'all') results = results.filter(it => it.category === cat); resultsEl.innerHTML = results.map(it => `<div data-url="${escapeHtml(it.url)}">${escapeHtml(it.name)} <span style="opacity:.6;font-size:11px">[${escapeHtml(it.category||'misc')}]</span></div>`).join(''); resultsEl.style.display = 'block'; }); resultsEl.addEventListener('click', e => { const div = e.target.closest('div[data-url]'); if(div) location.href = div.dataset.url; }); categoryFilterEl.addEventListener('change', () => searchEl.dispatchEvent(new Event('input'))); btnRandom.addEventListener('click', () => { const cat = categoryFilterEl.value; let item; if (cat === 'obscure') item = pickTrulyRandomObscure(); else { const pool = cat === 'all' ? masterList : masterList.filter(it => it.category === cat); if (pool.length) item = pool[Math.floor(Math.random() * pool.length)]; } if (item) goToItem(item); else alert(`No items found in category "${cat}".`); }); btnRandomObscure.addEventListener('click', () => { const r = pickTrulyRandomObscure(); if (r) goToItem(r); else alert('No obscure flags found. Please try rebuilding the database.'); }); btnRandomLongName.addEventListener('click', () => { const pool = masterList.filter(it => it.name.length >= LONG_NAME_MIN_LENGTH); if (pool.length) goToItem(pool[Math.floor(Math.random() * pool.length)]); else alert(`No flags found with long names (≥ ${LONG_NAME_MIN_LENGTH} chars).`); }); btnRandomHistorical.addEventListener('click', () => { const pool = masterList.filter(it => it.category === 'historical'); if (pool.length) goToItem(pool[Math.floor(Math.random() * pool.length)]); }); btnPrev.addEventListener('click', () => { if (currentIndex > 0) goToItem(currentList[currentIndex - 1]); }); btnNext.addEventListener('click', () => { if (currentIndex < currentList.length - 1) goToItem(currentList[currentIndex + 1]); }); btnFavToggle.addEventListener('click', () => { if (currentItem) toggleFav(currentItem.id); }); btnViewFavs.addEventListener('click', () => { const favArray = masterList.filter(it => favorites.has(it.id)); if (!favArray.length) { alert('No favorites yet.'); return; } goToItem(favArray[0]); }); btnRandomFav.addEventListener('click', () => { const favArray = masterList.filter(it => favorites.has(it.id)); if (!favArray.length) { alert('No favorites yet.'); return; } goToItem(favArray[Math.floor(Math.random() * favArray.length)]); }); btnRebuild.addEventListener('click', () => { if (rebuilding) return; if (confirm('This will rebuild the flag database with the new, broader "obscure" category.\nThis is recommended and can take a minute.\nContinue?')) { rebuildMasterList(dbInst); } }); /* ========== INITIALIZE ========== */ async function initialize() { setStatus('Loading data...'); disableUI(); try { await waitForFuse(); dbInst = await openDB(); masterList = await idbGetAll(dbInst); if (!masterList.length) { setStatus('No local DB. Rebuilding...'); await rebuildMasterList(dbInst); } setStatus(`Loaded ${masterList.length} flags.`); safeFuseCreate(); enableUI(); const currentPageId = location.href.startsWith(BASE) ? location.href.slice(BASE.length) : null; currentItem = currentPageId ? masterList.find(it => it.id === currentPageId) : null; if (currentItem) { currentList = masterList.filter(it => it.category === currentItem.category && !isIndexPageId(it.id) && !isTwoLetterCountryId(it.id)); currentIndex = currentList.findIndex(it => it.id === currentPageId); } updateCurrentItemDisplay(); } catch (e) { setStatus('Error on init: ' + e.message); console.error(e); btnRebuild.disabled = false; } } initialize(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址