mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
feat: auto-detect OS in download banner (#93)
## Summary - Detects user's OS via `navigator.userAgent` and shows only the relevant download button (Windows, macOS Apple Silicon, or macOS Intel) - Uses WebGL renderer info to distinguish Apple Silicon from Intel Macs where possible - Adds a "Show all platforms" toggle so users can still access other platform downloads - Falls back to showing all 3 buttons if OS can't be detected ## Test plan - [x] Open the web app on a Mac — should see only the macOS (Apple Silicon) button - [x] Open on Windows — should see only the Windows button - [x] Click "Show all platforms" — all 3 buttons should appear - [x] Click "Show less" — should collapse back to the detected platform - [x] Spoof an unrecognized user agent — all 3 buttons should show (no toggle) Replaces #91 Closes #90 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -28,7 +28,63 @@ function dismiss(panel: HTMLElement): void {
|
||||
panel.addEventListener('transitionend', () => panel.remove(), { once: true });
|
||||
}
|
||||
|
||||
type Platform = 'macos-arm64' | 'macos-x64' | 'macos' | 'windows' | 'unknown';
|
||||
|
||||
function detectPlatform(): Platform {
|
||||
const ua = navigator.userAgent;
|
||||
if (/Windows/i.test(ua)) return 'windows';
|
||||
if (/Mac/i.test(ua)) {
|
||||
// WebGL renderer can reveal Apple Silicon vs Intel GPU
|
||||
try {
|
||||
const c = document.createElement('canvas');
|
||||
const gl = c.getContext('webgl') as WebGLRenderingContext | null;
|
||||
if (gl) {
|
||||
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
if (dbg) {
|
||||
const renderer = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL);
|
||||
if (/Apple M/i.test(renderer)) return 'macos-arm64';
|
||||
if (/Intel/i.test(renderer)) return 'macos-x64';
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
// Can't determine architecture — show both Mac options
|
||||
return 'macos';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
interface DlButton { cls: string; href: string; label: string }
|
||||
|
||||
const ALL_BUTTONS: DlButton[] = [
|
||||
{ cls: 'mac', href: '/api/download?platform=macos-arm64', label: '\uF8FF macOS (Apple Silicon)' },
|
||||
{ cls: 'mac', href: '/api/download?platform=macos-x64', label: '\uF8FF macOS (Intel)' },
|
||||
{ cls: 'win', href: '/api/download?platform=windows-exe', label: '\u229E Windows (.exe)' },
|
||||
];
|
||||
|
||||
function buttonsForPlatform(p: Platform): DlButton[] {
|
||||
switch (p) {
|
||||
case 'macos-arm64': return ALL_BUTTONS.filter(b => b.href.includes('macos-arm64'));
|
||||
case 'macos-x64': return ALL_BUTTONS.filter(b => b.href.includes('macos-x64'));
|
||||
case 'macos': return ALL_BUTTONS.filter(b => b.cls === 'mac');
|
||||
case 'windows': return ALL_BUTTONS.filter(b => b.cls === 'win');
|
||||
default: return ALL_BUTTONS;
|
||||
}
|
||||
}
|
||||
|
||||
function renderButtons(container: HTMLElement, buttons: DlButton[], panel: HTMLElement): void {
|
||||
container.innerHTML = buttons
|
||||
.map(b => `<a class="wm-dl-btn ${b.cls}" href="${b.href}">${b.label}</a>`)
|
||||
.join('');
|
||||
container.querySelectorAll('.wm-dl-btn').forEach(btn =>
|
||||
btn.addEventListener('click', () => dismiss(panel))
|
||||
);
|
||||
}
|
||||
|
||||
function buildPanel(): HTMLElement {
|
||||
const platform = detectPlatform();
|
||||
const primaryButtons = buttonsForPlatform(platform);
|
||||
const showToggle = platform !== 'unknown' && primaryButtons.length < ALL_BUTTONS.length;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'wm-dl-panel';
|
||||
el.innerHTML = `
|
||||
@@ -81,23 +137,36 @@ function buildPanel(): HTMLElement {
|
||||
color: var(--semantic-info);
|
||||
}
|
||||
.wm-dl-btn.win:hover { background: color-mix(in srgb, var(--semantic-info) 15%, transparent); }
|
||||
.wm-dl-toggle {
|
||||
background: none; border: none; color: var(--text-dim, #888);
|
||||
font-size: 9px; cursor: pointer; padding: 4px 0 0; text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.wm-dl-toggle:hover { color: var(--text, #e8e8e8); }
|
||||
</style>
|
||||
<div class="wm-dl-head">
|
||||
<div class="wm-dl-title">\u{1F5A5} Desktop Available</div>
|
||||
<button class="wm-dl-close" aria-label="Dismiss">\u00D7</button>
|
||||
</div>
|
||||
<div class="wm-dl-body">Native performance, secure local key storage, offline map tiles.</div>
|
||||
<div class="wm-dl-btns">
|
||||
<a class="wm-dl-btn mac" href="/api/download?platform=macos-arm64">\uF8FF macOS (Apple Silicon)</a>
|
||||
<a class="wm-dl-btn mac" href="/api/download?platform=macos-x64">\uF8FF macOS (Intel)</a>
|
||||
<a class="wm-dl-btn win" href="/api/download?platform=windows-exe">\u229E Windows (.exe)</a>
|
||||
</div>
|
||||
<div class="wm-dl-btns"></div>
|
||||
${showToggle ? '<button class="wm-dl-toggle">Show all platforms</button>' : ''}
|
||||
`;
|
||||
|
||||
const btnsContainer = el.querySelector('.wm-dl-btns') as HTMLElement;
|
||||
renderButtons(btnsContainer, primaryButtons, el);
|
||||
|
||||
el.querySelector('.wm-dl-close')!.addEventListener('click', () => dismiss(el));
|
||||
el.querySelectorAll('.wm-dl-btn').forEach(btn =>
|
||||
btn.addEventListener('click', () => dismiss(el))
|
||||
);
|
||||
|
||||
const toggle = el.querySelector('.wm-dl-toggle');
|
||||
if (toggle) {
|
||||
let showingAll = false;
|
||||
toggle.addEventListener('click', () => {
|
||||
showingAll = !showingAll;
|
||||
renderButtons(btnsContainer, showingAll ? ALL_BUTTONS : primaryButtons, el);
|
||||
toggle.textContent = showingAll ? 'Show less' : 'Show all platforms';
|
||||
});
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user