mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-13 21:38:58 +02:00
113 lines
4.3 KiB
Dart
113 lines
4.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Keeps touch-first tablets on the bottom navigation used by phones. A rail
|
|
/// is reserved for genuinely desktop-width windows; the old 600dp threshold
|
|
/// made an iPad portrait layout look like a shrunken desktop app.
|
|
const double navigationRailBreakpoint = 1440;
|
|
const double defaultWideContentMaxWidth = 960;
|
|
const double maxWideContentInset = 32;
|
|
|
|
bool useNavigationRailForWidth(double width) =>
|
|
width >= navigationRailBreakpoint;
|
|
|
|
/// Explicit horizontal safe spacing for controls drawn in full-bleed detail
|
|
/// headers. iOS reports no left/right safe-area inset in portrait, so toolbar
|
|
/// buttons otherwise land only a few pixels from the device edge.
|
|
double detailHeaderEdgeInset(BuildContext context) {
|
|
final platform = Theme.of(context).platform;
|
|
return platform == TargetPlatform.iOS || platform == TargetPlatform.macOS
|
|
? 12
|
|
: 0;
|
|
}
|
|
|
|
/// Visual scale for touch-first tablet surfaces. Large iPads have enough
|
|
/// pixels to make an unscaled phone density feel miniature, especially when
|
|
/// Simulator shrinks the whole device to fit a desktop display.
|
|
double adaptiveUiScaleForSize(Size size) {
|
|
if (size.shortestSide < 600 || size.width >= navigationRailBreakpoint) {
|
|
return 1;
|
|
}
|
|
return size.shortestSide >= 800 ? 1.2 : 1.1;
|
|
}
|
|
|
|
EdgeInsets _scaledInsets(EdgeInsets value, double divisor) => EdgeInsets.only(
|
|
left: value.left / divisor,
|
|
top: value.top / divisor,
|
|
right: value.right / divisor,
|
|
bottom: value.bottom / divisor,
|
|
);
|
|
|
|
/// Scales the complete app surface on tablets, including controls, dialogs,
|
|
/// navigation, spacing and hit targets. The child receives the correspondingly
|
|
/// smaller logical viewport so responsive layouts still reflow instead of
|
|
/// being cropped, while [FittedBox] maps that viewport back to the full screen.
|
|
class AdaptiveUiScaler extends StatelessWidget {
|
|
const AdaptiveUiScaler({super.key, required this.child});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final mediaQuery = MediaQuery.of(context);
|
|
final scale = adaptiveUiScaleForSize(mediaQuery.size);
|
|
if (scale == 1) return child;
|
|
|
|
final scaledSize = Size(
|
|
mediaQuery.size.width / scale,
|
|
mediaQuery.size.height / scale,
|
|
);
|
|
final scaledMediaQuery = mediaQuery.copyWith(
|
|
size: scaledSize,
|
|
// One inner logical pixel occupies [scale] outer logical pixels.
|
|
// Advertising the effective density keeps decoded artwork sharp.
|
|
devicePixelRatio: mediaQuery.devicePixelRatio * scale,
|
|
padding: _scaledInsets(mediaQuery.padding, scale),
|
|
viewPadding: _scaledInsets(mediaQuery.viewPadding, scale),
|
|
viewInsets: _scaledInsets(mediaQuery.viewInsets, scale),
|
|
systemGestureInsets: _scaledInsets(mediaQuery.systemGestureInsets, scale),
|
|
);
|
|
|
|
return SizedBox.expand(
|
|
child: FittedBox(
|
|
fit: BoxFit.fill,
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox.fromSize(
|
|
size: scaledSize,
|
|
child: MediaQuery(data: scaledMediaQuery, child: child),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Widest content span for a surface of [maxWidth]: content is never narrower
|
|
/// than [contentMaxWidth], and the centering margin never exceeds 32dp per
|
|
/// side so tablets retain near-full-width rows.
|
|
double adaptiveContentMaxWidth(
|
|
double maxWidth, {
|
|
double contentMaxWidth = defaultWideContentMaxWidth,
|
|
}) => maxWidth > contentMaxWidth + (maxWideContentInset * 2)
|
|
? maxWidth - (maxWideContentInset * 2)
|
|
: contentMaxWidth;
|
|
|
|
/// Horizontal inset that centers content of [maxWidth] at
|
|
/// [adaptiveContentMaxWidth]; zero once it fits. Prefer this constraint-based
|
|
/// form when the widget may live inside an already clamped box (e.g. a bottom
|
|
/// sheet), where screen width would over-inset.
|
|
double wideInsetForWidth(
|
|
double maxWidth, {
|
|
double contentMaxWidth = defaultWideContentMaxWidth,
|
|
}) => maxWidth > contentMaxWidth
|
|
? ((maxWidth - contentMaxWidth) / 2).clamp(0.0, maxWideContentInset)
|
|
: 0;
|
|
|
|
/// Horizontal inset that centers full-width list content on tablets/landscape;
|
|
/// zero on phones, so rows stop stretching across the whole screen.
|
|
double wideListInset(
|
|
BuildContext context, {
|
|
double contentMaxWidth = defaultWideContentMaxWidth,
|
|
}) => wideInsetForWidth(
|
|
MediaQuery.sizeOf(context).width,
|
|
contentMaxWidth: contentMaxWidth,
|
|
);
|