feat(automotive): scale the car interface and make it adjustable

A head unit is a large screen sitting an arm's length further away than a phone,
and Plezy drew phone-sized controls on it: the primary button measured 8.3 mm
against the 64 dp a car needs. The whole surface is now scaled - 1.35 by default,
adjustable in Appearance - by giving the app a smaller logical viewport and
scaling the result back, so text, spacing and touch targets grow together instead
of a font size being nudged in isolation.

The scale sits above the messenger and the root Scaffold so snackbars and dialogs
are scaled too, and insets are divided back into the scaled space so a system bar
still reserves its physical size. A scaled surface is also a short one: the setup
screen's fixed offsets and the now-playing transport are laid out to survive it,
and a mistyped scale in a hand-edited settings file is clamped rather than
failing startup.
This commit is contained in:
edde746
2026-08-06 03:45:09 +02:00
parent 4607d165fd
commit 961e9c0326
16 changed files with 483 additions and 137 deletions
+6 -12
View File
@@ -59,20 +59,14 @@ class GridSizeCalculator {
/// to account for sidebars or other elements that reduce the grid's actual
/// width. Never use a plain `SliverLayoutBuilder` for this: its constraints
/// include the scroll offset, so it rebuilds the whole grid every scroll tick.
static int getColumnCount(
double crossAxisExtent,
double maxCrossAxisExtent, {
double crossAxisSpacing = GridLayoutConstants.crossAxisSpacing,
}) {
return (crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil().clamp(1, 100);
static int getColumnCount(double crossAxisExtent, double maxCrossAxisExtent, {double? crossAxisSpacing}) {
final effectiveSpacing = crossAxisSpacing ?? GridLayoutConstants.crossAxisSpacing;
return (crossAxisExtent / (maxCrossAxisExtent + effectiveSpacing)).ceil().clamp(1, 100);
}
static double getCellWidthForColumnCount(
double crossAxisExtent,
int columnCount, {
double crossAxisSpacing = GridLayoutConstants.crossAxisSpacing,
}) {
return (crossAxisExtent - (crossAxisSpacing * (columnCount - 1))) / columnCount;
static double getCellWidthForColumnCount(double crossAxisExtent, int columnCount, {double? crossAxisSpacing}) {
final effectiveSpacing = crossAxisSpacing ?? GridLayoutConstants.crossAxisSpacing;
return (crossAxisExtent - (effectiveSpacing * (columnCount - 1))) / columnCount;
}
/// Computes the actual cell width that a grid with [getMaxCrossAxisExtent] would produce
+5 -3
View File
@@ -1,4 +1,5 @@
import 'package:flutter/widgets.dart';
import 'platform_detector.dart';
/// Layout and sizing constants used throughout the application
/// Screen width breakpoints for responsive design
@@ -50,13 +51,14 @@ class GridLayoutConstants {
/// reserves ([posterAspectRatio] adds 0.3 to the 2:3 image's denominator).
static const double squareGridCellAspectRatio = 2 / 2.3;
static const double crossAxisSpacing = 0;
static const double mainAxisSpacing = 0;
static double get crossAxisSpacing => PlatformDetector.isAutomotive() ? 24 : 0;
static double get mainAxisSpacing => PlatformDetector.isAutomotive() ? 24 : 0;
static double fullCardGridSpacingForScale(double scale) => (12 * scale).clamp(8, 18).toDouble();
/// Standard grid padding
static EdgeInsets get gridPadding => const EdgeInsets.only(left: 2, right: 2, bottom: 2);
static EdgeInsets get gridPadding =>
PlatformDetector.isAutomotive() ? const EdgeInsets.all(24) : const EdgeInsets.only(left: 2, right: 2, bottom: 2);
}
class TvLayoutConstants {