···
import { bangs } from "./bang";
4
+
function addToSearchHistory(
6
+
bang: { bang: string; name: string; url: string },
8
+
const history = getSearchHistory();
13
+
timestamp: Date.now(),
15
+
// Keep only last 500 searches
16
+
history.splice(500);
17
+
localStorage.setItem("search-history", JSON.stringify(history));
20
+
function getSearchHistory(): Array<{
27
+
return JSON.parse(localStorage.getItem("search-history") || "[]");
33
+
function clearSearchHistory() {
34
+
localStorage.setItem("search-history", "[]");
function getFocusableElements(
root: HTMLElement = document.body,
···
function noSearchDefaultPageRender() {
const searchCount = localStorage.getItem("search-count") || "0";
60
+
const historyEnabled = localStorage.getItem("history-enabled") === "true";
61
+
const searchHistory = getSearchHistory();
const app = document.querySelector<HTMLDivElement>("#app");
if (!app) throw new Error("App element not found");
···
<img src="/clipboard.svg" alt="Copy" />
91
+
<h2 style="margin-top: 24px;">Recent Searches</h2>
92
+
<div style="max-height: 200px; overflow-y: auto; text-align: left;">
94
+
searchHistory.length === 0
95
+
? `<div style="padding: 8px; text-align: center;">No search history</div>`
99
+
<div style="padding: 8px; border-bottom: 1px solid var(--border-color);">
100
+
<a href="?q=!${search.bang} ${search.query}">${search.name}: ${search.query}</a>
101
+
<span style="float: right; color: var(--text-color-secondary);">
102
+
${new Date(search.timestamp).toLocaleString()}
made with ♥ by <a href="https://github.com/taciturnaxolotl" target="_blank">Kieran Klukas</a> as <a href="https://github.com/taciturnaxolotl/unduck" target="_blank">open source</a> software
···
<div class="modal-content">
<button class="close-modal">×</button>
121
+
<div class="settings-section">
<label for="default-bang" id="bang-description">${bangs.find((b) => b.t === LS_DEFAULT_BANG)?.s || "Unknown bang"}</label>
<div class="bang-select-container">
<input type="text" id="default-bang" class="bang-select" value="${LS_DEFAULT_BANG}">
127
+
<div class="settings-section">
128
+
<h3>Search History (${searchHistory.length}/500)</h3>
129
+
<div style="display: flex; justify-content: space-between; align-items: center;">
130
+
<label class="switch">
131
+
<label for="history-toggle">Enable Search History</label>
132
+
<input type="checkbox" id="history-toggle" ${historyEnabled ? "checked" : ""}>
133
+
<span class="slider round"></span>
135
+
<button class="clear-history">Clear History</button>
···
app.querySelector<HTMLParagraphElement>("#bang-description");
if (!description) throw new Error("Bang description not found");
163
+
const historyToggle = app.querySelector<HTMLInputElement>("#history-toggle");
164
+
if (!historyToggle) throw new Error("History toggle not found");
165
+
const clearHistory = app.querySelector<HTMLButtonElement>(".clear-history");
166
+
if (!clearHistory) throw new Error("Clear history button not found");
urlInput.value = `${window.location.protocol}//${window.location.host}?q=%s`;
···
"(prefers-reduced-motion: reduce)",
if (!prefersReducedMotion) {
108
-
const audio = new Audio("/heavier-tick-sprite.mp3");
183
+
const spinAudio = new Audio("/heavier-tick-sprite.mp3");
184
+
const toggleOffAudio = new Audio("/toggle-button-off.mp3");
185
+
const toggleOnAudio = new Audio("/toggle-button-on.mp3");
186
+
const clickAudio = new Audio("/click-button.mp3");
187
+
const warningAudio = new Audio("/double-button.mp3");
settingsButton.addEventListener("mouseenter", () => {
settingsButton.addEventListener("mouseleave", () => {
116
-
audio.currentTime = 0;
195
+
spinAudio.currentTime = 0;
198
+
historyToggle.addEventListener("change", () => {
199
+
if (historyToggle.checked) {
200
+
toggleOffAudio.pause();
201
+
toggleOffAudio.currentTime = 0;
202
+
toggleOnAudio.currentTime = 0;
203
+
toggleOnAudio.play();
205
+
toggleOnAudio.pause();
206
+
toggleOnAudio.currentTime = 0;
207
+
toggleOffAudio.currentTime = 0;
208
+
toggleOffAudio.play();
212
+
clearHistory.addEventListener("click", () => {
213
+
warningAudio.play();
216
+
defaultBangSelect.addEventListener("bangError", () => {
217
+
warningAudio.currentTime = 0;
218
+
warningAudio.play();
221
+
defaultBangSelect.addEventListener("bangSuccess", () => {
222
+
clickAudio.currentTime = 0;
···
defaultBangSelect.value = LS_DEFAULT_BANG; // Reset to previous value
defaultBangSelect.classList.add("shake", "flash-red");
254
+
// Dispatch error event
255
+
defaultBangSelect.dispatchEvent(new CustomEvent("bangError"));
// Remove animation classes after animation completes
defaultBangSelect.classList.remove("shake", "flash-red");
···
265
+
defaultBangSelect.dispatchEvent(new CustomEvent("bangSuccess"));
localStorage.setItem("default-bang", newDefaultBang);
description.innerText = bang.s;
271
+
// Enable/disable search history
272
+
historyToggle.addEventListener("change", (event) => {
273
+
localStorage.setItem(
275
+
(event.target as HTMLInputElement).checked.toString(),
278
+
clearHistory.addEventListener("click", () => {
279
+
clearSearchHistory();
280
+
if (!prefersReducedMotion)
282
+
window.location.reload();
284
+
else window.location.reload();
const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? "ddg";
···
? bangs.find((b) => b.t === match[1].toLowerCase())
const cleanQuery = match ? query.replace(/!\S+\s*/i, "").trim() : query;
311
+
// Add search to history
312
+
if (localStorage.getItem("history-enabled") === "true") {
313
+
addToSearchHistory(cleanQuery, {
314
+
bang: selectedBang?.t || "",
315
+
name: selectedBang?.s || "",
316
+
url: selectedBang?.u || "",
return selectedBang?.u.replace(