Add Hardware tab with sensor stills next to UI previews.

Map each firmware project to a Commons or original module photo for local catalog browsing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-23 16:09:20 +08:00
parent 24c740174e
commit b083da50b1
26 changed files with 232 additions and 38 deletions

View File

@@ -51,6 +51,8 @@ After rebuilding a project, run its `firmware\export_from_build.ps1`, then click
Preferred card preview: **`media/preview.html`** — HTML recreation of the on-device LVGL UI (320×240). Auto-detected if present.
Cards also offer a **Hardware** tab (sensor / module still). Mapping lives in `lib/sensor-media.ts`; images under `public/sensors/` (see `ATTRIBUTION.md`).
Optional `meta.json` beside the `.bin` files:
```json

View File

@@ -1,13 +1,17 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import type { FirmwareInfo } from "@/lib/firmware";
import { sensorMediaForProject } from "@/lib/sensor-media";
import { cn } from "@/lib/utils";
const SCREEN_W = 320;
const SCREEN_H = 240;
/** Renders HTML on-device UI mock (preferred) — never uses raster covers. */
type Tab = "ui" | "sensor";
/** On-device HTML UI mock + optional hardware still, switchable via tabs. */
export function FirmwarePreview({
fw,
className,
@@ -17,12 +21,20 @@ export function FirmwarePreview({
className?: string;
title?: string;
}) {
const t = useTranslations("firmware");
const hostRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
const sensor = sensorMediaForProject(fw.project);
const hasUi = Boolean(fw.media.preview);
const [tab, setTab] = useState<Tab>(hasUi ? "ui" : "sensor");
useEffect(() => {
setTab(hasUi ? "ui" : sensor ? "sensor" : "ui");
}, [fw.id, hasUi, sensor]);
useEffect(() => {
const el = hostRef.current;
if (!el) return;
if (!el || tab !== "ui") return;
const update = () => {
const { width, height } = el.getBoundingClientRect();
@@ -34,34 +46,9 @@ export function FirmwarePreview({
const ro = new ResizeObserver(update);
ro.observe(el);
return () => ro.disconnect();
}, [fw.media.preview]);
}, [fw.media.preview, tab]);
if (fw.media.preview) {
return (
<div
ref={hostRef}
className={cn(
"relative h-full w-full overflow-hidden bg-[#0A1016]",
className
)}
>
<iframe
title={title || `${fw.name} UI preview`}
src={fw.media.preview}
width={SCREEN_W}
height={SCREEN_H}
loading="lazy"
sandbox="allow-scripts"
tabIndex={-1}
className="pointer-events-none absolute left-1/2 top-1/2 border-0 bg-[#0A1016]"
style={{
transform: `translate(-50%, -50%) scale(${scale})`,
transformOrigin: "center center",
}}
/>
</div>
);
}
const showTabs = hasUi && Boolean(sensor);
const initials = fw.name
.replace(/[^A-Za-z0-9\u4e00-\u9fff]+/g, " ")
@@ -74,16 +61,95 @@ export function FirmwarePreview({
return (
<div
className={cn(
"flex h-full w-full flex-col items-center justify-center gap-2 bg-[radial-gradient(ellipse_at_30%_20%,rgba(240,197,10,0.18),transparent_55%),linear-gradient(160deg,#1a1610,#0e0c09)]",
"relative flex h-full w-full flex-col overflow-hidden bg-[#0A1016]",
className
)}
>
<span className="font-[family-name:var(--font-display)] text-3xl font-extrabold tracking-tight text-[var(--accent-hot)]/80">
{initials || "FW"}
</span>
<span className="text-[0.65rem] uppercase tracking-[0.14em] text-[var(--muted)]">
HTML preview soon
</span>
{showTabs && (
<div
className="absolute left-2 top-2 z-10 flex gap-0.5 rounded-md bg-black/60 p-0.5 backdrop-blur-sm"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<button
type="button"
className={cn(
"rounded px-2 py-0.5 text-[0.65rem] font-semibold tracking-wide transition-colors",
tab === "ui"
? "bg-[var(--accent)] text-black"
: "text-[var(--ink)]/80 hover:text-[var(--ink)]"
)}
onClick={(e) => {
e.stopPropagation();
setTab("ui");
}}
>
{t("tabUi")}
</button>
<button
type="button"
className={cn(
"rounded px-2 py-0.5 text-[0.65rem] font-semibold tracking-wide transition-colors",
tab === "sensor"
? "bg-[var(--accent)] text-black"
: "text-[var(--ink)]/80 hover:text-[var(--ink)]"
)}
onClick={(e) => {
e.stopPropagation();
setTab("sensor");
}}
>
{t("tabSensor")}
</button>
</div>
)}
<div className="relative min-h-0 flex-1">
{tab === "ui" && hasUi ? (
<div
ref={hostRef}
className="relative h-full w-full overflow-hidden bg-[#0A1016]"
>
<iframe
title={title || `${fw.name} UI preview`}
src={fw.media.preview!}
width={SCREEN_W}
height={SCREEN_H}
loading="lazy"
sandbox="allow-scripts"
tabIndex={-1}
className="pointer-events-none absolute left-1/2 top-1/2 border-0 bg-[#0A1016]"
style={{
transform: `translate(-50%, -50%) scale(${scale})`,
transformOrigin: "center center",
}}
/>
</div>
) : tab === "sensor" && sensor ? (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-[radial-gradient(ellipse_at_50%_30%,rgba(240,197,10,0.08),transparent_60%),#0A1016] p-3">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={sensor.src}
alt={sensor.label}
className="max-h-[78%] max-w-full object-contain drop-shadow-[0_8px_24px_rgba(0,0,0,0.45)]"
loading="lazy"
draggable={false}
/>
<span className="text-center text-[0.7rem] font-medium tracking-wide text-[var(--muted)]">
{sensor.label}
</span>
</div>
) : (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-[radial-gradient(ellipse_at_30%_20%,rgba(240,197,10,0.18),transparent_55%),linear-gradient(160deg,#1a1610,#0e0c09)]">
<span className="font-[family-name:var(--font-display)] text-3xl font-extrabold tracking-tight text-[var(--accent-hot)]/80">
{initials || "FW"}
</span>
<span className="text-[0.65rem] uppercase tracking-[0.14em] text-[var(--muted)]">
HTML preview soon
</span>
</div>
)}
</div>
</div>
);
}

86
lib/sensor-media.ts Normal file
View File

@@ -0,0 +1,86 @@
/**
* Hardware / sensor stills shown in the preview “Hardware” tab.
* Prefer Wikimedia Commons (CC) or original studio stubs — see public/sensors/ATTRIBUTION.md
*/
export type SensorMedia = {
/** Short part name shown under the photo */
label: string;
/** Public URL under /sensors/… */
src: string;
};
/** Map firmware project folder name → sensor/module still. */
const BY_PROJECT: Record<string, SensorMedia> = {
temperature: {
label: "DHT11 / DHT22",
src: "/sensors/dht.jpg",
},
gy30test: {
label: "GY-30 / BH1750",
src: "/sensors/bh1750.jpg",
},
sgp30test: {
label: "SGP30",
src: "/sensors/sgp30.jpg",
},
ld2420test: {
label: "LD2420 mmWave",
src: "/sensors/ld2420.jpg",
},
phtotest: {
label: "microSD / TF",
src: "/sensors/microsd.jpg",
},
tftest: {
label: "Speaker (GPIO26)",
src: "/sensors/speaker.jpg",
},
activeBuzzerTest: {
label: "Active buzzer",
src: "/sensors/buzzer.jpg",
},
btntest: {
label: "Tactile button",
src: "/sensors/button.jpg",
},
touchbtntest: {
label: "TTP223 touch",
src: "/sensors/ttp223.jpg",
},
humanInfraredModuletest: {
label: "PIR SR505 / SR501",
src: "/sensors/pir.jpg",
},
voicechecktest: {
label: "Whistle / sound (digital)",
src: "/sensors/sound_sensor.jpg",
},
hw069digitdisplaytest: {
label: "HW-069 / TM1637",
src: "/sensors/tm1637.jpg",
},
hw269test: {
label: "HW-269 white LED",
src: "/sensors/white_led.jpg",
},
smdky009ledtest: {
label: "KY-009 RGB LED",
src: "/sensors/rgb.jpg",
},
lcd1602atest: {
label: "LCD1602A + PCF8574",
src: "/sensors/lcd1602.jpg",
},
bletest: {
label: "Onboard BLE (ESP32)",
src: "/sensors/bluetooth_icon.svg",
},
wifitest: {
label: "Onboard Wi‑Fi (ESP32)",
src: "/sensors/wifi_icon.svg",
},
};
export function sensorMediaForProject(project: string): SensorMedia | null {
return BY_PROJECT[project] ?? null;
}

View File

@@ -36,7 +36,9 @@
"noneSelected": "Select a firmware card to see details and flash",
"selectedLabel": "Selected",
"docs": "Documentation",
"mediaPlaceholder": "On-device HTML UI preview will appear here"
"mediaPlaceholder": "On-device HTML UI preview will appear here",
"tabUi": "UI",
"tabSensor": "Hardware"
},
"install": {
"cta": "Connect & flash",

View File

@@ -36,7 +36,9 @@
"noneSelected": "点选固件卡片查看详情并刷写",
"selectedLabel": "已选",
"docs": "文档",
"mediaPlaceholder": "设备界面 HTML 预览将显示在这里"
"mediaPlaceholder": "设备界面 HTML 预览将显示在这里",
"tabUi": "界面",
"tabSensor": "硬件"
},
"install": {
"cta": "连接并刷写",

View File

@@ -0,0 +1,36 @@
# Sensor / hardware stills — attribution
Images under `public/sensors/` are used only for the flasher catalog “Hardware” tab.
## Wikimedia Commons (Creative Commons)
| File | Source file on Commons | License (typical) |
|------|------------------------|-------------------|
| `dht.jpg` | AM2302 (DHT22) digital temperature and humidity sensor module | CC BY-SA (see Commons page) |
| `pir.jpg` | PIR motion sensor for Arduino oblique view | CC (see Commons page) |
| `lcd1602.jpg` | 16x2 Character LCD Display | CC (see Commons page) |
| `button.jpg` | Tactile switches / related Commons photo | CC (see Commons page) |
| `microsd.jpg` / `sd.jpg` | MicroSD / SD card Commons photos | CC (see Commons page) |
| `speaker.jpg` | Loudspeaker / speaker Commons photo | CC (see Commons page) |
| `rgb.jpg` | RGB LED Commons photo | CC (see Commons page) |
| `led.jpg` | Red LED Commons photo | CC (see Commons page) |
| `mic.jpg` | Microphone Commons photo | CC (see Commons page) |
| `wifi_icon.svg` | Wi-Fi Logo.svg | Trademarked logo; used for identification of onboard Wi‑Fi only |
| `bluetooth_icon.svg` | Bluetooth.svg | Trademarked logo; used for identification of onboard BLE only |
Full license text and author credit: open the matching file page on https://commons.wikimedia.org/
## Original studio stubs (no third-party copyright)
Generated for this project (no brand marks):
- `bh1750.jpg` — GY-30 / BH1750 style breakout
- `sgp30.jpg` — SGP30 style breakout
- `ld2420.jpg` — LD2420 style mmWave module
- `tm1637.jpg` — TM1637 / HW-069 style 4-digit display
- `buzzer.jpg` / `active_buzzer` source — active buzzer module
- `white_led.jpg` — HW-269 style white LED module
- `ttp223.jpg` — TTP223 style capacitive touch module
- `sound_sensor.jpg` — digital whistle / sound module
These are illustrative look-alikes, not manufacturer product photos.

BIN
public/sensors/bh1750.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/sensors/button.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
public/sensors/buzzer.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

BIN
public/sensors/dht.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
public/sensors/lcd1602.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
public/sensors/ld2420.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
public/sensors/led.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
public/sensors/mic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
public/sensors/microsd.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
public/sensors/pir.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
public/sensors/rgb.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
public/sensors/sd.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

BIN
public/sensors/sgp30.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

BIN
public/sensors/speaker.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
public/sensors/tm1637.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
public/sensors/ttp223.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB