fix(ui): improve adaptive phone and tablet layouts

This commit is contained in:
zarzet
2026-09-04 23:44:43 +07:00
parent 514081fc6a
commit 958b79ebca
16 changed files with 378 additions and 122 deletions
+25 -14
View File
@@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:spotiflac_android/theme/app_tokens.dart';
import 'package:spotiflac_android/theme/cover_palette.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
/// Collapsing album-detail header shared by the album, local-album, and
/// downloaded-album screens: full-bleed [background] (optionally blurred and
@@ -91,6 +92,11 @@ class AlbumDetailHeader extends StatelessWidget {
Widget _buildAppBar(BuildContext context, ColorScheme headerScheme) {
final tokens = context.tokens;
// iOS does not add horizontal safe-area padding in portrait. Give toolbar
// controls and header content an explicit inset so circular actions do not
// sit against the glass edge on either iPhone or iPad. Android retains its
// existing spacing.
final iosEdgeInset = detailHeaderEdgeInset(context);
// Scrim and gradient are drawn from the palette surface instead of black,
// so a light theme gets a light header with dark text and a dark theme
// keeps the familiar dark treatment — both tinted by the artwork.
@@ -157,8 +163,8 @@ class AlbumDetailHeader extends StatelessWidget {
),
),
Positioned(
left: 20,
right: 20,
left: 20 + iosEdgeInset,
right: 20 + iosEdgeInset,
bottom: 40,
child: AnimatedOpacity(
duration: tokens.motionFast,
@@ -235,20 +241,25 @@ class AlbumDetailHeader extends StatelessWidget {
);
},
),
leading:
leading ??
IconButton.filledTonal(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: const Icon(Icons.arrow_back),
style: IconButton.styleFrom(
minimumSize: Size.square(tokens.minTouchTarget),
backgroundColor: headerScheme.surfaceContainerHigh.withValues(
alpha: 0.75,
leadingWidth: kToolbarHeight + iosEdgeInset,
leading: Padding(
padding: EdgeInsets.only(left: iosEdgeInset),
child:
leading ??
IconButton.filledTonal(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: const Icon(Icons.arrow_back),
style: IconButton.styleFrom(
minimumSize: Size.square(tokens.minTouchTarget),
backgroundColor: headerScheme.surfaceContainerHigh.withValues(
alpha: 0.75,
),
foregroundColor: headerScheme.onSurfaceVariant,
),
foregroundColor: headerScheme.onSurfaceVariant,
onPressed: () => Navigator.pop(context),
),
onPressed: () => Navigator.pop(context),
),
),
actionsPadding: EdgeInsets.only(right: iosEdgeInset),
actions: appBarActions,
);
}
+20 -8
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/theme/app_tokens.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
/// The collapsing header used by every top-level tab and every settings-style
@@ -44,6 +45,7 @@ class AppSliverHeader extends StatelessWidget {
final topPadding = normalizedHeaderTopPadding(context);
final maxHeight = tokens.headerExpandedHeight + topPadding;
final minHeight = kToolbarHeight + topPadding;
final edgeInset = detailHeaderEdgeInset(context);
return SliverAppBar(
expandedHeight: maxHeight,
@@ -53,14 +55,22 @@ class AppSliverHeader extends StatelessWidget {
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
automaticallyImplyLeading: false,
leadingWidth: _showLeading ? kToolbarHeight + edgeInset : null,
leading: _showLeading
? leading ??
IconButton(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
)
? Padding(
padding: EdgeInsets.only(left: edgeInset),
child:
leading ??
IconButton(
tooltip: MaterialLocalizations.of(
context,
).backButtonTooltip,
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
)
: null,
actionsPadding: EdgeInsets.only(right: edgeInset),
actions: actions,
flexibleSpace: LayoutBuilder(
builder: (context, constraints) {
@@ -68,8 +78,9 @@ class AppSliverHeader extends StatelessWidget {
((constraints.maxHeight - minHeight) / (maxHeight - minHeight))
.clamp(0.0, 1.0);
final leftPadding = _showLeading
? _leadingClearance -
((_leadingClearance - _contentMargin) * expandRatio)
? (_leadingClearance + edgeInset) -
(((_leadingClearance + edgeInset) - _contentMargin) *
expandRatio)
: _contentMargin;
final fontSize =
tokens.headerCollapsedTitleSize +
@@ -78,6 +89,7 @@ class AppSliverHeader extends StatelessWidget {
expandRatio;
return FlexibleSpaceBar(
centerTitle: false,
expandedTitleScale: 1.0,
titlePadding: EdgeInsets.only(
left: leftPadding,
+9 -13
View File
@@ -13,7 +13,6 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
final String? coverUrl;
final void Function(String quality, String service) onSelect;
final String? recommendedService;
final ScrollController? scrollController;
const DownloadServicePicker({
super.key,
@@ -22,7 +21,6 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
this.coverUrl,
required this.onSelect,
this.recommendedService,
this.scrollController,
});
@override
@@ -44,16 +42,15 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
isScrollControlled: true,
enableDrag: false,
builder: (context) => AppDraggableSheet(
builder: (context, scrollController) => DownloadServicePicker(
trackName: trackName,
artistName: artistName,
coverUrl: coverUrl,
onSelect: onSelect,
recommendedService: recommendedService,
scrollController: scrollController,
),
constraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * 0.88,
),
builder: (context) => DownloadServicePicker(
trackName: trackName,
artistName: artistName,
coverUrl: coverUrl,
onSelect: onSelect,
recommendedService: recommendedService,
),
);
}
@@ -126,7 +123,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
return SafeArea(
child: SingleChildScrollView(
controller: widget.scrollController,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,