diff options
Diffstat (limited to 'main.js')
| -rw-r--r-- | main.js | 972 |
1 files changed, 972 insertions, 0 deletions
@@ -0,0 +1,972 @@ +// ═══════════════════════════════════════════════════════════ +// SOURCES +// ═══════════════════════════════════════════════════════════ +const SOURCES = { + dictionary: { + bang: "!d", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(q)}`, + ); + if (!r.ok) return []; + const d = await r.json(); + return d.slice(0, 3).flatMap((e) => + e.meanings.slice(0, 2).map((m) => ({ + meta: { phonetic: e.phonetic }, + snippet: m.definitions[0]?.definition || "", + source: "dictionary", + title: `${e.word} [${m.partOfSpeech}]`, + url: e.sourceUrls?.[0] || "#", + })), + ); + }, + name: "DICTIONARY", + }, + github: { + bang: "!g", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=8&sort=stars`, + ); + const d = await r.json(); + return (d.items || []).map((i) => ({ + meta: { forks: i.forks_count, lang: i.language, stars: i.stargazers_count }, + snippet: i.description || "NO DESCRIPTION", + source: "github", + title: i.full_name, + url: i.html_url, + })); + }, + name: "GITHUB", + }, + hackernews: { + bang: "!hn", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(q)}&hitsPerPage=8`, + ); + const d = await r.json(); + return (d.hits || []) + .filter((h) => h.title) + .map((h) => ({ + meta: { comments: h.num_comments, points: h.points }, + snippet: `${h.points || 0} POINTS // ${h.author}`, + source: "hackernews", + title: h.title, + url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, + })); + }, + name: "HACKERNEWS", + }, + news: { + bang: "!n", + count: 0, + enabled: true, + fetch: async (q) => { + // Combine HN, Reddit News, and Wiki Current Events logic + const p1 = fetch( + `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(q)}&tags=story&hitsPerPage=6`, + ) + .then((r) => r.json()) + .catch(() => ({ hits: [] })); + const p2 = fetch( + `https://www.reddit.com/r/worldnews+news+technology/search.json?q=${encodeURIComponent(q)}&restrict_sr=1&sort=new&limit=6`, + ) + .then((r) => r.json()) + .catch(() => ({ data: { children: [] } })); + + const [hn, rd] = await Promise.all([p1, p2]); + + const hnRes = (hn.hits || []).map((h) => ({ + meta: { date: h.created_at_i, lang: "HN" }, + snippet: `HACKERNEWS // ${h.points || 0} PTS // ${timeAgo(h.created_at_i * 1000)}`, + source: "news", + title: h.title, + url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, + })); + + const rdRes = (rd.data?.children || []).map((c) => ({ + meta: { date: c.data.created_utc, lang: "RD" }, + snippet: `REDDIT r/${c.data.subreddit.toUpperCase()} // ${c.data.score} UPVOTES`, + source: "news", + title: c.data.title, + url: `https://reddit.com${c.data.permalink}`, + })); + + // Interleave/Sort by recency + return [...hnRes, ...rdRes].sort((a, b) => b.meta.date - a.meta.date); + }, + name: "AGGREGATE NEWS", + }, + npm: { + bang: "!npm", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=8`, + ); + const d = await r.json(); + return (d.objects || []).map((o) => ({ + meta: { version: o.package.version }, + snippet: o.package.description || "NO DESCRIPTION", + source: "npm", + title: o.package.name, + url: `https://npmjs.com/package/${o.package.name}`, + })); + }, + name: "NPM", + }, + openlibrary: { + bang: "!b", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://openlibrary.org/search.json?q=${encodeURIComponent(q)}&limit=6`, + ); + const d = await r.json(); + return (d.docs || []).map((b) => ({ + meta: { year: b.first_publish_year }, + snippet: `BY ${(b.author_name || ["UNKNOWN"]).join(", ").toUpperCase()}`, + source: "openlibrary", + title: b.title, + url: `https://openlibrary.org${b.key}`, + })); + }, + name: "OPENLIBRARY", + }, + reddit: { + bang: "!r", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&limit=8`, + ); + const d = await r.json(); + return (d.data?.children || []).map((c) => ({ + meta: { comments: c.data.num_comments, score: c.data.score }, + snippet: c.data.selftext?.substring(0, 200) || `r/${c.data.subreddit}`, + source: "reddit", + title: c.data.title, + url: `https://reddit.com${c.data.permalink}`, + })); + }, + name: "REDDIT", + }, + stackoverflow: { + bang: "!so", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&intitle=${encodeURIComponent(q)}&site=stackoverflow&pagesize=8`, + ); + const d = await r.json(); + return (d.items || []).map((i) => ({ + meta: { answers: i.answer_count, score: i.score }, + snippet: `${i.answer_count} ANSWERS // ${i.view_count} VIEWS`, + source: "stackoverflow", + title: decodeHTML(i.title), + url: i.link, + })); + }, + name: "STACKOVERFLOW", + }, + wikipedia: { + bang: "!w", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=8`, + ); + const d = await r.json(); + return (d.query?.search || []).map((i) => ({ + meta: { words: i.wordcount }, + snippet: i.snippet.replace(/<[^>]*>/g, ""), + source: "wikipedia", + title: i.title, + url: `https://en.wikipedia.org/wiki/${encodeURIComponent(i.title)}`, + })); + }, + instant: async (q) => { + const s = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=1`, + ); + const sd = await s.json(); + if (!sd.query?.search?.length) return null; + const title = sd.query.search[0].title; + const r = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=${encodeURIComponent(title)}&format=json&origin=*`, + ); + const d = await r.json(); + const page = Object.values(d.query.pages)[0]; + return { + content: page.extract, + source: "WIKIPEDIA", + title: page.title, + url: `https://en.wikipedia.org/wiki/${encodeURIComponent(title)}`, + }; + }, + name: "WIKIPEDIA", + }, +}; + +const STOCK_EXCHANGES = { + AAPL: "NASDAQ", + ADA: "CRYPTO", + AMD: "NASDAQ", + AMZN: "NASDAQ", + BAC: "NYSE", + BTC: "CRYPTO", + DIS: "NYSE", + DOGE: "CRYPTO", + ETH: "CRYPTO", + EURUSD: "FX", + GBPUSD: "FX", + GOOG: "NASDAQ", + GOOGL: "NASDAQ", + INTC: "NASDAQ", + JNJ: "NYSE", + JPM: "NYSE", + KO: "NYSE", + META: "NASDAQ", + MSFT: "NASDAQ", + NFLX: "NASDAQ", + NVDA: "NASDAQ", + PYPL: "NASDAQ", + QQQ: "NASDAQ", + SOL: "CRYPTO", + SPY: "AMEX", + TSLA: "NASDAQ", + USDJPY: "FX", + V: "NYSE", + WMT: "NYSE", + XOM: "NYSE", + XRP: "CRYPTO", +}; + +function getSymbol(ticker) { + const t = ticker.toUpperCase(); + const ex = STOCK_EXCHANGES[t]; + if (ex === "CRYPTO") return `BINANCE:${t}USDT`; + if (ex === "FX") return `FX:${t}`; + if (ex) return `${ex}:${t}`; + return `NASDAQ:${t}`; +} + +// ═══════════════════════════════════════════════════════════ +// STATE +// ═══════════════════════════════════════════════════════════ +const state = { + calcExpr: "", + calcResult: "0", + hasSearched: false, + history: JSON.parse(localStorage.getItem("sh") || "[]"), + query: "", + results: [], + watchlist: JSON.parse(localStorage.getItem("sw") || '["TSLA", "AAPL", "BTC", "ETH"]'), +}; + +const $ = (id) => document.getElementById(id); +const searchInput = $("search-input"); +const resultsEl = $("results"); +const heroEl = $("hero"); +const mainEl = $("main-content"); +const statsBar = $("stats-bar"); + +// ═══════════════════════════════════════════════════════════ +// INIT +// ═══════════════════════════════════════════════════════════ +function init() { + renderSources(); + renderHistory(); + renderWatchlist(); + initTheme(); + updateClocks(); + setInterval(updateClocks, 1000); + setupCalc(); + setupListeners(); + setupWatchlistListeners(); +} + +function renderSources() { + $("sources-list").innerHTML = Object.entries(SOURCES) + .map( + ([k, s]) => ` + <div class="source-item ${s.enabled ? "active" : ""}" data-source="${k}" onclick="toggleSource('${k}')"> + ${s.name} <span class="source-count" id="c-${k}">${s.count}</span> + </div> + `, + ) + .join(""); +} + +function toggleSource(k) { + SOURCES[k].enabled = !SOURCES[k].enabled; + renderSources(); + if (state.query) search(state.query); +} + +function toggleWidget(id) { + $(id).classList.toggle("open"); +} + +// ═══════════════════════════════════════════════════════════ +// THEME +// ═══════════════════════════════════════════════════════════ +function initTheme() { + const t = localStorage.getItem("theme"); + if (t === "light") document.body.classList.add("light-mode"); +} + +function toggleTheme() { + document.body.classList.toggle("light-mode"); + const isLight = document.body.classList.contains("light-mode"); + localStorage.setItem("theme", isLight ? "light" : "dark"); +} + +// ═══════════════════════════════════════════════════════════ +// WATCHLIST +// ═══════════════════════════════════════════════════════════ +function renderWatchlist() { + const c = $("watchlist-container"); + if (!state.watchlist.length) { + c.innerHTML = '<div style="color:var(--gray);font-size:11px;padding:10px;">EMPTY</div>'; + return; + } + c.innerHTML = state.watchlist + .map((t) => { + const ex = STOCK_EXCHANGES[t] || "STOCK"; + return ` + <div class="watchlist-item-row" onclick="searchFor('$${t}')"> + <span>${t} <span style="color:var(--gray)">${ex}</span></span> + <span class="watchlist-del-btn" onclick="removeTicker(event, '${t}')">×</span> + </div>`; + }) + .join(""); +} + +function addTicker() { + const val = $("watchlist-input").value.toUpperCase().trim(); + if (val && !state.watchlist.includes(val)) { + state.watchlist.push(val); + localStorage.setItem("sw", JSON.stringify(state.watchlist)); + renderWatchlist(); + $("watchlist-input").value = ""; + } +} + +window.removeTicker = (e, t) => { + e.stopPropagation(); // Prevent search trigger + state.watchlist = state.watchlist.filter((i) => i !== t); + localStorage.setItem("sw", JSON.stringify(state.watchlist)); + renderWatchlist(); +}; + +function setupWatchlistListeners() { + $("watchlist-add-btn").onclick = addTicker; + $("watchlist-input").addEventListener("keydown", (e) => { + if (e.key === "Enter") addTicker(); + }); +} + +// ═══════════════════════════════════════════════════════════ +// SEARCH +// ═══════════════════════════════════════════════════════════ +async function search(query) { + if (!query.trim()) return goHome(); + + // Show results view + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + + // Stock ticker + const stockMatch = query.match(/^\$([A-Za-z]{1,5})$/); + if (stockMatch) { + showStockWidget(stockMatch[1].toUpperCase()); + addHistory(query); + return; + } + + // Bangs + const bangMatch = query.match(/^!(\w+)\s*(.*)/); + if (bangMatch) { + const bang = "!" + bangMatch[1]; + const q = bangMatch[2]; + const src = Object.entries(SOURCES).find(([_, s]) => s.bang === bang); + if (src && q) { + Object.keys(SOURCES).forEach((k) => (SOURCES[k].enabled = k === src[0])); + renderSources(); + query = q; + } + } + + // Calc + if (query.startsWith("=")) { + showCalcResult(query.slice(1)); + return; + } + + state.query = query; + showLoading(); + addHistory(query); + + const start = performance.now(); + const enabled = Object.entries(SOURCES).filter(([_, s]) => s.enabled); + + fetchInstant(query); + + const promises = enabled.map(async ([k, s]) => { + try { + const r = await s.fetch(query); + SOURCES[k].count = r.length; + $(`c-${k}`).textContent = r.length; + return r; + } catch (e) { + return []; + } + }); + + const all = await Promise.all(promises); + state.results = all.flat(); + const elapsed = Math.round(performance.now() - start); + + $("stat-results").textContent = state.results.length; + $("stat-sources").textContent = enabled.length; + $("stat-time").textContent = elapsed + "MS"; + $("results-label").textContent = `${state.results.length} RESULTS`; + $("results-info").textContent = `${elapsed}MS // ${enabled.length} SOURCES`; + + renderResults(); +} + +function goHome() { + state.hasSearched = false; + heroEl.classList.remove("compact"); + mainEl.style.display = "none"; + statsBar.classList.remove("visible"); + searchInput.value = ""; +} + +// ═══════════════════════════════════════════════════════════ +// STOCK WIDGET +// ═══════════════════════════════════════════════════════════ +function showStockWidget(ticker) { + const symbol = getSymbol(ticker); + const widgetId = "tv_" + Math.random().toString(36).substr(2, 9); + const isLight = document.body.classList.contains("light-mode"); + + resultsEl.innerHTML = ` + <div class="stock-widget"> + <div class="stock-header"> + <div class="stock-info"> + <h2>${ticker}</h2> + <div class="ticker">${symbol}</div> + </div> + <div class="stock-actions-top"> + <button class="stock-btn" onclick="window.open('https://www.tradingview.com/symbols/${symbol}/','_blank')">TRADINGVIEW ↗</button> + <button class="stock-btn" onclick="copy('${symbol}')">COPY</button> + </div> + </div> + <div class="tradingview-widget-container" id="${widgetId}"></div> + </div> + `; + + $("results-label").textContent = `STOCK: ${ticker}`; + $("results-info").textContent = symbol; + + loadTradingView(widgetId, symbol, isLight); +} + +function loadTradingView(containerId, symbol, isLight) { + if (!window.TradingView) { + const s = document.createElement("script"); + s.src = "https://s3.tradingview.com/tv.js"; + s.onload = () => createChart(containerId, symbol, isLight); + document.head.appendChild(s); + } else { + createChart(containerId, symbol, isLight); + } +} + +function createChart(containerId, symbol, isLight) { + new TradingView.widget({ + allow_symbol_change: true, + backgroundColor: isLight ? "rgba(244, 244, 245, 1)" : "rgba(0,0,0,1)", + container_id: containerId, + enable_publishing: false, + gridColor: isLight ? "rgba(220, 220, 220, 1)" : "rgba(30,30,30,1)", + height: 500, + interval: "D", + locale: "en", + studies: ["Volume@tv-basicstudies"], + style: "1", + symbol: symbol, + theme: isLight ? "light" : "dark", + timezone: "Etc/UTC", + toolbar_bg: isLight ? "#f4f4f5" : "#000", + width: "100%", + }); +} + +async function fetchInstant(query) { + if (/^[a-zA-Z]+$/.test(query) && SOURCES.dictionary.enabled) { + try { + const r = await fetch( + `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(query)}`, + ); + if (r.ok) { + const d = await r.json(); + if (d[0]) { + const meanings = d[0].meanings + .slice(0, 3) + .map((m) => `[${m.partOfSpeech.toUpperCase()}] ${m.definitions[0]?.definition}`) + .join("\n\n"); + showInstant({ + content: meanings, + source: "DICTIONARY", + title: d[0].word.toUpperCase() + (d[0].phonetic ? ` ${d[0].phonetic}` : ""), + }); + return; + } + } + } catch {} + } + if (SOURCES.wikipedia.enabled) { + const w = await SOURCES.wikipedia.instant?.(query); + if (w) showInstant(w); + } +} + +function showInstant(data) { + const el = document.createElement("div"); + el.className = "instant-box"; + el.innerHTML = ` + <div class="instant-label">INSTANT ANSWER</div> + <div class="instant-title">${esc(data.title)}</div> + <div class="instant-content">${esc(data.content) + .split("\n") + .map((p) => `<p>${p}</p>`) + .join("")}</div> + <div class="instant-meta"> + <span>SOURCE: ${esc(data.source)}</span> + ${data.url ? `<a href="${data.url}" target="_blank">VIEW FULL →</a>` : ""} + </div> + `; + resultsEl.innerHTML = ""; + resultsEl.appendChild(el); +} + +function renderResults() { + if (state.results.length === 0) { + resultsEl.innerHTML = `<div class="empty-state"><div class="empty-title">NO RESULTS</div><div class="empty-text">TRY DIFFERENT KEYWORDS OR ENABLE MORE SOURCES</div></div>`; + return; + } + + const instant = resultsEl.querySelector(".instant-box"); + resultsEl.innerHTML = ""; + if (instant) resultsEl.appendChild(instant); + + state.results.forEach((r, i) => { + const el = document.createElement("div"); + el.className = "result"; + el.innerHTML = ` + <div class="result-index">${String(i + 1).padStart(2, "0")}</div> + <div class="result-body"> + <div class="result-source">${r.source.toUpperCase()}</div> + <div class="result-title" onclick="window.open('${esc(r.url)}','_blank')">${esc(r.title)}</div> + <div class="result-url">${esc(r.url)}</div> + <div class="result-snippet">${highlight(esc(r.snippet), state.query)}</div> + ${ + r.meta + ? `<div class="result-meta"> + ${r.meta.stars !== undefined ? `<span>★ ${fmt(r.meta.stars)}</span>` : ""} + ${r.meta.forks !== undefined ? `<span>⑂ ${fmt(r.meta.forks)}</span>` : ""} + ${r.meta.score !== undefined ? `<span>↑ ${fmt(r.meta.score)}</span>` : ""} + ${r.meta.comments !== undefined ? `<span>◨ ${fmt(r.meta.comments)}</span>` : ""} + ${r.meta.words !== undefined ? `<span>◎ ${fmt(r.meta.words)}W</span>` : ""} + ${r.meta.lang ? `<span>${r.meta.lang.toUpperCase()}</span>` : ""} + ${r.meta.version ? `<span>V${r.meta.version}</span>` : ""} + </div>` + : "" + } + </div> + `; + resultsEl.appendChild(el); + }); +} + +function showLoading() { + resultsEl.innerHTML = Array(4) + .fill( + `<div class="loading-row"><span class="loading-text">FETCHING</span><div class="loading-bar"></div></div>`, + ) + .join(""); +} + +function showCalcResult(expr) { + try { + const result = Function('"use strict";return (' + expr.replace(/\^/g, "**") + ")")(); + resultsEl.innerHTML = `<div class="instant-box"><div class="instant-label">CALCULATOR</div><div class="instant-title" style="font-variant-numeric:tabular-nums">${esc(expr)} = ${result}</div></div>`; + } catch { + toast("INVALID EXPRESSION"); + } +} + +// ═══════════════════════════════════════════════════════════ +// CALCULATOR +// ═══════════════════════════════════════════════════════════ +function setupCalc() { + document.querySelectorAll(".calc-btn").forEach((btn) => { + btn.onclick = () => { + const v = btn.dataset.v; + if (v === "C") { + state.calcExpr = ""; + state.calcResult = "0"; + } else if (v === "=") { + try { + state.calcResult = Function( + '"use strict";return (' + state.calcExpr.replace(/\^/g, "**") + ")", + )(); + } catch { + state.calcResult = "ERR"; + } + } else { + state.calcExpr += v; + } + $("calc-expr").textContent = state.calcExpr; + $("calc-result").textContent = state.calcResult; + }; + }); +} + +// ═══════════════════════════════════════════════════════════ +// TOOLS +// ═══════════════════════════════════════════════════════════ +function toolUUID() { + const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); + }); + copy(uuid); + toast("UUID: " + uuid.substring(0, 13) + "..."); +} + +let timerInterval, + timerSec = 0; +function toolTimer() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">TIMER</div><div class="instant-title" id="timer-display" style="font-variant-numeric:tabular-nums">00:00:00</div><div class="tool-btn-row" style="margin-top:20px"><button class="tool-action" onclick="startTimer()">START</button><button class="tool-action" onclick="stopTimer()">STOP</button><button class="tool-action" onclick="resetTimer()">RESET</button></div></div>`; + $("results-label").textContent = "TIMER"; +} +window.startTimer = () => { + if (timerInterval) return; + timerInterval = setInterval(() => { + timerSec++; + const h = String(Math.floor(timerSec / 3600)).padStart(2, "0"); + const m = String(Math.floor((timerSec % 3600) / 60)).padStart(2, "0"); + const s = String(timerSec % 60).padStart(2, "0"); + $("timer-display").textContent = `${h}:${m}:${s}`; + }, 1000); +}; +window.stopTimer = () => { + clearInterval(timerInterval); + timerInterval = null; +}; +window.resetTimer = () => { + stopTimer(); + timerSec = 0; + $("timer-display").textContent = "00:00:00"; +}; + +function toolBase64() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">BASE64</div><textarea id="b64-input" placeholder="ENTER TEXT..."></textarea><div class="tool-btn-row"><button class="tool-action" onclick="$('b64-output').textContent=btoa($('b64-input').value)">ENCODE</button><button class="tool-action" onclick="try{$('b64-output').textContent=atob($('b64-input').value)}catch{$('b64-output').textContent='INVALID'}">DECODE</button></div><div class="tool-output" id="b64-output">OUTPUT</div></div>`; + $("results-label").textContent = "BASE64"; +} + +function toolJSON() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">JSON</div><textarea id="json-input" placeholder="PASTE JSON..."></textarea><div class="tool-btn-row"><button class="tool-action" onclick="formatJSON()">FORMAT</button><button class="tool-action" onclick="minifyJSON()">MINIFY</button></div><pre class="tool-output" id="json-output" style="white-space:pre-wrap;max-height:300px;overflow:auto">OUTPUT</pre></div>`; + $("results-label").textContent = "JSON"; +} +window.formatJSON = () => { + try { + $("json-output").textContent = JSON.stringify(JSON.parse($("json-input").value), null, 2); + } catch (e) { + $("json-output").textContent = "INVALID: " + e.message; + } +}; +window.minifyJSON = () => { + try { + $("json-output").textContent = JSON.stringify(JSON.parse($("json-input").value)); + } catch (e) { + $("json-output").textContent = "INVALID: " + e.message; + } +}; + +function toolHash() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">SHA-256 HASH</div><textarea id="hash-input" placeholder="ENTER TEXT..."></textarea><div class="tool-btn-row"><button class="tool-action" onclick="genHash()">GENERATE</button></div><div class="tool-output" id="hash-output">OUTPUT</div></div>`; + $("results-label").textContent = "HASH"; +} +window.genHash = async () => { + const data = new TextEncoder().encode($("hash-input").value); + const hash = await crypto.subtle.digest("SHA-256", data); + $("hash-output").textContent = Array.from(new Uint8Array(hash)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +}; + +function toolColor() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">COLOR</div><input type="color" id="color-input" value="#ffffff" style="width:100%;height:100px;border:3px solid var(--fg);background:var(--bg);cursor:crosshair;"><div class="tool-output" id="color-output" style="margin-top:20px"></div></div>`; + const updateColor = () => { + const hex = $("color-input").value; + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + $("color-output").innerHTML = + `HEX: ${hex}<br>RGB: ${r}, ${g}, ${b}<br>HSL: ${rgbToHsl(r, g, b)}`; + }; + $("color-input").oninput = updateColor; + updateColor(); + $("results-label").textContent = "COLOR"; +} +function rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + const max = Math.max(r, g, b), + min = Math.min(r, g, b); + let h, + s, + l = (max + min) / 2; + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + break; + case g: + h = ((b - r) / d + 2) / 6; + break; + case b: + h = ((r - g) / d + 4) / 6; + break; + } + } + return `${Math.round(h * 360)}°, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%`; +} + +function toolPassword() { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"; + let pass = ""; + for (let i = 0; i < 20; i++) pass += chars[Math.floor(Math.random() * chars.length)]; + copy(pass); + toast("PASSWORD: " + pass.substring(0, 10) + "..."); +} + +function toolLoremIpsum() { + const lorem = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."; + copy(lorem); + toast("LOREM IPSUM COPIED"); +} + +// ═══════════════════════════════════════════════════════════ +// HISTORY +// ═══════════════════════════════════════════════════════════ +function addHistory(q) { + state.history = state.history.filter((h) => h.q !== q); + state.history.unshift({ q, t: Date.now() }); + state.history = state.history.slice(0, 20); + localStorage.setItem("sh", JSON.stringify(state.history)); + renderHistory(); +} + +function renderHistory() { + const list = $("history-list"); + if (!state.history.length) { + list.innerHTML = '<div style="color:var(--gray);font-size:11px;">NO HISTORY</div>'; + return; + } + list.innerHTML = state.history + .slice(0, 6) + .map( + (h) => + `<div class="history-item" onclick="searchFor('${esc(h.q)}')"><span>${esc(h.q)}</span><span class="history-time">${timeAgo(h.t)}</span></div>`, + ) + .join(""); +} + +// ═══════════════════════════════════════════════════════════ +// CLOCKS +// ═══════════════════════════════════════════════════════════ +function updateClocks() { + const now = new Date(); + $("clock").textContent = now.toLocaleTimeString("en-GB"); + const zones = [ + { label: "LOCAL", tz: Intl.DateTimeFormat().resolvedOptions().timeZone }, + { label: "UTC", tz: "UTC" }, + { label: "NYC", tz: "America/New_York" }, + { label: "TOKYO", tz: "Asia/Tokyo" }, + ]; + $("world-clocks").innerHTML = zones + .map( + (z) => + `<div class="clock"><div class="clock-time">${new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: z.tz })}</div><div class="clock-label">${z.label}</div></div>`, + ) + .join(""); +} + +// ═══════════════════════════════════════════════════════════ +// LISTENERS +// ═══════════════════════════════════════════════════════════ +function setupListeners() { + let debounce; + searchInput.addEventListener("input", (e) => { + clearTimeout(debounce); + debounce = setTimeout(() => search(e.target.value), 300); + }); + searchInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + clearTimeout(debounce); + search(searchInput.value); + } + if (e.key === "Escape") goHome(); + }); + document.addEventListener("keydown", (e) => { + if ( + e.key === "/" && + document.activeElement !== searchInput && + document.activeElement !== $("watchlist-input") + ) { + e.preventDefault(); + searchInput.focus(); + } + if (e.key === "Escape") goHome(); + }); + $("clear-btn").onclick = goHome; + $("voice-btn").onclick = voiceSearch; +} + +function voiceSearch() { + if (!("webkitSpeechRecognition" in window || "SpeechRecognition" in window)) { + toast("VOICE NOT SUPPORTED"); + return; + } + const Sr = window.SpeechRecognition || window.webkitSpeechRecognition; + const r = new Sr(); + r.lang = "en-US"; + $("voice-btn").style.background = "#fff"; + $("voice-btn").style.color = "#000"; + r.onresult = (e) => { + searchInput.value = e.results[0][0].transcript; + search(searchInput.value); + }; + r.onend = () => { + $("voice-btn").style.background = ""; + $("voice-btn").style.color = ""; + }; + r.start(); +} + +// ═══════════════════════════════════════════════════════════ +// UTILITIES +// ═══════════════════════════════════════════════════════════ +function esc(s) { + if (!s) return ""; + return s.replace( + /[&<>"']/g, + (m) => ({ "'": "'", '"': """, "&": "&", "<": "<", ">": ">" })[m], + ); +} +function decodeHTML(s) { + const t = document.createElement("textarea"); + t.innerHTML = s; + return t.value; +} +function highlight(text, query) { + if (!query) return text; + const words = query.split(/\s+/).filter((w) => w.length > 2); + let result = text; + words.forEach((w) => { + result = result.replace(new RegExp(`(${w})`, "gi"), "<mark>$1</mark>"); + }); + return result; +} +function fmt(n) { + if (n >= 1000000) return (n / 1000000).toFixed(1) + "M"; + if (n >= 1000) return (n / 1000).toFixed(1) + "K"; + return n?.toString() || "0"; +} +function timeAgo(ts) { + const s = Math.floor((Date.now() - ts) / 1000); + if (s < 60) return "NOW"; + if (s < 3600) return Math.floor(s / 60) + "M"; + if (s < 86400) return Math.floor(s / 3600) + "H"; + return Math.floor(s / 86400) + "D"; +} +function copy(text) { + navigator.clipboard.writeText(text); + toast("COPIED"); +} +function toast(msg) { + const t = document.createElement("div"); + t.className = "toast"; + t.textContent = msg; + $("toast-container").appendChild(t); + setTimeout(() => t.remove(), 3000); +} +function searchFor(q) { + searchInput.value = q; + search(q); +} +function insertBang(bang) { + searchInput.value = bang + " "; + searchInput.focus(); +} + +init(); |
