feat: add WT32-SC01 LCD support + dynamic display resolution
- Add WT32-SC01 V1 (ESP32-WROVER, 480x320 LCD) as second board option - Split ESP32 code into epaper/ and lcd/ subdirectories - LCD version: LovyanGFX, color display, touch-to-refresh, no deep sleep - Add display_type, width, height to displays table (migration 003) - Frontend: display type selector with predefined resolutions - Preview and layout editor now use dynamic resolution from display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -143,7 +143,7 @@ export default function EpaperPreview({ layoutId, elements, layoutWidth = 800, l
|
||||
if (el.type === 'line') {
|
||||
return <div key={idx} style={{
|
||||
position: 'absolute', left, top,
|
||||
width: (el.width || 800) * SCALE,
|
||||
width: (el.width || layoutWidth) * SCALE,
|
||||
height: Math.max(1, (el.height || 2) * SCALE),
|
||||
backgroundColor: '#000',
|
||||
}} />;
|
||||
|
||||
@@ -5,6 +5,9 @@ import { api } from '../api/client';
|
||||
interface Display {
|
||||
id: number;
|
||||
name: string;
|
||||
display_type: string;
|
||||
width: number;
|
||||
height: number;
|
||||
layout_id: number | null;
|
||||
api_token: string | null;
|
||||
last_seen: string | null;
|
||||
@@ -12,23 +15,27 @@ interface Display {
|
||||
}
|
||||
|
||||
interface Layout { id: number; name: string }
|
||||
interface DisplayType { width: number; height: number; label: string }
|
||||
|
||||
const emptyForm = { name: '', layout_id: '', refresh_sec: '300' };
|
||||
const emptyForm = { name: '', display_type: 'epaper-7.5', layout_id: '', refresh_sec: '300' };
|
||||
|
||||
export default function Displays() {
|
||||
const [displays, setDisplays] = useState<Display[]>([]);
|
||||
const [layouts, setLayouts] = useState<Layout[]>([]);
|
||||
const [types, setTypes] = useState<Record<string, DisplayType>>({});
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const [d, l] = await Promise.all([
|
||||
const [d, l, t] = await Promise.all([
|
||||
api.get<Display[]>('/api/displays'),
|
||||
api.get<Layout[]>('/api/layouts'),
|
||||
api.get<Record<string, DisplayType>>('/api/displays/types'),
|
||||
]);
|
||||
setDisplays(d);
|
||||
setLayouts(l);
|
||||
setTypes(t);
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
}
|
||||
@@ -40,8 +47,12 @@ export default function Displays() {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
try {
|
||||
const dtype = types[form.display_type];
|
||||
await api.post('/api/displays', {
|
||||
name: form.name,
|
||||
display_type: form.display_type,
|
||||
width: dtype?.width ?? 800,
|
||||
height: dtype?.height ?? 480,
|
||||
layout_id: form.layout_id ? parseInt(form.layout_id) : null,
|
||||
refresh_sec: parseInt(form.refresh_sec),
|
||||
});
|
||||
@@ -61,6 +72,20 @@ export default function Displays() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTypeChange(id: number, display_type: string) {
|
||||
try {
|
||||
const dtype = types[display_type];
|
||||
await api.put(`/api/displays/${id}`, {
|
||||
display_type,
|
||||
width: dtype?.width ?? 800,
|
||||
height: dtype?.height ?? 480,
|
||||
});
|
||||
load();
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
if (!confirm('Display wirklich loeschen?')) return;
|
||||
try {
|
||||
@@ -81,6 +106,7 @@ export default function Displays() {
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Display-Typ</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Layout</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Refresh (s)</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">API URL</th>
|
||||
@@ -90,11 +116,23 @@ export default function Displays() {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{displays.length === 0 && (
|
||||
<tr><td colSpan={6} className="px-4 py-6 text-center text-gray-400">Keine Displays konfiguriert</td></tr>
|
||||
<tr><td colSpan={7} className="px-4 py-6 text-center text-gray-400">Keine Displays konfiguriert</td></tr>
|
||||
)}
|
||||
{displays.map((d) => (
|
||||
<tr key={d.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-2 font-medium text-gray-800">{d.name}</td>
|
||||
<td className="px-4 py-2">
|
||||
<select
|
||||
className="border border-gray-300 rounded px-2 py-1 text-xs"
|
||||
value={d.display_type}
|
||||
onChange={(e) => handleTypeChange(d.id, e.target.value)}
|
||||
>
|
||||
{Object.entries(types).map(([key, t]) => (
|
||||
<option key={key} value={key}>{t.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="ml-2 text-xs text-gray-400">{d.width}x{d.height}</span>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<select
|
||||
className="border border-gray-300 rounded px-2 py-1 text-xs"
|
||||
@@ -133,6 +171,18 @@ export default function Displays() {
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Display-Typ</label>
|
||||
<select
|
||||
className="border border-gray-300 rounded px-3 py-1.5 text-sm"
|
||||
value={form.display_type}
|
||||
onChange={(e) => setForm({ ...form, display_type: e.target.value })}
|
||||
>
|
||||
{Object.entries(types).map(([key, t]) => (
|
||||
<option key={key} value={key}>{t.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Layout</label>
|
||||
<select
|
||||
|
||||
@@ -358,7 +358,7 @@ export default function Layouts() {
|
||||
</div>
|
||||
|
||||
{/* E-paper preview */}
|
||||
<h4 className="text-sm font-medium text-gray-600 mb-2">Vorschau (800x480)</h4>
|
||||
<h4 className="text-sm font-medium text-gray-600 mb-2">Vorschau ({selected.width}x{selected.height})</h4>
|
||||
<EpaperPreview
|
||||
layoutId={selected.id}
|
||||
elements={selected.elements}
|
||||
|
||||
Reference in New Issue
Block a user