Skip to content

Commit 1136ab9

Browse files
Merge pull request #91 from Sameem-baba/feat/custom-search-engine
feat(settings): add custom search engine support with validation (clean)
2 parents 5bf760e + 41374e0 commit 1136ab9

File tree

9 files changed

+443
-55
lines changed

9 files changed

+443
-55
lines changed

src/main.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createMenuTemplate } from "./actions.js";
99
import WindowManager, { createIsolatedWindow } from "./window-manager.js";
1010
import settingsManager from "./settings-manager.js";
1111
import { attachContextMenus, setWindowManager } from "./context-menu.js";
12+
import { isBuiltInSearchEngine } from "./search-engine.js";
1213
// import { setupAutoUpdater } from "./auto-updater.js";
1314

1415
const P2P_PROTOCOL = {
@@ -205,4 +206,14 @@ ipcMain.on('update-group-properties', (event, groupId, properties) => {
205206
}
206207
});
207208
});
209+
210+
ipcMain.handle('check-built-in-engine', (event, template) => {
211+
try {
212+
return isBuiltInSearchEngine(template);
213+
} catch (error) {
214+
console.error('Error in check-built-in-engine:', error);
215+
return false; // fallback if anything goes wrong
216+
}
217+
});
218+
208219
export { windowManager };

src/pages/nav-box.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ class NavBox extends HTMLElement {
88
this._resizeListener = null;
99
this.buildNavBox();
1010
this.attachEvents();
11+
this.updateSearchPlaceholder();
1112
this.attachThemeListener();
1213
}
1314

