diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js
index 9e665b2..a643255 100644
--- a/frontend/js/i18n/en.js
+++ b/frontend/js/i18n/en.js
@@ -1013,6 +1013,11 @@ export default {
'layout.prop.type': 'Type',
'layout.type_content': 'Content',
'layout.type_widget': 'Widget',
+ 'layout.prop.fit': 'Fit',
+ 'layout.fit_contain': 'Contain (whole image, may letterbox)',
+ 'layout.fit_cover': 'Cover (fill zone, may crop)',
+ 'layout.fit_fill': 'Stretch (fill zone, may distort)',
+ 'layout.fit_hint': 'How video/images scale to the zone. Contain shows the whole frame without cropping.',
// Video walls
'wall.title': 'Video Walls',
diff --git a/frontend/js/views/layout-editor.js b/frontend/js/views/layout-editor.js
index 01d0de0..33bfe5c 100644
--- a/frontend/js/views/layout-editor.js
+++ b/frontend/js/views/layout-editor.js
@@ -144,6 +144,14 @@ async function renderEditor(container, layoutId) {
+
@@ -250,9 +258,10 @@ async function renderEditor(container, layoutId) {
document.getElementById('propW').value = z.width_percent;
document.getElementById('propH').value = z.height_percent;
document.getElementById('propType').value = z.zone_type;
+ document.getElementById('propFit').value = z.fit_mode || 'cover';
}
- ['propName', 'propX', 'propY', 'propW', 'propH', 'propType'].forEach(id => {
+ ['propName', 'propX', 'propY', 'propW', 'propH', 'propType', 'propFit'].forEach(id => {
document.getElementById(id).oninput = () => {
if (selectedZone === null) return;
const z = zones[selectedZone];
@@ -262,12 +271,13 @@ async function renderEditor(container, layoutId) {
z.width_percent = parseFloat(document.getElementById('propW').value) || 10;
z.height_percent = parseFloat(document.getElementById('propH').value) || 10;
z.zone_type = document.getElementById('propType').value;
+ z.fit_mode = document.getElementById('propFit').value;
renderZones();
};
});
document.getElementById('addZoneBtn').onclick = () => {
- zones.push({ id: null, name: t('layout.zone_n', { n: zones.length + 1 }), x_percent: 10, y_percent: 10, width_percent: 30, height_percent: 30, z_index: 0, zone_type: 'content', fit_mode: 'cover', background_color: '#000000', sort_order: zones.length });
+ zones.push({ id: null, name: t('layout.zone_n', { n: zones.length + 1 }), x_percent: 10, y_percent: 10, width_percent: 30, height_percent: 30, z_index: 0, zone_type: 'content', fit_mode: 'contain', background_color: '#000000', sort_order: zones.length });
selectedZone = zones.length - 1;
renderZones();
updateProperties();
diff --git a/server/db/schema.sql b/server/db/schema.sql
index 4575128..8395854 100644
--- a/server/db/schema.sql
+++ b/server/db/schema.sql
@@ -141,7 +141,7 @@ CREATE TABLE IF NOT EXISTS layout_zones (
height_percent REAL NOT NULL DEFAULT 100,
z_index INTEGER NOT NULL DEFAULT 0,
zone_type TEXT NOT NULL DEFAULT 'content',
- fit_mode TEXT NOT NULL DEFAULT 'cover',
+ fit_mode TEXT NOT NULL DEFAULT 'contain',
background_color TEXT DEFAULT '#000000',
sort_order INTEGER NOT NULL DEFAULT 0
);
diff --git a/server/routes/layouts.js b/server/routes/layouts.js
index 837f945..7653aa6 100644
--- a/server/routes/layouts.js
+++ b/server/routes/layouts.js
@@ -101,7 +101,7 @@ router.post('/', (req, res) => {
zones.forEach((z, i) => {
stmt.run(uuidv4(), id, z.name || `Zone ${i + 1}`, z.x_percent || 0, z.y_percent || 0,
z.width_percent || 100, z.height_percent || 100, z.z_index || 0,
- z.zone_type || 'content', z.fit_mode || 'cover', z.background_color || '#000000', i);
+ z.zone_type || 'content', z.fit_mode || 'contain', z.background_color || '#000000', i);
});
}
@@ -151,7 +151,7 @@ router.post('/:id/zones', (req, res) => {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, req.params.id, name || 'New Zone', x_percent || 0, y_percent || 0,
width_percent || 50, height_percent || 50, z_index || 0,
- zone_type || 'content', fit_mode || 'cover', background_color || '#000000', maxOrder + 1);
+ zone_type || 'content', fit_mode || 'contain', background_color || '#000000', maxOrder + 1);
db.prepare("UPDATE layouts SET updated_at = strftime('%s','now') WHERE id = ?").run(req.params.id);