termsearch 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TermSearch
2
2
 
3
- [![Version](https://img.shields.io/badge/version-0.3.8-blue.svg)](#)
3
+ [![Version](https://img.shields.io/npm/v/termsearch.svg)](https://www.npmjs.com/package/termsearch)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)](https://nodejs.org)
6
6
  [![Platform](https://img.shields.io/badge/Termux%20%7C%20Linux%20%7C%20macOS-green.svg)](#)
@@ -286,6 +286,27 @@ const ENGINE_PRESETS = [
286
286
  { id: 'github', label: 'GitHub Focus', engines: ['github-api', 'github', 'duckduckgo', 'wikipedia'] },
287
287
  ];
288
288
 
289
+ // ─── Engine availability (requires config) ────────────────────────────────
290
+ const SEARXNG_ROUTED = new Set([
291
+ 'bing', 'google', 'yahoo', 'startpage', 'qwant',
292
+ 'youtube', 'reddit', 'hackernews', 'sepiasearch',
293
+ 'wikidata', 'crossref', 'openalex', 'openlibrary',
294
+ 'mastodon users', 'mastodon hashtags', 'tootfinder',
295
+ 'lemmy communities', 'lemmy posts',
296
+ 'piratebay', '1337x', 'nyaa',
297
+ ]);
298
+
299
+ function isEngineAvailable(engine) {
300
+ const cfg = state.config || {};
301
+ if (engine === 'brave') return Boolean(cfg.brave?.enabled && cfg.brave?.api_key);
302
+ if (engine === 'mojeek') return Boolean(cfg.mojeek?.enabled && cfg.mojeek?.api_key);
303
+ if (engine === 'yandex') return cfg.yandex?.enabled !== false;
304
+ if (engine === 'ahmia') return cfg.ahmia?.enabled !== false;
305
+ if (engine === 'marginalia') return cfg.marginalia?.enabled !== false;
306
+ if (SEARXNG_ROUTED.has(engine)) return Boolean(cfg.searxng?.enabled && cfg.searxng?.url);
307
+ return true; // duckduckgo, wikipedia, github, github-api — sempre disponibili
308
+ }
309
+
289
310
  function detectPresetFromBase(base) {
290
311
  const raw = String(base || '').toLowerCase();
291
312
  if (!raw) return 'custom';
@@ -316,8 +337,11 @@ function LangPicker() {
316
337
 
317
338
  function EnginePicker(opts = {}) {
318
339
  const compact = Boolean(opts.compact);
340
+ // [] = ALL (no filter sent to backend)
341
+ const isAll = state.selectedEngines.length === 0;
342
+ const selectedCount = isAll ? 0 : state.selectedEngines.length;
343
+
319
344
  const details = el('details', { className: `engine-picker${compact ? ' engine-picker-compact' : ''}` });
320
- const selectedCount = state.selectedEngines.length;
321
345
  const summary = compact
322
346
  ? el('summary', { className: 'engine-picker-summary engine-picker-summary-icon', title: 'Search engines' },
323
347
  iconEl('filter', 'engine-filter-icon'),
@@ -329,45 +353,64 @@ function EnginePicker(opts = {}) {
329
353
  );
330
354
 
331
355
  const body = el('div', { className: 'engine-picker-body' });
356
+
357
+ // Preset buttons
332
358
  const presetRow = el('div', { className: 'engine-preset-row' });
333
359
  ENGINE_PRESETS.forEach((preset) => {
360
+ const isActive = preset.id === 'all' ? isAll
361
+ : preset.engines.length > 0 && preset.engines.every(e => state.selectedEngines.includes(e)) && state.selectedEngines.length === preset.engines.length;
334
362
  presetRow.append(el('button', {
335
- className: `btn ${preset.id === 'balanced' ? 'btn-primary' : ''}`,
363
+ className: `btn ${isActive || preset.id === 'balanced' ? 'btn-primary' : ''}`,
336
364
  type: 'button',
337
365
  onClick: () => {
338
- setSelectedEngines(preset.engines);
339
- details.open = false;
340
- if (state.query) doSearch(state.query, state.category);
341
- else renderApp();
366
+ // 'all' preset → clear filter (backend uses all configured providers)
367
+ setSelectedEngines(preset.id === 'all' ? [] : preset.engines);
368
+ // visually check/uncheck all available chips
369
+ [...details.querySelectorAll('.engine-chip input:not(:disabled)')].forEach((input) => {
370
+ input.checked = preset.id === 'all' || preset.engines.includes(input.closest('.engine-chip')?.querySelector('span')?.textContent?.trim().toLowerCase() || '');
371
+ });
342
372
  },
343
373
  }, preset.label));
344
374
  });
345
375
  body.append(presetRow);
346
376
 
377
+ // Engine chips per group
347
378
  ENGINE_GROUPS.forEach((group) => {
348
379
  const card = el('div', { className: 'engine-group' });
349
380
  card.append(el('div', { className: 'engine-group-title' }, group.label));
350
381
  const list = el('div', { className: 'engine-chip-wrap' });
351
382
  group.items.forEach((engine) => {
352
- const checked = state.selectedEngines.includes(engine);
383
+ const available = isEngineAvailable(engine);
384
+ // checked = explicitly selected, OR in "all" mode ([] = all available)
385
+ const checked = available && (isAll || state.selectedEngines.includes(engine));
353
386
  const id = `engine-${engine.replace(/[^a-z0-9]+/g, '-')}-${Math.random().toString(36).slice(2, 6)}`;
354
- const input = el('input', { id, type: 'checkbox', ...(checked ? { checked: '' } : {}) });
355
- const label = el('label', { className: 'engine-chip', for: id }, input, el('span', {}, engine));
387
+ const inputAttrs = { id, type: 'checkbox', ...(checked ? { checked: '' } : {}), ...(available ? {} : { disabled: '' }) };
388
+ const input = el('input', inputAttrs);
389
+ const chipClass = `engine-chip${available ? '' : ' engine-chip-unavailable'}`;
390
+ const title = available ? engine : `${engine} — not configured (Settings)`;
391
+ const label = el('label', { className: chipClass, for: id, title },
392
+ input,
393
+ el('span', {}, engine),
394
+ );
356
395
  list.append(label);
357
396
  });
358
397
  card.append(list);
359
398
  body.append(card);
360
399
  });
361
400
 
401
+ // Apply / Reset
362
402
  body.append(el('div', { className: 'engine-actions' },
363
403
  el('button', {
364
404
  className: 'btn btn-primary',
365
405
  type: 'button',
366
406
  onClick: () => {
367
- const selected = [...details.querySelectorAll('.engine-chip input:checked')]
368
- .map((node) => node.parentElement?.textContent?.trim().toLowerCase())
407
+ const checked = [...details.querySelectorAll('.engine-chip input:not(:disabled):checked')]
408
+ .map((node) => node.closest('.engine-chip')?.querySelector('span')?.textContent?.trim().toLowerCase())
369
409
  .filter(Boolean);
370
- setSelectedEngines(selected);
410
+ const availableAll = ENGINE_GROUPS.flatMap(g => g.items).filter(isEngineAvailable);
411
+ // If all available engines are checked, send [] (no filter)
412
+ const allChecked = availableAll.every(e => checked.includes(e));
413
+ setSelectedEngines(allChecked ? [] : checked);
371
414
  details.open = false;
372
415
  if (state.query) doSearch(state.query, state.category);
373
416
  else renderApp();
@@ -396,7 +439,6 @@ function EnginePicker(opts = {}) {
396
439
  document.removeEventListener('click', onClickOutside, true);
397
440
  }
398
441
  };
399
- // Defer to avoid catching the same click that opened it
400
442
  requestAnimationFrame(() => document.addEventListener('click', onClickOutside, true));
401
443
  });
402
444
 
@@ -1138,11 +1180,12 @@ function renderApp() {
1138
1180
 
1139
1181
  // ─── Homepage ─────────────────────────────────────────────────────────────
1140
1182
  function addToBrowser() {
1183
+ let attemptedLegacyAdd = false;
1141
1184
  // Try legacy Firefox API
1142
1185
  try {
1143
1186
  if (window.external?.AddSearchProvider) {
1187
+ attemptedLegacyAdd = true;
1144
1188
  window.external.AddSearchProvider(location.origin + '/opensearch.xml');
1145
- return;
1146
1189
  }
1147
1190
  } catch {}
1148
1191
  // Show inline hint
@@ -1157,6 +1200,7 @@ function addToBrowser() {
1157
1200
  urlBox.append(urlText, copyBtn);
1158
1201
  const hint = el('div', { className: 'add-browser-hint' },
1159
1202
  el('div', { className: 'add-browser-title' }, 'Add TermSearch to your browser'),
1203
+ attemptedLegacyAdd ? el('div', { className: 'add-browser-note' }, 'If no browser prompt appears, add it manually with the URL below.') : null,
1160
1204
  el('div', { className: 'add-browser-steps' },
1161
1205
  el('div', {}, el('span', { className: 'add-browser-badge' }, 'Firefox'), ' Address bar → click TermSearch icon → "Add"'),
1162
1206
  el('div', {}, el('span', { className: 'add-browser-badge' }, 'Chrome'), ' Settings → Search engine → Manage → Add'),
@@ -1165,11 +1209,37 @@ function addToBrowser() {
1165
1209
  el('div', { className: 'add-browser-label' }, 'Search URL (paste in browser settings):'),
1166
1210
  urlBox,
1167
1211
  );
1168
- document.querySelector('.home-actions')?.after(hint);
1212
+ const footer = document.querySelector('.footer');
1213
+ if (footer) footer.before(hint);
1214
+ else document.getElementById('app')?.append(hint);
1215
+ hint.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1169
1216
  setTimeout(() => hint.remove(), 20000);
1170
1217
  }
1171
1218
 
1172
1219
  function renderHome(app) {
1220
+ // Top-right controls
1221
+ const topBar = el('div', { className: 'home-topbar' },
1222
+ LangPicker(),
1223
+ el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
1224
+ el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
1225
+ );
1226
+
1227
+ // History below search (if present)
1228
+ const historyEl = (state.historyEnabled && state.searchHistory.length > 0)
1229
+ ? el('div', { className: 'home-history' },
1230
+ ...state.searchHistory.slice(0, 8).map((q) =>
1231
+ el('button', {
1232
+ className: 'home-history-item',
1233
+ type: 'button',
1234
+ onClick: () => { state.query = q; state.category = 'web'; doSearch(q, 'web'); },
1235
+ },
1236
+ el('span', { className: 'home-history-icon', html: ICONS.search }),
1237
+ el('span', {}, q),
1238
+ ),
1239
+ ),
1240
+ )
1241
+ : null;
1242
+
1173
1243
  const home = el('div', { className: 'home' },
1174
1244
  el('div', { className: 'home-logo' }, 'Term', el('strong', {}, 'Search')),
1175
1245
  el('div', { className: 'home-tagline' },
@@ -1177,23 +1247,20 @@ function renderHome(app) {
1177
1247
  el('span', { className: 'tagline-mobile' }, 'Private local search'),
1178
1248
  ),
1179
1249
  el('div', { className: 'home-search' }, SearchForm('', (q) => { state.query = q; state.category = 'web'; doSearch(q, 'web'); })),
1180
- el('div', { className: 'home-actions' },
1181
- LangPicker(),
1182
- el('button', { className: 'btn', onClick: () => navigate('#/settings') }, iconEl('settings'), ' Settings'),
1183
- el('button', { className: 'btn', onClick: toggleTheme }, iconEl('theme'), ' Theme'),
1184
- el('button', { className: 'btn', onClick: addToBrowser, title: 'Add as browser search engine' }, iconEl('search'), ' Add to browser'),
1185
- el('a', { className: 'btn', href: 'https://github.com/DioNanos/termsearch', target: '_blank', rel: 'noopener noreferrer' }, iconEl('github'), ' GitHub'),
1186
- ),
1250
+ historyEl,
1187
1251
  );
1188
1252
 
1189
1253
  const footer = el('div', { className: 'footer' },
1190
1254
  el('span', { className: 'footer-link' }, '© 2026 DioNanos'),
1191
- el('a', { className: 'footer-link', href: 'https://github.com/DioNanos/termsearch', target: '_blank', rel: 'noopener' },
1192
- iconEl('github'), 'GitHub',
1255
+ el('a', { className: 'footer-link', href: 'https://github.com/DioNanos/termsearch', target: '_blank', rel: 'noopener noreferrer' },
1256
+ iconEl('github'), ' GitHub',
1257
+ ),
1258
+ el('button', { className: 'footer-link footer-add-btn', onClick: addToBrowser, title: 'Add as default search engine' },
1259
+ iconEl('search'), ' Add to browser',
1193
1260
  ),
1194
1261
  );
1195
1262
 
1196
- app.append(home, footer);
1263
+ app.append(topBar, home, footer);
1197
1264
  }
1198
1265
 
1199
1266
  // ─── Settings ─────────────────────────────────────────────────────────────
@@ -97,7 +97,7 @@ a:hover { color: var(--link-h); }
97
97
  .pulse { animation: pulse 1.5s ease-in-out infinite; }
98
98
 
99
99
  /* ─── Layout ──────────────────────────────────────────────────────────────── */
100
- #app { min-height: 100vh; display: flex; flex-direction: column; }
100
+ #app { min-height: 100vh; display: flex; flex-direction: column; position: relative; }
101
101
 
102
102
  /* ─── Header ──────────────────────────────────────────────────────────────── */
103
103
  .header {
@@ -281,6 +281,17 @@ a:hover { color: var(--link-h); }
281
281
  .engine-chip input {
282
282
  margin: 0;
283
283
  }
284
+ .engine-chip-unavailable {
285
+ opacity: 0.35;
286
+ cursor: not-allowed;
287
+ border-style: dashed;
288
+ pointer-events: none;
289
+ }
290
+ .engine-chip-unavailable span {
291
+ font-weight: 700;
292
+ text-decoration: line-through;
293
+ text-decoration-color: rgba(255,255,255,0.25);
294
+ }
284
295
  .engine-actions {
285
296
  display: flex;
286
297
  gap: 8px;
@@ -289,13 +300,23 @@ a:hover { color: var(--link-h); }
289
300
  }
290
301
 
291
302
  /* ─── Homepage ────────────────────────────────────────────────────────────── */
303
+ .home-topbar {
304
+ position: absolute;
305
+ top: 12px;
306
+ right: 16px;
307
+ display: flex;
308
+ align-items: center;
309
+ gap: 4px;
310
+ z-index: 10;
311
+ }
312
+
292
313
  .home {
293
314
  flex: 1;
294
315
  display: flex;
295
316
  flex-direction: column;
296
317
  align-items: center;
297
318
  justify-content: center;
298
- padding: 48px 20px 80px;
319
+ padding: 48px 20px 40px;
299
320
  gap: 0;
300
321
  }
301
322
 
@@ -326,14 +347,44 @@ a:hover { color: var(--link-h); }
326
347
  .tagline-mobile { display: none; }
327
348
  .tagline-desktop { display: inline; }
328
349
 
329
- .home-search { width: 100%; max-width: 560px; margin-bottom: 14px; }
350
+ .home-search { width: 100%; max-width: 560px; margin-bottom: 0; }
330
351
 
331
- .home-actions {
352
+ /* History below search */
353
+ .home-history {
354
+ width: 100%;
355
+ max-width: 560px;
356
+ margin-top: 6px;
357
+ display: flex;
358
+ flex-direction: column;
359
+ gap: 1px;
360
+ border: 1px solid var(--border);
361
+ border-radius: var(--radius);
362
+ overflow: hidden;
363
+ background: var(--bg2);
364
+ }
365
+ .home-history-item {
332
366
  display: flex;
333
367
  align-items: center;
334
- gap: 8px;
335
- flex-wrap: wrap;
336
- justify-content: center;
368
+ gap: 10px;
369
+ padding: 8px 14px;
370
+ font-size: 13px;
371
+ color: var(--text2);
372
+ text-align: left;
373
+ border-bottom: 1px solid var(--border2);
374
+ cursor: pointer;
375
+ transition: background 0.1s;
376
+ background: transparent;
377
+ border-left: none;
378
+ border-right: none;
379
+ border-top: none;
380
+ }
381
+ .home-history-item:last-child { border-bottom: none; }
382
+ .home-history-item:hover { background: var(--bg3); color: var(--text); }
383
+ .home-history-icon {
384
+ opacity: 0.35;
385
+ flex-shrink: 0;
386
+ display: inline-flex;
387
+ align-items: center;
337
388
  }
338
389
 
339
390
  /* ─── Search Bar ──────────────────────────────────────────────────────────── */
@@ -954,6 +1005,11 @@ a:hover { color: var(--link-h); }
954
1005
  color: var(--text);
955
1006
  margin-bottom: 10px;
956
1007
  }
1008
+ .add-browser-note {
1009
+ margin-bottom: 10px;
1010
+ font-size: 11px;
1011
+ color: #c4b5fd;
1012
+ }
957
1013
  .add-browser-steps {
958
1014
  display: flex;
959
1015
  flex-direction: column;
@@ -1017,12 +1073,18 @@ a:hover { color: var(--link-h); }
1017
1073
  /* ─── Footer ──────────────────────────────────────────────────────────────── */
1018
1074
  .footer {
1019
1075
  border-top: 1px solid var(--border2);
1020
- padding: 12px 16px;
1076
+ padding: 10px 16px;
1021
1077
  display: flex;
1022
1078
  align-items: center;
1023
1079
  justify-content: center;
1024
1080
  gap: 16px;
1025
1081
  }
1082
+ .footer-add-btn {
1083
+ background: none;
1084
+ border: none;
1085
+ cursor: pointer;
1086
+ padding: 0;
1087
+ }
1026
1088
  .footer-link {
1027
1089
  color: var(--text3);
1028
1090
  font-size: 10px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termsearch",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Personal search engine for Termux/Linux/macOS — zero-config, privacy-first, AI-optional",
5
5
  "type": "module",
6
6
  "bin": {