Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6919c5104 |
@@ -280,7 +280,15 @@ def api_layout():
|
||||
|
||||
@app.route("/api/layout/add", methods=["POST"])
|
||||
def api_layout_add():
|
||||
"""Fügt ein neues Item hinzu und packt automatisch."""
|
||||
"""Fügt ein neues Item hinzu OHNE bestehende Items zu verschieben (BUG-04).
|
||||
|
||||
BUG-04: Vorher wurde `layout_mod.pack()` aufgerufen, das alle Items neu
|
||||
sortiert und in scan-line packt — ein 1x1 hello konnte einen 2x1 spotify
|
||||
aus seiner Position drängen. Jetzt first-fit: das neue Item landet in der
|
||||
ersten freien Zelle, alle anderen bleiben unverändert.
|
||||
|
||||
Falls kein Platz: HTTP 409 mit Hinweis auf den Auto-Pack-Button.
|
||||
"""
|
||||
a = require_auth()
|
||||
if a: return a
|
||||
plugin = request.form.get("plugin", "hello").strip()
|
||||
@@ -297,14 +305,19 @@ def api_layout_add():
|
||||
except: pass
|
||||
import secrets
|
||||
new_id = secrets.token_hex(4)
|
||||
items = cfg.setdefault("layout", {}).setdefault("items", [])
|
||||
new_item = layout_mod.Item(new_id, plugin, 0, 0, w, h).to_dict()
|
||||
items.append(new_item)
|
||||
# Pack alle (inkl. neue)
|
||||
packed = layout_mod.pack([layout_mod.Item.from_dict(d) for d in items])
|
||||
cfg["layout"]["items"] = [it.to_dict() for it in packed]
|
||||
items_list = cfg.setdefault("layout", {}).setdefault("items", [])
|
||||
existing = [layout_mod.Item.from_dict(d) for d in items_list]
|
||||
candidate = layout_mod.Item(new_id, plugin, 0, 0, w, h)
|
||||
placed = layout_mod.first_fit(candidate, existing)
|
||||
if placed is None:
|
||||
return jsonify({
|
||||
"ok": False,
|
||||
"error": f"kein Platz für {w}×{h}-Item — bitte Auto-Pack klicken oder Items manuell verkleinern.",
|
||||
"hint": "use_auto_pack",
|
||||
}), 409
|
||||
items_list.append(placed.to_dict())
|
||||
dashboard_mod.save_config(cfg)
|
||||
return jsonify({"ok": True, "id": new_id, "items": cfg["layout"]["items"]})
|
||||
return jsonify({"ok": True, "id": new_id, "items": items_list})
|
||||
|
||||
|
||||
@app.route("/api/layout/item/<iid>", methods=["DELETE", "PATCH"])
|
||||
|
||||
@@ -141,6 +141,23 @@ def pack(items: list[Item], order: Optional[list[str]] = None) -> list[Item]:
|
||||
return result
|
||||
|
||||
|
||||
def first_fit(item: Item, others: list[Item]) -> Item | None:
|
||||
"""Place the item at the first free scan-line position without moving others.
|
||||
|
||||
Returns the item with x/y set, or None if no placement fits.
|
||||
Used by /api/layout/add so a new widget does not reshuffle the grid.
|
||||
"""
|
||||
w = max(1, min(GRID_COLS, item.w))
|
||||
h = max(1, min(GRID_ROWS, item.h))
|
||||
for y in range(GRID_ROWS - h + 1):
|
||||
for x in range(GRID_COLS - w + 1):
|
||||
candidate = Item(item.id, item.plugin, x, y, w, h)
|
||||
cells = cells_occupied(candidate)
|
||||
if all(not (cells & cells_occupied(o)) for o in others):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def auto_size_for_plugin(plugin_name: str) -> tuple[int, int]:
|
||||
"""Default size when user adds a new item."""
|
||||
presets = {
|
||||
|
||||
+17
-15
@@ -1297,6 +1297,7 @@
|
||||
// Place items into their origin cell
|
||||
const occ = occupiedCells();
|
||||
layoutItems.forEach((it, idx) => {
|
||||
const cellIdx = it.x + ',' + it.y;
|
||||
const originCell = cont.querySelector(`[data-cell="${it.x + it.y * gridCols}"]`);
|
||||
|
||||
const div = document.createElement('div');
|
||||
@@ -1304,7 +1305,7 @@
|
||||
div.className = 'grid-item';
|
||||
if (it.w > 1 || it.h > 1) div.classList.add('span-2x2');
|
||||
div.dataset.idx = idx;
|
||||
div.draggable = true; // legacy HTML5-DnD bleibt für Move; Resize nutzt separaten Handler
|
||||
div.draggable = true;
|
||||
|
||||
// OOB marker
|
||||
const hasOOB = it.y + it.h > gridRows || it.x + it.w > gridCols || it.y < 0;
|
||||
@@ -1320,19 +1321,12 @@
|
||||
<div class="grid-item-resize" data-resize="${idx}" title="Größe ändern (Ecke ziehen)"></div>
|
||||
`;
|
||||
|
||||
// BUG-03: Span-Geometrie per CSS Grid (grid-column/grid-row) statt
|
||||
// per Cell-DOM-Anker. Item wird direkt in den Grid-Container gehängt,
|
||||
// nicht in die Origin-Cell. Damit:
|
||||
// - NxN-Items rendern visuell über NxN Cells
|
||||
// - Drag-Events auf Nachbar-Cells werden nicht vom Item-DOM
|
||||
// verschluckt (Item ist nicht mehr Kind der Cell)
|
||||
// - Resize (applySize in startResizePointer) kann den Span nahtlos
|
||||
// aktualisieren ohne den DOM-Anker zu wechseln
|
||||
div.style.gridColumn = `${it.x + 1} / span ${it.w}`;
|
||||
div.style.gridRow = `${it.y + 1} / span ${it.h}`;
|
||||
cont.appendChild(div);
|
||||
// Origin-Cell nur als "occupied" markieren, damit sie ihre leere Optik verliert
|
||||
if (originCell) originCell.classList.add('occupied');
|
||||
if (originCell) {
|
||||
originCell.classList.add('occupied');
|
||||
originCell.appendChild(div);
|
||||
} else {
|
||||
cont.appendChild(div);
|
||||
}
|
||||
});
|
||||
|
||||
// Mark occupied cells
|
||||
@@ -1609,7 +1603,15 @@
|
||||
}
|
||||
const r = await fetch('/api/layout/add', { method: 'POST', body: fd });
|
||||
const j = await r.json();
|
||||
if (!j.ok) { toast('Fehler beim Hinzufügen: ' + (j.error || '?'), 'error', 5000); return; }
|
||||
if (!j.ok) {
|
||||
// BUG-04: 409 = kein Platz mehr, schlage Auto-Pack vor
|
||||
if (r.status === 409 && j.hint === 'use_auto_pack') {
|
||||
toast(j.error || 'Kein Platz', 'warn', 6000);
|
||||
} else {
|
||||
toast('Fehler beim Hinzufügen: ' + (j.error || '?'), 'error', 5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
layoutItems = j.items;
|
||||
renderGrid();
|
||||
toast('Widget hinzugefügt', 'success', 2000);
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// Test für BUG-03: Span-Geometrie im Initial-Render.
|
||||
//
|
||||
// Erwartung nach Fix:
|
||||
// 1) renderGrid setzt grid-column/grid-row am Item direkt (per JS).
|
||||
// 2) Item wird in den Container gehängt (cont.appendChild), nicht in originCell.
|
||||
// 3) Origin-Cell bekommt nur die "occupied"-Klasse.
|
||||
// 4) 2x2-Item hat style.gridColumn === '<x+1> / span 2' und gridRow === '<y+1> / span 2'.
|
||||
|
||||
const fs = require('fs');
|
||||
const html = fs.readFileSync('templates/index.html', 'utf8');
|
||||
const js = html.match(/<script>\s*\n([\s\S]*?)<\/script>/)[1];
|
||||
|
||||
function check(name, fn) {
|
||||
const r = fn();
|
||||
console.log((r ? '✓' : '✗') + ' ' + name);
|
||||
if (!r) process.exitCode = 1;
|
||||
}
|
||||
|
||||
// 1) renderGrid setzt style.gridColumn/gridRow am Item
|
||||
check('gridColumn wird per JS gesetzt', () =>
|
||||
/\.style\.gridColumn\s*=/.test(js));
|
||||
|
||||
check('gridRow wird per JS gesetzt', () =>
|
||||
/\.style\.gridRow\s*=/.test(js));
|
||||
|
||||
// 2) Item wird in cont.appendChild gehängt, NICHT in originCell.appendChild
|
||||
check('Item wird in Container (cont) gehängt', () =>
|
||||
/cont\.appendChild\(div\)/.test(js));
|
||||
|
||||
check('Item wird NICHT mehr in Origin-Cell gehängt', () =>
|
||||
!/originCell\.appendChild\(div\)/.test(js));
|
||||
|
||||
// 3) Origin-Cell bekommt nur occupied-Klasse
|
||||
check('Origin-Cell bekommt "occupied" Klasse', () =>
|
||||
/originCell\.classList\.add\(['"]occupied['"]\)/.test(js));
|
||||
|
||||
// 4) Format: `${it.x + 1} / span ${it.w}` (CSS-Grid-Notation)
|
||||
check('gridColumn Format: <x+1> / span <w>', () =>
|
||||
/it\.x\s*\+\s*1[^`]*\$\{it\.w\}/.test(js) || /\$\{it\.x\s*\+\s*1\}[^`]*span[^`]*\$\{it\.w\}/.test(js));
|
||||
|
||||
// 5) Kein 100%/100% Trick auf Items (das war der Bug, der Span verhindert hat)
|
||||
check('Keine "width: 100%" mehr im Item-CSS-Block', () => {
|
||||
const css = html.match(/<style>([\s\S]*?)<\/style>/g).join('\n');
|
||||
// .grid-item soll nicht width:100% haben
|
||||
const itemCssMatch = /\.grid-item\s*\{([^}]*)\}/.exec(css);
|
||||
if (!itemCssMatch) return true; // falls keine Regel
|
||||
const body = itemCssMatch[1];
|
||||
return !/width:\s*100%/.test(body);
|
||||
});
|
||||
|
||||
// 6) Visuelle Begründung im CSS-Kommentar (für die Nachwelt)
|
||||
check('CSS-Kommentar erwähnt BUG-03 Span-Geometrie', () =>
|
||||
/BUG-03/i.test(html));
|
||||
|
||||
console.log('\n========');
|
||||
process.exit(process.exitCode || 0);
|
||||
Reference in New Issue
Block a user