import "./global.css"; import { bangs } from "./bangs/hashbang.ts"; import { addToSearchHistory, clearSearchHistory, createAudio, getSearchHistory, storage, } from "./libs.ts"; import notFoundPageRender from "./404.ts"; export const CONSTANTS = { MAX_HISTORY: 500, ANIMATION_DURATION: 375, LOCAL_STORAGE_KEYS: { SEARCH_HISTORY: "search-history", SEARCH_COUNT: "search-count", HISTORY_ENABLED: "history-enabled", DEFAULT_BANG: "default-bang", CUSTOM_BANGS: "custom-bangs", }, CUTIES: { NOTFOUND: [ "(╯︵╰,)", "(。•́︿•̀。)", "(⊙_☉)", "(╯°□°)╯︵ ┻━┻", "(ಥ﹏ಥ)", "(✿◕‿◕✿)", "(╥﹏╥)", "(。•́︿•̀。)", "(✧ω✧)", "(•́_•̀)", "(╯°□°)╯︵ ┻━┻", ], LEFT: ["╰(°□°╰)", "(◕‿◕´)", "(・ω・´)"], RIGHT: ["(╯°□°)╯", "(`◕‿◕)", "(`・ω・)"], UP: ["(↑°□°)↑", "(´◕‿◕)↑", "↑(´・ω・)↑"], DOWN: ["(↓°□°)↓", "(´◕‿◕)↓", "↓(´・ω・)↓"], }, }; const customBangs: { [key: string]: { c?: string; d: string; r: number; s: string; sc?: string; t: string; u: string; }; } = JSON.parse(localStorage.getItem("custom-bangs") || "{}"); function getFocusableElements( root: HTMLElement = document.body, ): HTMLElement[] { return Array.from( root.querySelectorAll( 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])', ), ); } function setOutsideElementsTabindex(modal: HTMLElement, tabindex: number) { const modalElements = getFocusableElements(modal); const allElements = getFocusableElements(); for (const element of allElements) { if (!modalElements.includes(element)) { element.setAttribute("tabindex", tabindex.toString()); } } } const createTemplate = (data: { searchCount: string; historyEnabled: boolean; searchHistory: Array<{ bang: string; query: string; name: string; timestamp: number; }>; LS_DEFAULT_BANG: string; }) => `
${data.searchCount} ${data.searchCount === "1" ? "search" : "searches"}

┐( ˘_˘ )┌

DuckDuckGo's bang redirects are too slow. Add the following URL as a custom search engine to your browser. Enables all of DuckDuckGo's bangs.

${ data.historyEnabled ? `

Recent Searches

${ data.searchHistory.length === 0 ? `
No search history
` : data.searchHistory .map( (search) => `
${search.name}: ${search.query} ${new Date(search.timestamp).toLocaleString()}
`, ) .join("") }
` : "" }
`; function noSearchDefaultPageRender() { const searchCount = storage.get(CONSTANTS.LOCAL_STORAGE_KEYS.SEARCH_COUNT) || "0"; const historyEnabled = storage.get(CONSTANTS.LOCAL_STORAGE_KEYS.HISTORY_ENABLED) === "true"; const searchHistory = getSearchHistory(); const app = document.querySelector("#app"); if (!app) throw new Error("App element not found"); app.innerHTML = createTemplate({ searchCount, historyEnabled, searchHistory, LS_DEFAULT_BANG, }); const elements = { app, cutie: app.querySelector("#cutie"), copyInput: app.querySelector(".url-input"), copyButton: app.querySelector(".copy-button"), copyIcon: app.querySelector(".copy-button img"), urlInput: app.querySelector(".url-input"), settingsButton: app.querySelector(".settings-button"), modal: app.querySelector("#settings-modal"), closeModal: app.querySelector(".close-modal"), defaultBangSelect: app.querySelector("#default-bang"), description: app.querySelector("#bang-description"), historyToggle: app.querySelector("#history-toggle"), clearHistory: app.querySelector(".clear-history"), bangName: app.querySelector(".bang-name"), bangShortcut: app.querySelector(".bang-shortcut"), bangSearchUrl: app.querySelector(".bang-search-url"), bangBaseUrl: app.querySelector(".bang-base-url"), addBang: app.querySelector(".add-bang"), removeBangs: app.querySelectorAll(".remove-bang"), } as const; // Validate all elements exist for (const [key, element] of Object.entries(elements)) { if (!element) throw new Error(`${key} not found`); } // After validation, we can assert elements are non-null const validatedElements = elements as { [K in keyof typeof elements]: NonNullable<(typeof elements)[K]>; }; validatedElements.urlInput.value = `${window.location.protocol}//${window.location.host}?q=%s`; const prefersReducedMotion = window.matchMedia( "(prefers-reduced-motion: reduce)", ).matches; if (!prefersReducedMotion) { // Add mouse tracking behavior document.addEventListener("click", (e) => { const x = e.clientX; const y = e.clientY; const centerX = window.innerWidth / 2; const centerY = window.innerHeight / 2; const differenceX = x - centerX; const differenceY = y - centerY; if ( Math.abs(differenceX) > Math.abs(differenceY) && Math.abs(differenceX) > 100 ) { validatedElements.cutie.textContent = differenceX < 0 ? CONSTANTS.CUTIES.LEFT[ Math.floor(Math.random() * CONSTANTS.CUTIES.LEFT.length) ] : CONSTANTS.CUTIES.RIGHT[ Math.floor(Math.random() * CONSTANTS.CUTIES.RIGHT.length) ]; } else if (Math.abs(differenceY) > 100) { validatedElements.cutie.textContent = differenceY < 0 ? CONSTANTS.CUTIES.UP[ Math.floor(Math.random() * CONSTANTS.CUTIES.UP.length) ] : CONSTANTS.CUTIES.DOWN[ Math.floor(Math.random() * CONSTANTS.CUTIES.DOWN.length) ]; } }); const audio = { spin: createAudio("/heavier-tick-sprite.opus"), toggleOff: createAudio("/toggle-button-off.opus"), toggleOn: createAudio("/toggle-button-on.opus"), click: createAudio("/click-button.opus"), warning: createAudio("/double-button.opus"), copy: createAudio("/foot-switch.opus"), }; validatedElements.copyButton.addEventListener("click", () => { audio.copy.currentTime = 0; audio.copy.play(); }); validatedElements.settingsButton.addEventListener("mouseenter", () => { audio.spin.play(); }); validatedElements.settingsButton.addEventListener("mouseleave", () => { audio.spin.pause(); audio.spin.currentTime = 0; }); validatedElements.historyToggle.addEventListener("change", () => { if (validatedElements.historyToggle.checked) { audio.toggleOff.pause(); audio.toggleOff.currentTime = 0; audio.toggleOn.currentTime = 0; audio.toggleOn.play(); } else { audio.toggleOn.pause(); audio.toggleOn.currentTime = 0; audio.toggleOff.currentTime = 0; audio.toggleOff.play(); } }); validatedElements.clearHistory.addEventListener("click", () => { audio.warning.play(); }); validatedElements.defaultBangSelect.addEventListener("bangError", () => { audio.warning.currentTime = 0; audio.warning.play(); }); validatedElements.defaultBangSelect.addEventListener("bangSuccess", () => { audio.click.currentTime = 0; audio.click.play(); }); validatedElements.closeModal.addEventListener("closed", () => { validatedElements.settingsButton.classList.remove("rotate"); audio.spin.playbackRate = 0.7; audio.spin.currentTime = 0; audio.spin.play(); audio.spin.onended = () => { audio.spin.playbackRate = 1; }; }); validatedElements.addBang.addEventListener("click", () => { audio.click.currentTime = 0.1; audio.click.playbackRate = 2; audio.click.play(); }); validatedElements.removeBangs.forEach((button) => { button.addEventListener("click", () => { audio.warning.currentTime = 0; audio.warning.play(); }); }); } validatedElements.copyButton.addEventListener("click", async () => { await navigator.clipboard.writeText(validatedElements.urlInput.value); validatedElements.copyIcon.src = "/clipboard-check.svg"; if (!prefersReducedMotion) validatedElements.copyInput.classList.add("flash-white"); setTimeout(() => { validatedElements.copyInput.classList.remove("flash-white"); validatedElements.copyIcon.src = "/clipboard.svg"; }, 375); }); validatedElements.settingsButton.addEventListener("click", () => { validatedElements.settingsButton.classList.add("rotate"); validatedElements.modal.style.display = "block"; setOutsideElementsTabindex(validatedElements.modal, -1); }); validatedElements.closeModal.addEventListener("click", () => { validatedElements.closeModal.dispatchEvent(new Event("closed")); }); window.addEventListener("click", (event) => { if (event.target === validatedElements.modal) { validatedElements.closeModal.dispatchEvent(new Event("closed")); } }); validatedElements.closeModal.addEventListener("closed", () => { validatedElements.modal.style.display = "none"; setOutsideElementsTabindex(validatedElements.modal, 0); if (validatedElements.historyToggle.checked !== historyEnabled) if (!prefersReducedMotion) setTimeout(() => { window.location.reload(); }, 300); else window.location.reload(); }); validatedElements.defaultBangSelect.addEventListener("change", (event) => { const newDefaultBang = (event.target as HTMLSelectElement).value.replace( /^!+/, "", ); const bang = customBangs[newDefaultBang] || bangs[newDefaultBang]; if (!bang) { validatedElements.defaultBangSelect.value = LS_DEFAULT_BANG; validatedElements.defaultBangSelect.classList.add("shake", "flash-red"); validatedElements.defaultBangSelect.dispatchEvent( new CustomEvent("bangError"), ); setTimeout(() => { validatedElements.defaultBangSelect.classList.remove( "shake", "flash-red", ); }, 300); return; } validatedElements.defaultBangSelect.dispatchEvent( new CustomEvent("bangSuccess"), ); storage.set(CONSTANTS.LOCAL_STORAGE_KEYS.DEFAULT_BANG, newDefaultBang); validatedElements.description.innerText = "Default Bang: " + bang.s; }); validatedElements.historyToggle.addEventListener("change", (event) => { storage.set( CONSTANTS.LOCAL_STORAGE_KEYS.HISTORY_ENABLED, (event.target as HTMLInputElement).checked.toString(), ); }); validatedElements.clearHistory.addEventListener("click", () => { clearSearchHistory(); if (!prefersReducedMotion) setTimeout(() => { window.location.reload(); }, 375); else window.location.reload(); }); validatedElements.addBang.addEventListener("click", () => { const name = validatedElements.bangName.value.trim(); const shortcut = validatedElements.bangShortcut.value .trim() .replace(/^!+/, ""); const searchUrl = validatedElements.bangSearchUrl.value.trim(); const baseUrl = validatedElements.bangBaseUrl.value.trim(); if (!name || !searchUrl || !baseUrl) return; customBangs[shortcut] = { t: name, s: shortcut, u: searchUrl, d: baseUrl, r: 0, }; storage.set( CONSTANTS.LOCAL_STORAGE_KEYS.CUSTOM_BANGS, JSON.stringify(customBangs), ); if (!prefersReducedMotion) setTimeout(() => { window.location.reload(); }, 375); else window.location.reload(); }); validatedElements.removeBangs.forEach((button) => { button.addEventListener("click", (event) => { const shortcut = (event.target as HTMLButtonElement).dataset .shortcut as string; delete customBangs[shortcut]; storage.set( CONSTANTS.LOCAL_STORAGE_KEYS.CUSTOM_BANGS, JSON.stringify(customBangs), ); if (!prefersReducedMotion) setTimeout(() => { window.location.reload(); }, 375); else window.location.reload(); }); }); } const LS_DEFAULT_BANG = storage.get(CONSTANTS.LOCAL_STORAGE_KEYS.DEFAULT_BANG) ?? "ddg"; const defaultBang = bangs[LS_DEFAULT_BANG]; function ensureProtocol(url: string, defaultProtocol = "https://") { try { const parsedUrl = new URL(url); return parsedUrl.href; // If valid, return as is } catch (e) { return `${defaultProtocol}${url}`; } } function getBangredirectUrl() { const url = new URL(window.location.href); const query = url.searchParams.get("q")?.trim() ?? ""; switch (url.pathname.replace(/\/$/, "")) { case "": { if (!query || query === "!" || query === "!settings") { noSearchDefaultPageRender(); return null; } const match = query.toLowerCase().match(/^!(\S+)|!(\S+)$/i); const selectedBang = match ? customBangs[match[1] || match[2]] || bangs[match[1] || match[2]] : defaultBang; const cleanQuery = match ? query.replace(/!\S+\s*|^(\S+!|!\S+)$/i, "").trim() : query; // Redirect to base domain if cleanQuery is empty if (!cleanQuery && selectedBang?.d) { return ensureProtocol(selectedBang.d); } const redirectUrl = selectedBang?.u.replace( "{{{s}}}", encodeURIComponent(cleanQuery).replace(/%2F/g, "/"), ); // Do these operations after determining redirect URL to minimize delay setTimeout(() => { const count = ( Number.parseInt( storage.get(CONSTANTS.LOCAL_STORAGE_KEYS.SEARCH_COUNT) || "0", ) + 1 ).toString(); storage.set(CONSTANTS.LOCAL_STORAGE_KEYS.SEARCH_COUNT, count); if ( storage.get(CONSTANTS.LOCAL_STORAGE_KEYS.HISTORY_ENABLED) === "true" ) { addToSearchHistory(cleanQuery, { bang: selectedBang?.t || "", name: selectedBang?.s || "", url: selectedBang?.u || "", }); } }, 0); return redirectUrl; } default: notFoundPageRender(); return null; } } function doRedirect() { const searchUrl = getBangredirectUrl(); if (!searchUrl) return; window.location.replace(searchUrl); } doRedirect();