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 +1 -1
- package/frontend/dist/app.js +92 -25
- package/frontend/dist/style.css +70 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TermSearch
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/termsearch)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](#)
|
package/frontend/dist/app.js
CHANGED
|
@@ -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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
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
|
|
355
|
-
const
|
|
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
|
|
368
|
-
.map((node) => node.
|
|
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
|
-
|
|
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('.
|
|
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
|
-
|
|
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 ─────────────────────────────────────────────────────────────
|
package/frontend/dist/style.css
CHANGED
|
@@ -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
|
|
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:
|
|
350
|
+
.home-search { width: 100%; max-width: 560px; margin-bottom: 0; }
|
|
330
351
|
|
|
331
|
-
|
|
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:
|
|
335
|
-
|
|
336
|
-
|
|
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:
|
|
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;
|