Skip to content

Commit 09950e5

Browse files
andy5995claude
andauthored
Add client-side search to index.md and apps.md (#142)
* Add client-side search to index.md and apps.md - New shared script assets/js/search.js powers both widgets - Home page: fetches apps.json on focus, shows up to 20 matching apps with icon + link, plus a "see all matches" link to apps.html?q=... - Apps page: filters the existing table in place, pre-fills from the ?q= URL param so the handoff from the home page lands pre-filtered Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Eagerly load apps.json on home-page search init Previously the "Loading app database…" status text never updated until the user focused the input, which made it look stuck. Kick off the fetch on init so the count (or an error) replaces the placeholder within a second of page load. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 84b34f7 commit 09950e5

4 files changed

Lines changed: 159 additions & 1 deletion

File tree

_includes/head-custom.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<script src="{{ '/assets/js/search.js' | relative_url }}" defer></script>

apps.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
#### Here are listed all **3121** unique applications, AppImage packages and command line utilities managed by [AM](https://github.com/ivan-hc/AM) and [AppMan](https://github.com/ivan-hc/AppMan) for the x86_64 architecture, plus **78** more entries and items to help you install apps more easily.
77

8-
*Use your browser's built-in search tool to easily navigate to this page or use the tags below.*
8+
*Use the search box below to filter the table, or jump straight to a category using the tags below.*
99

1010

1111
#### *Categories*
@@ -26,6 +26,13 @@
2626

2727
-----------------
2828

29+
<div id="app-search-box" style="margin: 1em 0;">
30+
<label for="app-search-input" style="font-weight: bold;">Search applications:</label>
31+
<input type="search" id="app-search-input" placeholder="Type a name or keyword..." autocomplete="off"
32+
style="width: 100%; max-width: 480px; padding: 0.5em 0.75em; margin-top: 0.25em; font-size: 1em; border: 1px solid #999; border-radius: 4px; box-sizing: border-box;">
33+
<span id="app-search-count" style="margin-left: 0.5em; font-style: italic; color: #666;"></span>
34+
</div>
35+
2936
| ICON | PACKAGE NAME | DESCRIPTION | INSTALLER |
3037
| --- | --- | --- | --- |
3138
| <img loading="lazy" src="icons/0ad-prerelease.png" width="48" height="48"> | [***0ad-prerelease***](apps/0ad-prerelease.md) | *FOSS historical Real Time Strategy, RTS game of ancient warfare (Pre-release).*..[ *read more* ](apps/0ad-prerelease.md)*!* | [*blob*](https://github.com/ivan-hc/AM/blob/main/programs/x86_64/0ad-prerelease) **/** [*raw*](https://raw.githubusercontent.com/ivan-hc/AM/main/programs/x86_64/0ad-prerelease) |

assets/js/search.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
(function () {
2+
'use strict';
3+
4+
function escapeHtml(s) {
5+
return String(s).replace(/[&<>"']/g, function (c) {
6+
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
7+
});
8+
}
9+
10+
// Filters the rows of the big apps table in-place.
11+
function initTableFilter() {
12+
var input = document.getElementById('app-search-input');
13+
var counter = document.getElementById('app-search-count');
14+
if (!input) return;
15+
16+
var table = input.closest('div').nextElementSibling;
17+
while (table && table.tagName !== 'TABLE') table = table.nextElementSibling;
18+
if (!table) return;
19+
20+
var rows = Array.prototype.slice.call(table.tBodies[0] ? table.tBodies[0].rows : table.rows);
21+
if (rows.length && rows[0].cells[0] && rows[0].cells[0].tagName === 'TH') rows.shift();
22+
23+
var haystacks = rows.map(function (row) {
24+
return (row.textContent || '').toLowerCase();
25+
});
26+
var total = rows.length;
27+
28+
function update() {
29+
var q = input.value.trim().toLowerCase();
30+
var visible = 0;
31+
for (var i = 0; i < rows.length; i++) {
32+
var match = q === '' || haystacks[i].indexOf(q) !== -1;
33+
rows[i].style.display = match ? '' : 'none';
34+
if (match) visible++;
35+
}
36+
counter.textContent = q === ''
37+
? '(' + total + ' apps)'
38+
: '(' + visible + ' of ' + total + ' shown)';
39+
}
40+
41+
input.addEventListener('input', update);
42+
43+
var params = new URLSearchParams(window.location.search);
44+
var initialQ = params.get('q');
45+
if (initialQ) {
46+
input.value = initialQ;
47+
input.focus();
48+
}
49+
update();
50+
}
51+
52+
// Fetches apps.json and renders a short list of matches with links.
53+
function initJsonSearch() {
54+
var MAX_RESULTS = 20;
55+
var APPS_URL = 'apps.json';
56+
var APPS_PAGE = 'apps.html';
57+
58+
var input = document.getElementById('home-search-input');
59+
var status = document.getElementById('home-search-status');
60+
var results = document.getElementById('home-search-results');
61+
var more = document.getElementById('home-search-more');
62+
if (!input) return;
63+
64+
var apps = null;
65+
var pending = null;
66+
67+
function load() {
68+
if (apps || pending) return pending;
69+
pending = fetch(APPS_URL).then(function (r) { return r.json(); }).then(function (data) {
70+
apps = data;
71+
status.textContent = apps.length + ' apps in the database.';
72+
if (input.value.trim()) render(input.value.trim());
73+
}).catch(function () {
74+
status.textContent = 'Could not load the app database. Try again later.';
75+
});
76+
return pending;
77+
}
78+
79+
function render(q) {
80+
results.innerHTML = '';
81+
more.innerHTML = '';
82+
if (!apps) return;
83+
if (!q) {
84+
status.textContent = apps.length + ' apps in the database.';
85+
return;
86+
}
87+
var needle = q.toLowerCase();
88+
var matches = [];
89+
for (var i = 0; i < apps.length; i++) {
90+
var a = apps[i];
91+
var hay = (a.packageName + ' ' + (a.description || '')).toLowerCase();
92+
if (hay.indexOf(needle) !== -1) matches.push(a);
93+
}
94+
if (!matches.length) {
95+
status.textContent = 'No apps match "' + q + '".';
96+
return;
97+
}
98+
status.textContent = matches.length + ' match' + (matches.length === 1 ? '' : 'es')
99+
+ (matches.length > MAX_RESULTS ? ' (showing first ' + MAX_RESULTS + ')' : '') + ':';
100+
var shown = matches.slice(0, MAX_RESULTS);
101+
var html = '';
102+
for (var j = 0; j < shown.length; j++) {
103+
var m = shown[j];
104+
var safeName = escapeHtml(m.packageName);
105+
var safeDesc = escapeHtml((m.description || '').replace(/\.\.\.$/, ''));
106+
var safeIcon = escapeHtml(m.icon || '');
107+
html += '<li style="display: flex; align-items: center; gap: 0.6em; padding: 0.35em 0.25em; border-bottom: 1px solid #eee;">'
108+
+ '<img loading="lazy" src="' + safeIcon + '" alt="" width="32" height="32" style="flex: 0 0 32px;">'
109+
+ '<span style="flex: 1;">'
110+
+ '<a href="apps/' + safeName + '.html"><strong>' + safeName + '</strong></a>'
111+
+ '<br><span style="color: #555; font-size: 0.9em;">' + safeDesc + '</span>'
112+
+ '</span>'
113+
+ '</li>';
114+
}
115+
results.innerHTML = html;
116+
if (matches.length > MAX_RESULTS) {
117+
more.innerHTML = '<a href="' + APPS_PAGE + '?q=' + encodeURIComponent(q) + '">See all '
118+
+ matches.length + ' matches on the apps page &rarr;</a>';
119+
}
120+
}
121+
122+
input.addEventListener('focus', load);
123+
input.addEventListener('input', function () {
124+
if (apps) render(input.value.trim());
125+
else load();
126+
});
127+
128+
load();
129+
}
130+
131+
function init() {
132+
initTableFilter();
133+
initJsonSearch();
134+
}
135+
136+
if (document.readyState === 'loading') {
137+
document.addEventListener('DOMContentLoaded', init);
138+
} else {
139+
init();
140+
}
141+
})();

index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
| - | - |
1919
| [<img loading="lazy" src="https://github.com/user-attachments/assets/f55d4242-bd5f-4195-946c-e01c6fe5e264" width="512" height="256">](https://portable-linux-apps.github.io/apps.html) | [<img loading="lazy" src="https://raw.githubusercontent.com/ivan-hc/AM/main/sample/sample.png" width="512" height="256">](https://github.com/ivan-hc/AM) |
2020

21+
<div id="home-search" style="margin: 1.5em auto; max-width: 640px; text-align: left;">
22+
<label for="home-search-input" style="font-weight: bold; display: block; margin-bottom: 0.25em; text-align: center;">Search applications</label>
23+
<input type="search" id="home-search-input" placeholder="Start typing an app name or keyword..." autocomplete="off"
24+
style="width: 100%; padding: 0.6em 0.85em; font-size: 1em; border: 1px solid #999; border-radius: 4px; box-sizing: border-box;">
25+
<div id="home-search-status" style="margin-top: 0.5em; font-style: italic; color: #666; text-align: center;">Loading app database&hellip;</div>
26+
<ul id="home-search-results" style="list-style: none; padding: 0; margin: 0.5em 0 0;"></ul>
27+
<div id="home-search-more" style="margin-top: 0.5em; text-align: center;"></div>
28+
</div>
29+
2130
#### *Categories*
2231

2332
***[AppImages](appimages.md)*** - ***[am-utils](am-utils.md)*** - ***[android](android.md)*** - ***[audio](audio.md)*** - ***[comic](comic.md)*** - ***[command-line](command-line.md)*** - ***[communication](communication.md)*** - ***[disk](disk.md)*** - ***[education](education.md)*** - ***[file-manager](file-manager.md)*** - ***[finance](finance.md)*** - ***[game](game.md)*** - ***[gnome](gnome.md)*** - ***[graphic](graphic.md)*** - ***[internet](internet.md)*** - ***[kde](kde.md)*** - ***[office](office.md)*** - ***[password](password.md)*** - ***[steam](steam.md)*** - ***[system-monitor](system-monitor.md)*** - ***[video](video.md)*** - ***[web-app](web-app.md)*** - ***[web-browser](web-browser.md)*** - ***[wine](wine.md)***

0 commit comments

Comments
 (0)