Skip to content

Commit 9657281

Browse files
committed
feat(featured): add compact featured banner (static config)
1 parent 3c6c5b7 commit 9657281

File tree

5 files changed

+124
-0
lines changed

5 files changed

+124
-0
lines changed

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ <h2 class="panel-heading-sm centered" data-i18n="advanced.title">Mode avancé</h
181181
</section>
182182

183183

184+
<div id="featuredBanner" class="featured-banner" aria-hidden="false"></div>
184185
<div id="apps" class="app-list" aria-live="polite" aria-busy="true"></div>
185186
<section id="appDetails" class="app-details" hidden>
186187
<button id="backToListBtn" class="btn btn-outline back-btn" data-i18n="details.back">← Retour</button>

src/renderer/config/featured.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Simple static featured config used for the compact banner
2+
// Each item: { name, title, desc, color }
3+
module.exports = [
4+
{ name: 'code', title: 'Visual Studio Code', desc: 'A lightweight but powerful source code editor.', color: '#007ACC' },
5+
{ name: 'vlc', title: 'VLC', desc: 'A free and open source cross-platform multimedia player.', color: '#E02525' },
6+
{ name: 'jellyfin', title: 'Jellyfin', desc: 'A personal media server that puts you in control.', color: '#F58A25' }
7+
];
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
(function registerFeatured(){
2+
const namespace = window.features = window.features || {};
3+
4+
function renderItem(item, container) {
5+
const html = `
6+
<div class="featured-item" role="group" aria-label="${item.title}">
7+
<div class="featured-visual" style="background:${item.color || 'var(--primary)'}">
8+
<img class="featured-icon" src="${getIconUrl(item.name||'')}" alt="${item.title}" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/Portable-Linux-Apps/Portable-Linux-Apps.github.io/main/icons/blank.png'"/>
9+
</div>
10+
<div class="featured-body">
11+
<div class="featured-title">${item.title}</div>
12+
<div class="featured-desc">${item.desc}</div>
13+
</div>
14+
</div>`;
15+
container.innerHTML = html;
16+
}
17+
18+
function init(options = {}) {
19+
const container = options.container || document.getElementById('featuredBanner');
20+
const state = options.state || {};
21+
const items = options.items || (window?.require ? (window.require('./config/featured') || []) : []);
22+
if (!container) return null;
23+
container.innerHTML = `
24+
<div class="featured-inner">
25+
<button class="featured-prev" aria-label="Previous">◀</button>
26+
<div class="featured-slot"></div>
27+
<button class="featured-next" aria-label="Next">▶</button>
28+
<div class="featured-dots" aria-hidden="true"></div>
29+
</div>`;
30+
31+
const slot = container.querySelector('.featured-slot');
32+
const prev = container.querySelector('.featured-prev');
33+
const next = container.querySelector('.featured-next');
34+
const dots = container.querySelector('.featured-dots');
35+
36+
let idx = 0;
37+
let timer = null;
38+
39+
function updateDots() {
40+
dots.innerHTML = items.map((_, i) => `<button class="dot" data-idx="${i}" aria-label="${i+1}"></button>`).join('');
41+
const btns = dots.querySelectorAll('.dot');
42+
btns.forEach(b => b.addEventListener('click', () => { goTo(parseInt(b.dataset.idx,10)); }));
43+
}
44+
45+
function show(index) {
46+
if (!items || !items.length) { slot.innerHTML = ''; return; }
47+
idx = (index + items.length) % items.length;
48+
renderItem(items[idx], slot);
49+
const btns = dots.querySelectorAll('.dot');
50+
btns.forEach((b,i)=> b.classList.toggle('active', i===idx));
51+
// add click handler on slot to open details
52+
const item = items[idx];
53+
const itemEl = slot.querySelector('.featured-item');
54+
if (itemEl) {
55+
itemEl.style.cursor = 'pointer';
56+
itemEl.onclick = () => { if (typeof showDetails === 'function') showDetails(item.name); };
57+
}
58+
}
59+
60+
function goTo(i) { show(i); resetTimer(); }
61+
function nextItem() { show(idx+1); }
62+
function prevItem() { show(idx-1); }
63+
function resetTimer() { if (timer) { clearInterval(timer); } timer = setInterval(nextItem, 6000); }
64+
65+
prev.addEventListener('click', () => { prevItem(); resetTimer(); });
66+
next.addEventListener('click', () => { nextItem(); resetTimer(); });
67+
68+
updateDots();
69+
show(0);
70+
resetTimer();
71+
72+
// Accessibility: keyboard
73+
container.addEventListener('keydown', (e) => {
74+
if (e.key === 'ArrowLeft') { prevItem(); resetTimer(); }
75+
if (e.key === 'ArrowRight') { nextItem(); resetTimer(); }
76+
});
77+
78+
return Object.freeze({ show, goTo, destroy() { clearInterval(timer); } });
79+
}
80+
81+
namespace.featured = Object.freeze({ init });
82+
})();

