refactor(homarr): extract layout packing to filter plugin

This commit is contained in:
Tobias Wüst 2026-05-19 11:19:29 +02:00
parent e0cb1ac68c
commit 61193e26f4
Signed by: Tobias-Wuest
GPG key ID: 2D8992B0F4CA97E8
6 changed files with 405 additions and 58 deletions

View file

@ -1,35 +1,13 @@
{#-
Auto-layout packing macro.
Homarr database seed.
Greedy left-to-right packing of apps into a grid with `cols` columns.
Returns the list of apps with computed x/y/w/h fields.
Width is clamped to cols (so an app wider than the grid is downsized
rather than overflowing). Height is taken as-is.
The packing algorithm previously lived in this template as a Jinja
`pack()` macro with from_json/to_json round-trips. It has been
extracted to the `homarr_compute_layouts` filter plugin (see
filter_plugins/homarr_layout.py) and the result is provided as the
`homarr_layout` fact set in tasks/main.yml. This template therefore
only renders SQL — no logic.
-#}
{%- macro pack(apps, cols) -%}
{%- set ns = namespace(x=0, y=0, row_h=0, out=[]) -%}
{%- for app in apps -%}
{%- set w = [app.width, cols] | min -%}
{%- set h = app.height | default(1) -%}
{%- if ns.x + w > cols -%}
{%- set ns.x = 0 -%}
{%- set ns.y = ns.y + ns.row_h -%}
{%- set ns.row_h = 0 -%}
{%- endif -%}
{%- set _ = ns.out.append({'id': app.id, 'x': ns.x, 'y': ns.y, 'w': w, 'h': h}) -%}
{%- set ns.x = ns.x + w -%}
{%- if h > ns.row_h -%}
{%- set ns.row_h = h -%}
{%- endif -%}
{%- endfor -%}
{{- ns.out | to_json -}}
{%- endmacro -%}
{%- set desktop_layout = pack(homarr_apps, 10) | from_json -%}
{%- set tablet_layout = pack(homarr_apps, 6) | from_json -%}
{%- set mobile_layout = pack(homarr_apps, 2) | from_json -%}
BEGIN TRANSACTION;
-- =====================================================================
@ -148,11 +126,14 @@ VALUES (
'{"json": {}}'
);
-- Section height is sized to fit the computed layout (see
-- homarr_compute_layouts filter). It grows automatically when more
-- apps or taller tiles are added.
INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height)
VALUES
('section-apps', 'layout-desktop', NULL, 0, 0, 10, 3),
('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4),
('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6);
('section-apps', 'layout-desktop', NULL, 0, 0, 10, {{ homarr_layout.section_height.desktop }}),
('section-apps', 'layout-tablet', NULL, 0, 0, 6, {{ homarr_layout.section_height.tablet }}),
('section-apps', 'layout-mobile', NULL, 0, 0, 2, {{ homarr_layout.section_height.mobile }});
-- Board permissions
INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission)
@ -161,11 +142,11 @@ VALUES
('board-default', 'group-credentials-admin', 'full-access');
-- =====================================================================
-- APPS (auto-generated from homarr_apps variable)
-- APPS (positions pre-computed by homarr_compute_layouts filter)
-- =====================================================================
{% if homarr_apps | length > 0 %}
{% for app in homarr_apps %}
{% for app in homarr_layout.apps %}
-- {{ app.name }}
INSERT OR IGNORE INTO app (id, name, description, icon_url, href)
VALUES (
'app-{{ app.id }}',
@ -184,28 +165,11 @@ VALUES (
'{"json": {}}'
);
{% endfor %}
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
{% for entry in desktop_layout %}
('item-{{ entry.id }}', 'section-apps', 'layout-desktop', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %}
{% endfor %}
;
('item-{{ app.id }}', 'section-apps', 'layout-desktop', {{ app.desktop.x }}, {{ app.desktop.y }}, {{ app.desktop.w }}, {{ app.desktop.h }}),
('item-{{ app.id }}', 'section-apps', 'layout-tablet', {{ app.tablet.x }}, {{ app.tablet.y }}, {{ app.tablet.w }}, {{ app.tablet.h }}),
('item-{{ app.id }}', 'section-apps', 'layout-mobile', {{ app.mobile.x }}, {{ app.mobile.y }}, {{ app.mobile.w }}, {{ app.mobile.h }});
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
{% for entry in tablet_layout %}
('item-{{ entry.id }}', 'section-apps', 'layout-tablet', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %}
{% endfor %}
;
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
{% for entry in mobile_layout %}
('item-{{ entry.id }}', 'section-apps', 'layout-mobile', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %}
{% endfor %}
;
{% endif %}
COMMIT;
COMMIT;