mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 17:56:34 +00:00
feat: per-cell content fit (cover|contain|fill), default cover
- Migration adds layout_cells.fit column (default 'cover') - LayoutCell type + mapper + repo accept/persist fit - Bundle ships fit per cell - Admin cell edit form: Fit dropdown with industry-default Cover - Rust kiosk applies ContentFit::Cover|Contain|Fill per cell.fit Cover = fill cell, crop overflow (industry default — Nx Witness etc) Contain = letterbox, no crop Fill = stretch, distort
This commit is contained in:
parent
9679ae7eb1
commit
1e09582379
9 changed files with 36 additions and 4 deletions
|
|
@ -47,8 +47,12 @@ pub struct BundleCell {
|
|||
pub web_url: Option<String>,
|
||||
pub html_content: Option<String>,
|
||||
pub cooling_timeout_seconds: Option<u32>,
|
||||
#[serde(default = "default_fit")]
|
||||
pub fit: String,
|
||||
}
|
||||
|
||||
fn default_fit() -> String { "cover".to_string() }
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct BundleCamera {
|
||||
pub id: u32,
|
||||
|
|
|
|||
|
|
@ -256,7 +256,11 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle, server_url: &s
|
|||
let area = (cell.col_span * cell.row_span) as f32 / total_area;
|
||||
if let Some((paintable, badge)) = ensure_warm(cam_id, cam, cell.stream_selector.as_deref(), area) {
|
||||
let picture = Picture::for_paintable(&paintable);
|
||||
picture.set_content_fit(gtk::ContentFit::Contain);
|
||||
picture.set_content_fit(match cell.fit.as_str() {
|
||||
"contain" => gtk::ContentFit::Contain,
|
||||
"fill" => gtk::ContentFit::Fill,
|
||||
_ => gtk::ContentFit::Cover,
|
||||
});
|
||||
picture.set_vexpand(true);
|
||||
picture.set_hexpand(true);
|
||||
// Wrap in Overlay so we can stack a stream-role badge on top
|
||||
|
|
|
|||
|
|
@ -748,12 +748,16 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
entityIdRaw && String(entityIdRaw).trim() !== "" ? Number(entityIdRaw) : null;
|
||||
deps.repo.assignCellEntity(cellId, Number.isFinite(entityId) ? entityId : null);
|
||||
|
||||
// stream_selector + spans are still settable on the cell.
|
||||
// stream_selector + spans + fit are still settable on the cell.
|
||||
const dimsPatch: Record<string, unknown> = {};
|
||||
const streamSelector = body?.["stream_selector"];
|
||||
if (streamSelector === "auto" || streamSelector === "main" || streamSelector === "sub") {
|
||||
dimsPatch["stream_selector"] = streamSelector;
|
||||
}
|
||||
const fit = body?.["fit"];
|
||||
if (fit === "cover" || fit === "contain" || fit === "fill") {
|
||||
dimsPatch["fit"] = fit;
|
||||
}
|
||||
const colSpanRaw = body?.["col_span"];
|
||||
const rowSpanRaw = body?.["row_span"];
|
||||
if (colSpanRaw != null && String(colSpanRaw).trim() !== "") {
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ export function rowToLayoutCell(r: Row): LayoutCell {
|
|||
cooling_timeout_seconds: nn(r["cooling_timeout_seconds"]),
|
||||
options: j<Record<string, unknown>>(r["options"], {}),
|
||||
entity_id: nn(r["entity_id"]),
|
||||
fit: (s(r["fit"]) || "cover") as "cover" | "contain" | "fill",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -596,4 +596,9 @@ export const MIGRATIONS: readonly MigrationEntry[] = [
|
|||
addColumnIfNotExists(db, "kiosks", "fan_rpm", "INTEGER");
|
||||
addColumnIfNotExists(db, "kiosks", "fan_pwm", "INTEGER");
|
||||
},
|
||||
|
||||
// ---- per-cell content fit (cover|contain|fill) ----
|
||||
(db: DatabaseSync) => {
|
||||
addColumnIfNotExists(db, "layout_cells", "fit", "TEXT NOT NULL DEFAULT 'cover'");
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -556,6 +556,7 @@ export class Repository {
|
|||
cooling_timeout_seconds?: number | null;
|
||||
options?: Record<string, unknown>;
|
||||
entity_id?: number | null;
|
||||
fit?: "cover" | "contain" | "fill";
|
||||
}): LayoutCell {
|
||||
// Resolve content fields from the entity (if given). The legacy columns
|
||||
// remain populated for backward-compatible bundle generation.
|
||||
|
|
@ -574,8 +575,8 @@ export class Repository {
|
|||
}
|
||||
|
||||
const result = this.prep(
|
||||
`INSERT INTO layout_cells (layout_id, "row", col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options, entity_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
`INSERT INTO layout_cells (layout_id, "row", col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options, entity_id, fit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
input.layout_id,
|
||||
input.row,
|
||||
|
|
@ -590,6 +591,7 @@ export class Repository {
|
|||
input.cooling_timeout_seconds ?? null,
|
||||
J(input.options ?? {}),
|
||||
input.entity_id ?? null,
|
||||
input.fit ?? "cover",
|
||||
);
|
||||
const id = Number(result.lastInsertRowid);
|
||||
void this.notify("layout_cells", "create", id);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export interface BundleCell {
|
|||
web_url: string | null;
|
||||
html_content: string | null;
|
||||
cooling_timeout_seconds: number | null;
|
||||
fit: "cover" | "contain" | "fill";
|
||||
}
|
||||
|
||||
export interface BundleLayout {
|
||||
|
|
@ -152,6 +153,7 @@ export function generateBundle(
|
|||
web_url: webUrl,
|
||||
html_content: htmlContent,
|
||||
cooling_timeout_seconds: c.cooling_timeout_seconds,
|
||||
fit: c.fit,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ export interface LayoutCell {
|
|||
cooling_timeout_seconds: number | null;
|
||||
options: Record<string, unknown>;
|
||||
entity_id: number | null;
|
||||
fit: "cover" | "contain" | "fill";
|
||||
}
|
||||
|
||||
export interface Kiosk {
|
||||
|
|
|
|||
|
|
@ -1610,6 +1610,15 @@ export function renderCell(
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Fit</label>
|
||||
<select name="fit" class="form-input">
|
||||
<option value="cover" selected={c.fit === "cover"}>Cover (fill, crop overflow)</option>
|
||||
<option value="contain" selected={c.fit === "contain"}>Contain (letterbox)</option>
|
||||
<option value="fill" selected={c.fit === "fill"}>Fill (stretch)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group span-grid">
|
||||
<div>
|
||||
<label>Width</label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue