Files
plezy/lib/utils/layout_constants.dart
edde746 961e9c0326 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.
2026-08-06 03:45:09 +02:00

80 lines
2.9 KiB
Dart

import 'package:flutter/widgets.dart';
import 'platform_detector.dart';
/// Layout and sizing constants used throughout the application
/// Screen width breakpoints for responsive design
class ScreenBreakpoints {
static const double mobile = 600;
static const double wideTablet = 900;
static const double desktop = 1200;
static const double largeDesktop = 1600;
// Legacy alias for backward compatibility
static const double tablet = mobile;
static bool isMobile(double width) => width < mobile;
static bool isTablet(double width) => width >= mobile && width < desktop;
static bool isDesktop(double width) => width >= desktop && width < largeDesktop;
static bool isDesktopOrLarger(double width) => width >= desktop;
static bool isWideTabletOrLarger(double width) => width >= wideTablet;
}
/// Animation and notification durations.
class AppDurations {
static const Duration animMedium = Duration(milliseconds: 300);
static const Duration animSlow = Duration(milliseconds: 500);
static const Duration snackBarDefault = Duration(seconds: 3);
static const Duration snackBarLong = Duration(seconds: 4);
}
class GridLayoutConstants {
static const double posterAspectRatio = 2 / 3.3;
static const double fullCardPosterAspectRatio = 2 / 3;
static const double episodeThumbnailAspectRatio = 16 / 9;
static const double episodeGridCellAspectRatio = 1.4;
/// 1:1 music artwork (albums/artists/tracks). Also the full-card image
/// ratio for square items, mirroring [fullCardPosterAspectRatio].
static const double squareAspectRatio = 1 / 1;
/// Square grid cell: 1:1 image plus the same text band the poster cell
/// reserves ([posterAspectRatio] adds 0.3 to the 2:3 image's denominator).
static const double squareGridCellAspectRatio = 2 / 2.3;
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 =>
PlatformDetector.isAutomotive() ? const EdgeInsets.all(24) : const EdgeInsets.only(left: 2, right: 2, bottom: 2);
}
class TvLayoutConstants {
static const double horizontalInset = 72;
static const double shelfHorizontalInset = 56;
static const double shelfVerticalGap = 32;
static const double heroContentMaxWidth = 760;
static const double heroLogoWidth = 520;
static const double heroLogoHeight = 150;
static const double compactHeroLogoWidth = 420;
static const double compactHeroLogoHeight = 112;
static double scaleForHeight(double height) => (height / 1080).clamp(0.85, 1.35).toDouble();
static double scaleForSize(Size size) => scaleForHeight(size.height);
static double scaleOf(BuildContext context) => scaleForSize(MediaQuery.sizeOf(context));
}