15+
connectedCallback() {
16+
// Ensure placeholder updates when element is attached to DOM
17+
this.updateSearchPlaceholder();
18+
}
19+
1420
setStyledUrl(url) {
1521
const urlInput = this.querySelector("#url");
1622
if (!urlInput) return;
@@ -50,7 +56,7 @@ class NavBox extends HTMLElement {
5056
const urlInput = document.createElement("input");
5157
urlInput.type = "text";
5258
urlInput.id = "url";
53-
urlInput.placeholder = "Search with DuckDuckGo or type a P2P URL";
59+
this.updateSearchPlaceholder();
5460

5561
const qrButton = this.createButton(
5662
"qr-code",
@@ -260,6 +266,54 @@ class NavBox extends HTMLElement {
260266
}
261267
}
262268

269+
_titleCase(key) {
270+
if (!key) return 'Search';
271+
return key.replace(/(^|[\s-])(\w)/g, (_, p1, p2) => p1 + p2.toUpperCase());
272+
}
273+
274+
_safeParseUrl(s) {
275+
try { return new URL(s); } catch {}
276+
try { return new URL('https://' + s); } catch {}
277+
return null;
278+
}
279+
280+
async updateSearchPlaceholder() {
281+
const input = this.querySelector('#url');
282+
if (!input) return;
283+
284+
try {
285+
const { ipcRenderer } = require("electron");
286+
const engineKey = await ipcRenderer.invoke('settings-get', 'searchEngine');
287+
288+
let name;
289+
if (engineKey === 'custom') {
290+
const tpl = await ipcRenderer.invoke('settings-get', 'customSearchTemplate');
291+
const u = this._safeParseUrl(tpl);
292+
293+
if (u?.hostname) {
294+
// remove 'www.' and the top-level domain (.com, .org, .net, etc.)
295+
const cleanHost = u.hostname
296+
.replace(/^www\./i, '') // remove www.
297+
.replace(/\.[^.]+$/, ''); // remove last dot + tld
298+
299+
// capitalize first letter
300+
name = this._titleCase(cleanHost);
301+
} else {
302+
name = 'Custom';
303+
}
304+
} else if (engineKey) {
305+
name = this._titleCase(engineKey);
306+
} else {
307+
name = 'DuckDuckGo'
308+
}
309+
310+
input.placeholder = `Search with ${name} or type a P2P URL`;
311+
} catch (err) {
312+
console.warn('updateSearchPlaceholder failed:', err);
313+
input.placeholder = 'Search or type a P2P URL';
314+
}
315+
}
316+
263317
attachEvents() {
264318
this.addEventListener("click", (event) => {
265319
const button = event.target.closest("button");

src/pages/settings.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,32 @@ <h1 class="section-title">Search Engine</h1>
124124
<div class="select-option" data-value="ecosia">Ecosia</div>
125125
<div class="select-option" data-value="kagi">Kagi</div>
126126
<div class="select-option" data-value="startpage">Startpage</div>
127+
<div class="select-option" data-value="custom">Custom</div>
127128
</div>
128129
</div>
129130
<input type="hidden" id="search-engine" value="duckduckgo">
130131
</div>
131132
</div>
133+
134+
<div
135+
class="setting-row tall-row"
136+
id="custom-search-row"
137+
style="display: none"
138+
>
139+
<div class="setting-label">Custom search URL</div>
140+
<div class="setting-control">
141+
<input
142+
type="text"
143+
id="custom-search-template"
144+
class="text-input input-fill"
145+
placeholder="https://example.com/search?q=%s"
146+
autocomplete="off"
147+
/>
148+
<div id="custom-search-message" class="help-or-error">
149+
Use <code>%s</code> where the query should go.
150+
</div>
151+
</div>
152+
</div>
132153

133154
<div class="setting-row">
134155
<div class="setting-label">Clear cache</div>

src/pages/static/js/settings.js

Lines changed: 181 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,57 @@
88
let settingsAPI;
99
let eventCleanupFunctions = [];
1010

11+
function validateSearchTemplate(tpl) {
12+
if (typeof tpl !== "string")
13+
return { valid: false, reason: "Template must be a string." };
14+
15+
const s = tpl.trim();
16+
if (!s) return { valid: false, reason: "Template cannot be empty." };
17+
18+
try {
19+
new URL(s); // just test if it's a valid URL structure
20+
return { valid: true };
21+
} catch {
22+
return { valid: false, reason: "Template must be a valid URL." };
23+
}
24+
}
25+
26+
function setTemplateFieldState(inputEl, messageEl, state) {
27+
inputEl.classList.remove("invalid", "valid");
28+
messageEl.classList.remove("error", "success");
29+
30+
if (state.valid) {
31+
inputEl.classList.add("valid");
32+
messageEl.classList.add("success");
33+
messageEl.innerHTML =
34+
"✅ Press <b>Enter</b> to set this custom search engine.";
35+
} else {
36+
inputEl.classList.add("invalid");
37+
messageEl.classList.add("error");
38+
messageEl.textContent = state.reason || "Invalid template.";
39+
}
40+
}
41+
42+
/**
43+
* Checks if the provided search template matches any built-in search engine.
44+
* @param {string} tpl - The custom search template URL.
45+
* @returns {boolean} - True if it's a built-in search engine, otherwise false.
46+
*/
47+
async function isBuiltInSearchEngine(tpl) {
48+
try {
49+
if (!window.electronAPI?.onCheckBuiltInEngine) {
50+
console.warn("onCheckBuiltInEngine API not available in this context");
51+
return false;
52+
}
53+
54+
const result = await window.electronAPI.onCheckBuiltInEngine(tpl);
55+
return result;
56+
} catch (err) {
57+
console.error('IPC check failed:', err);
58+
return false;
59+
}
60+
}
61+
1162
// Initialize API access with fallback handling
1263
function initializeAPI() {
1364
console.log('Settings: Attempting to initialize API...');
@@ -115,6 +166,12 @@ document.addEventListener('DOMContentLoaded', async () => {
115166
searchEngine.value = newEngine;
116167
updateCustomDropdownDisplays();
117168
}
169+
170+
// Toggle the Custom URL row live when engine changes
171+
const row = document.getElementById("custom-search-row");
172+
if (row) {
173+
row.style.display = newEngine === "custom" ? "" : "none";
174+
}
118175
});
119176
eventCleanupFunctions.push(cleanup2);
120177
}
@@ -161,6 +218,12 @@ document.addEventListener('DOMContentLoaded', async () => {
161218

162219
// Get form elements
163220
const searchEngine = document.getElementById('search-engine');
221+
const customSearchRow = document.getElementById("custom-search-row");
222+
const customSearchTemplate = document.getElementById(
223+
"custom-search-template"
224+
);
225+
const customSearchMessage = document.getElementById("custom-search-message");
226+
164227
const themeToggle = document.getElementById('theme-toggle');
165228
const showClock = document.getElementById('show-clock');
166229
const verticalTabs = document.getElementById('vertical-tabs');
@@ -273,9 +336,81 @@ document.addEventListener('DOMContentLoaded', async () => {
273336
});
274337

275338
// Add change listeners for form elements
276-
searchEngine?.addEventListener('change', async (e) => {
277-
console.log('Search engine changed:', e.target.value);
278-
await saveSettingToBackend('searchEngine', e.target.value);
339+
searchEngine?.addEventListener("change", async (e) => {
340+
const value = e.target.value;
341+
console.log("Search engine changed (UI):", value);
342+
343+
if (value === "custom") {
344+
// Show inline input/modal, but do NOT save the engine yet
345+
if (customSearchRow) customSearchRow.style.display = "";
346+
// optional UX: prefill from existing template and focus
347+
customSearchTemplate?.focus();
348+
customSearchTemplate?.select?.();
349+
return; // do NOT call settings.set('searchEngine', 'custom') yet
350+
}
351+
352+
// For all non-custom engines, persist immediately and hide the row
353+
await saveSettingToBackend("searchEngine", value);
354+
if (customSearchRow) customSearchRow.style.display = "none";
355+
});
356+
357+
customSearchTemplate?.addEventListener("input", async () => {
358+
const tpl = customSearchTemplate.value.trim();
359+
const state = validateSearchTemplate(tpl);
360+
361+
const isBuiltIn = await isBuiltInSearchEngine(tpl);
362+
363+
364+
if (isBuiltIn) {
365+
state.valid = false;
366+
state.reason = "This search engine already exists in the browser.";
367+
}
368+
369+
setTemplateFieldState(customSearchTemplate, customSearchMessage, state);
370+
});
371+
372+
// Save custom search template on Enter
373+
customSearchTemplate?.addEventListener("keydown", async (e) => {
374+
if (e.key !== "Enter") return;
375+
const tpl = customSearchTemplate.value.trim();
376+
const state = validateSearchTemplate(tpl);
377+
378+
const isBuiltIn = await isBuiltInSearchEngine(tpl);
379+
380+
if (isBuiltIn) {
381+
state.valid = false;
382+
state.reason = "This search engine already exists in the browser.";
383+
}
384+
setTemplateFieldState(customSearchTemplate, customSearchMessage, state);
385+
if (!state.valid) return;
386+
387+
388+
// 🚫 Check for built-in search engines
389+
if (isBuiltIn) {
390+
customSearchMessage.style.display = "block";
391+
customSearchMessage.textContent = "This search engine already exists in the browser.";
392+
return;
393+
}
394+
395+
try {
396+
// Save template first
397+
await saveSettingToBackend("customSearchTemplate", tpl);
398+
// Then set engine to custom (only now)
399+
if (searchEngine && searchEngine.value !== "custom") {
400+
searchEngine.value = "custom";
401+
}
402+
await saveSettingToBackend("searchEngine", "custom");
403+
404+
// ✅ Hide the helper message once successfully set
405+
customSearchMessage.textContent = "";
406+
customSearchMessage.style.display = "none";
407+
408+
if (customSearchRow) customSearchRow.style.display = "";
409+
showSettingsSavedMessage("Custom search template saved", "success");
410+
} catch (err) {
411+
console.error(err);
412+
showSettingsSavedMessage("Failed to save custom template", "error");
413+
}
279414
});
280415

281416
themeToggle?.addEventListener('change', async (e) => {
@@ -356,7 +491,14 @@ async function loadSettingsFromBackend() {
356491

357492
// Populate form fields with settings data
358493
function populateFormFields(settings) {
359-
const searchEngine = document.getElementById('search-engine');
494+
const searchEngine = document.getElementById("search-engine");
495+
const customSearchRow = document.getElementById("custom-search-row");
496+
const customSearchTemplate = document.getElementById(
497+
"custom-search-template"
498+
);
499+
const customSearchMessage = document.getElementById("custom-search-message");
500+
501+
360502
const themeToggle = document.getElementById('theme-toggle');
361503
const showClock = document.getElementById('show-clock');
362504
const verticalTabs = document.getElementById('vertical-tabs');
@@ -366,6 +508,39 @@ function populateFormFields(settings) {
366508
if (searchEngine && settings.searchEngine) {
367509
searchEngine.value = settings.searchEngine;
368510
}
511+
512+
// Show/hide the custom row based on saved engine
513+
if (customSearchRow) {
514+
customSearchRow.style.display =
515+
settings.searchEngine === "custom" ? "" : "none";
516+
}
517+
518+
// Prefill template input
519+
if (customSearchTemplate) {
520+
const tpl = settings.customSearchTemplate || "https://duckduckgo.com/?q=%s";
521+
customSearchTemplate.value = tpl;
522+
523+
const state = validateSearchTemplate(tpl);
524+
525+
// Apply only visual input state (valid/invalid)…
526+
customSearchTemplate.classList.remove("invalid", "valid");
527+
if (state.valid) customSearchTemplate.classList.add("valid");
528+
else customSearchTemplate.classList.add("invalid");
529+
530+
// …and control the message based on whether the engine is already set
531+
// If engine is already 'custom' and template is valid, HIDE the message.
532+
if (settings.searchEngine === "custom" && state.valid) {
533+
customSearchMessage.textContent = "";
534+
customSearchMessage.classList.remove("error", "success");
535+
customSearchMessage.style.display = "none";
536+
} else {
537+
// Otherwise show the neutral hint (not the success text)
538+
customSearchMessage.style.display = "";
539+
customSearchMessage.classList.remove("error", "success");
540+
customSearchMessage.innerHTML = 'Please include a placeholder for the search term. If none, the browser will automatically add a search query parameter <code>?q=</code>.';
541+
}
542+
}
543+
369544
if (themeToggle && settings.theme) {
370545
themeToggle.value = settings.theme;
371546

@@ -417,6 +592,7 @@ async function saveSettingToBackend(key, value) {
417592
// Create user-friendly success messages
418593
const successMessages = {
419594
'searchEngine': 'Search engine updated successfully!',
595+
'customSearchTemplate': "Custom template updated successfully!",
420596
'theme': 'Theme updated successfully!',
421597
'showClock': 'Clock setting updated successfully!',
422598
'wallpaper': 'Wallpaper updated successfully!',
@@ -432,6 +608,7 @@ async function saveSettingToBackend(key, value) {
432608
// Create user-friendly error messages
433609
const errorMessages = {
434610
'searchEngine': 'Failed to save search engine setting',
611+
'customSearchTemplate': "Failed to save custom template",
435612
'theme': 'Failed to save theme setting',
436613
'showClock': 'Failed to save clock setting',
437614
'wallpaper': 'Failed to save wallpaper setting',

0 commit comments

Comments
 (0)