src/renderer/renderer.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,6 +1559,15 @@ const handleIconCachePurged = () => {
15591559
};
15601560

15611561
const searchFeature = window.features?.search?.init?.({
1562+
// ...existing options...
1563+
});
1564+
1565+
// Initialize featured banner (compact) feature
1566+
const featuredFeature = window.features?.featured?.init?.({
1567+
container: document.getElementById('featuredBanner'),
1568+
items: (window?.require ? window.require('./config/featured') : [])
1569+
});
1570+
15621571
state,
15631572
searchInput: document.getElementById('searchInput'),
15641573
tabs: Array.from(tabs),

style.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,31 @@ body.details-mode #appDetails { opacity:1; transform:translateY(0); }
10251025
#updatesPanel { text-align:center; }
10261026
#updatesPanel h2, #updatesPanel p { text-align:center; }
10271027
#updatesPanel .updates-actions-row { margin:32px auto 0 auto; width:fit-content; }
1028+
1029+
/* Featured banner (compact) */
1030+
.featured-banner { width:100%; margin-bottom:18px; }
1031+
.featured-banner .featured-inner { display:flex; align-items:center; gap:12px; padding:10px 14px; border-radius:12px; background:linear-gradient(90deg, rgba(0,0,0,0.03), rgba(0,0,0,0.02)); }
1032+
.featured-banner .featured-prev, .featured-banner .featured-next { background:transparent; border:none; color:var(--fg); font-size:16px; padding:8px; cursor:pointer; }
1033+
.featured-banner .featured-slot { flex:1; min-height:100px; display:flex; align-items:center; }
1034+
.featured-item { display:flex; align-items:center; gap:12px; width:100%; }
1035+
.featured-visual { width:96px; height:96px; border-radius:8px; display:flex; align-items:center; justify-content:center; }
1036+
.featured-icon { width:64px; height:64px; object-fit:contain; }
1037+
.featured-body { flex:1; overflow:hidden; }
1038+
.featured-title { font-weight:700; font-size:1rem; color:var(--fg); }
1039+
.featured-desc { color:var(--muted); font-size:0.95rem; opacity:0.95; }
1040+
.featured-dots { display:flex; gap:6px; margin-left:12px; }
1041+
.featured-dots .dot { width:8px; height:8px; border-radius:50%; background:rgba(0,0,0,0.12); border:none; }
1042+
.featured-dots .dot.active { background:var(--primary); }
1043+
@media (prefers-color-scheme: dark){
1044+
.featured-banner .featured-inner{ background:linear-gradient(90deg, rgba(255,255,255,0.01), rgba(255,255,255,0.01)); }
1045+
.featured-dots .dot { background:rgba(255,255,255,0.06); }
1046+
}
1047+
@media (max-width:640px){
1048+
.featured-item { gap:10px; }
1049+
.featured-visual { width:72px; height:72px; }
1050+
.featured-title { font-size:0.95rem; }
1051+
.featured-desc { display:none; }
1052+
}
10281053
#updatesPanel .update-spinner { justify-content:center; }
10291054
#updatesPanel #updateResult { text-align:center; }
10301055
/* Spinner de mise à jour */

0 commit comments

Comments
 (0)