88let settingsAPI ;
99let 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
1263function 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
358493function 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