diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index e42e716d..bfc8e1e3 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -678,7 +678,9 @@ class _DownloadedAlbumScreenState extends ConsumerState LayoutBuilder( builder: (context, constraints) { const spacing = 8.0; - final itemWidth = (constraints.maxWidth - spacing) / 2; + final columns = (constraints.maxWidth / 320).floor().clamp(2, 4); + final itemWidth = + (constraints.maxWidth - spacing * (columns - 1)) / columns; final actions = [ SelectionActionButton( icon: Icons.share_outlined, diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 76049d6c..a7e681a9 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -886,7 +886,9 @@ class _LocalAlbumScreenState extends ConsumerState LayoutBuilder( builder: (context, constraints) { const spacing = 8.0; - final itemWidth = (constraints.maxWidth - spacing) / 2; + final columns = (constraints.maxWidth / 320).floor().clamp(2, 4); + final itemWidth = + (constraints.maxWidth - spacing * (columns - 1)) / columns; final actions = []; if (flacEligibleCount > 0) { diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 5a68d7e7..4b62b2cf 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -236,14 +236,18 @@ class _MainShellState extends ConsumerState color: colorScheme.primary, ), title: Text(context.l10n.safMigrationTitle), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.safMigrationMessage1), - const SizedBox(height: 12), - Text(context.l10n.safMigrationMessage2), - ], + content: ConstrainedBox( + // Keeps the text column readable on tablets/landscape. + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.safMigrationMessage1), + const SizedBox(height: 12), + Text(context.l10n.safMigrationMessage2), + ], + ), ), actions: [ TextButton( diff --git a/lib/screens/queue_tab_item_widgets.dart b/lib/screens/queue_tab_item_widgets.dart index 2aaf7481..f5ba784e 100644 --- a/lib/screens/queue_tab_item_widgets.dart +++ b/lib/screens/queue_tab_item_widgets.dart @@ -31,7 +31,9 @@ extension _QueueTabItemWidgets on _QueueTabState { LayoutBuilder( builder: (context, constraints) { const spacing = 8.0; - final itemWidth = (constraints.maxWidth - spacing) / 2; + final columns = (constraints.maxWidth / 320).floor().clamp(2, 4); + final itemWidth = + (constraints.maxWidth - spacing * (columns - 1)) / columns; final actions = []; if (localOnlySelection && flacEligibleCount > 0) { diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index d75d9af5..615a0558 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -872,9 +872,7 @@ class _DownloadSettingsPageState extends ConsumerState { final countryName = _regionCountryName(context, code); return ListTile( title: Text(code), - subtitle: countryName != code - ? Text(countryName) - : null, + subtitle: countryName != code ? Text(countryName) : null, trailing: isSelected ? Icon(Icons.check, color: colorScheme.primary) : null, @@ -975,7 +973,12 @@ class _ServiceSelector extends ConsumerWidget { : LayoutBuilder( builder: (context, constraints) { const spacing = 8.0; - final chipWidth = (constraints.maxWidth - spacing) / 2; + final columns = (constraints.maxWidth / 320).floor().clamp( + 2, + 4, + ); + final chipWidth = + (constraints.maxWidth - spacing * (columns - 1)) / columns; return Wrap( spacing: spacing, runSpacing: spacing, @@ -1168,7 +1171,9 @@ class _SettingsChoiceGrid extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { const spacing = 8.0; - final chipWidth = (constraints.maxWidth - spacing) / 2; + final columns = (constraints.maxWidth / 320).floor().clamp(2, 4); + final chipWidth = + (constraints.maxWidth - spacing * (columns - 1)) / columns; return Wrap( spacing: spacing, runSpacing: spacing, diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 1d295cfb..e1746222 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -14,13 +14,14 @@ class AppTheme { // regression introduced by the Flutter upgrade. Forcing a non-predictive // builder restores the correct back order (close modal, then pop page), at the // cost of the predictive-back preview animation. - static const PageTransitionsTheme _pageTransitionsTheme = PageTransitionsTheme( - builders: { - TargetPlatform.android: FadeForwardsPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), - TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), - }, - ); + static const PageTransitionsTheme _pageTransitionsTheme = + PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeForwardsPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), + }, + ); static ThemeData light({ColorScheme? dynamicScheme, Color? seedColor}) { final scheme = @@ -44,6 +45,7 @@ class AppTheme { inputDecorationTheme: _inputDecorationTheme(scheme), listTileTheme: _listTileTheme(scheme), dialogTheme: _dialogTheme(scheme), + bottomSheetTheme: _bottomSheetTheme, navigationBarTheme: _navigationBarTheme(scheme), snackBarTheme: _snackBarTheme(scheme), progressIndicatorTheme: _progressIndicatorTheme(scheme), @@ -81,6 +83,7 @@ class AppTheme { inputDecorationTheme: _inputDecorationTheme(scheme), listTileTheme: _listTileTheme(scheme), dialogTheme: _dialogTheme(scheme), + bottomSheetTheme: _bottomSheetTheme, navigationBarTheme: _navigationBarTheme(scheme, isAmoled: isAmoled), snackBarTheme: _snackBarTheme(scheme), progressIndicatorTheme: _progressIndicatorTheme(scheme), @@ -208,6 +211,13 @@ class AppTheme { contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), ); + // Caps modal sheet width on tablets/landscape (no effect on phones, whose + // screens are narrower); Flutter centers the sheet when narrower than the + // screen. + static const BottomSheetThemeData _bottomSheetTheme = BottomSheetThemeData( + constraints: BoxConstraints(maxWidth: 640), + ); + static DialogThemeData _dialogTheme(ColorScheme scheme) => DialogThemeData( elevation: 6, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), diff --git a/lib/widgets/update_dialog.dart b/lib/widgets/update_dialog.dart index 748289c1..8317a147 100644 --- a/lib/widgets/update_dialog.dart +++ b/lib/widgets/update_dialog.dart @@ -135,292 +135,299 @@ class _UpdateDialogState extends State { child: Dialog( backgroundColor: colorScheme.surfaceContainerHigh, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(16), - ), - child: Icon( - Icons.system_update_rounded, - color: colorScheme.onPrimaryContainer, - size: 28, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.forced - ? context.l10n.updateRequiredTitle - : context.l10n.updateAvailable, - style: Theme.of(context).textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 2), - Text( - widget.forced - ? context.l10n.updateRequiredNotice( - widget.updateInfo.releasesBehind, - ) - : context.l10n.updateNewVersionReady, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith( - color: widget.forced - ? colorScheme.error - : colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 20), - - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: isDark - ? Color.alphaBlend( - Colors.white.withValues(alpha: 0.08), - colorScheme.surface, - ) - : Color.alphaBlend( - Colors.black.withValues(alpha: 0.04), - colorScheme.surface, - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + child: ConstrainedBox( + // Keeps the changelog column readable on tablets/landscape. + constraints: const BoxConstraints(maxWidth: 480), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - _VersionChip( - version: AppInfo.displayVersion, - label: context.l10n.updateCurrent, - colorScheme: colorScheme, - ), - const SizedBox(width: 12), - Icon( - Icons.arrow_forward_rounded, - size: 20, - color: colorScheme.primary, - ), - const SizedBox(width: 12), - _VersionChip( - version: widget.updateInfo.version, - label: context.l10n.updateNew, - colorScheme: colorScheme, - isNew: true, - ), - ], - ), - ), - const SizedBox(height: 20), - - if (_isDownloading) ...[ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: isDark - ? Color.alphaBlend( - Colors.white.withValues(alpha: 0.05), - colorScheme.surface, - ) - : Color.alphaBlend( - Colors.black.withValues(alpha: 0.03), - colorScheme.surface, - ), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: colorScheme.primary, - ), - ), - const SizedBox(width: 12), - Text( - context.l10n.updateDownloading, - style: Theme.of(context).textTheme.titleSmall - ?.copyWith(fontWeight: FontWeight.w600), - ), - ], + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(16), ), - const SizedBox(height: 12), - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: _progress, - minHeight: 6, - backgroundColor: colorScheme.surfaceContainerHighest, - ), + child: Icon( + Icons.system_update_rounded, + color: colorScheme.onPrimaryContainer, + size: 28, ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _statusText, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(color: colorScheme.onSurfaceVariant), + widget.forced + ? context.l10n.updateRequiredTitle + : context.l10n.updateAvailable, + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), + const SizedBox(height: 2), Text( - '${(_progress * 100).toInt()}%', - style: Theme.of(context).textTheme.bodySmall + widget.forced + ? context.l10n.updateRequiredNotice( + widget.updateInfo.releasesBehind, + ) + : context.l10n.updateNewVersionReady, + style: Theme.of(context).textTheme.bodyMedium ?.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.w600, + color: widget.forced + ? colorScheme.error + : colorScheme.onSurfaceVariant, ), ), ], ), + ), + ], + ), + const SizedBox(height: 20), + + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.08), + colorScheme.surface, + ) + : Color.alphaBlend( + Colors.black.withValues(alpha: 0.04), + colorScheme.surface, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outlineVariant.withValues(alpha: 0.5), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _VersionChip( + version: AppInfo.displayVersion, + label: context.l10n.updateCurrent, + colorScheme: colorScheme, + ), + const SizedBox(width: 12), + Icon( + Icons.arrow_forward_rounded, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 12), + _VersionChip( + version: widget.updateInfo.version, + label: context.l10n.updateNew, + colorScheme: colorScheme, + isNew: true, + ), ], ), ), - ] else ...[ - Text( - context.l10n.updateWhatsNew, - style: Theme.of( - context, - ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Container( - constraints: const BoxConstraints(maxHeight: 180), - decoration: BoxDecoration( - color: isDark - ? Color.alphaBlend( - Colors.white.withValues(alpha: 0.05), - colorScheme.surface, - ) - : Color.alphaBlend( - Colors.black.withValues(alpha: 0.03), - colorScheme.surface, - ), - borderRadius: BorderRadius.circular(16), - ), - child: SingleChildScrollView( + const SizedBox(height: 20), + + if (_isDownloading) ...[ + Container( padding: const EdgeInsets.all(16), - child: Text( - _formatChangelog( - widget.updateInfo.changelog, - context.l10n.updateSeeReleaseNotes, - ), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(height: 1.5), + decoration: BoxDecoration( + color: isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) + : Color.alphaBlend( + Colors.black.withValues(alpha: 0.03), + colorScheme.surface, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: colorScheme.primary, + ), + ), + const SizedBox(width: 12), + Text( + context.l10n.updateDownloading, + style: Theme.of(context).textTheme.titleSmall + ?.copyWith(fontWeight: FontWeight.w600), + ), + ], + ), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: _progress, + minHeight: 6, + backgroundColor: + colorScheme.surfaceContainerHighest, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _statusText, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + Text( + '${(_progress * 100).toInt()}%', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], ), ), - ), - ], - const SizedBox(height: 24), + ] else ...[ + Text( + context.l10n.updateWhatsNew, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Container( + constraints: const BoxConstraints(maxHeight: 180), + decoration: BoxDecoration( + color: isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) + : Color.alphaBlend( + Colors.black.withValues(alpha: 0.03), + colorScheme.surface, + ), + borderRadius: BorderRadius.circular(16), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Text( + _formatChangelog( + widget.updateInfo.changelog, + context.l10n.updateSeeReleaseNotes, + ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(height: 1.5), + ), + ), + ), + ], + const SizedBox(height: 24), - if (_isDownloading) - SizedBox( - width: double.infinity, - child: widget.forced - ? const SizedBox.shrink() - : OutlinedButton( - onPressed: () => Navigator.pop(context), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), + if (_isDownloading) + SizedBox( + width: double.infinity, + child: widget.forced + ? const SizedBox.shrink() + : OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text(context.l10n.dialogCancel), + ), + ) + else + Column( + children: [ + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: _downloadAndInstall, + icon: const Icon(Icons.download_rounded, size: 20), + label: Text(context.l10n.updateDownloadInstall), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), - child: Text(context.l10n.dialogCancel), - ), - ) - else - Column( - children: [ - SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: _downloadAndInstall, - icon: const Icon(Icons.download_rounded, size: 20), - label: Text(context.l10n.updateDownloadInstall), - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), ), ), - ), - if (!widget.forced) const SizedBox(height: 8), - if (!widget.forced) - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - widget.onDisableUpdates(); - Navigator.pop(context); - }, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - vertical: 12, + if (!widget.forced) const SizedBox(height: 8), + if (!widget.forced) + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + widget.onDisableUpdates(); + Navigator.pop(context); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - context.l10n.updateDontRemind, - style: TextStyle( - color: colorScheme.onSurfaceVariant, + child: Text( + context.l10n.updateDontRemind, + style: TextStyle( + color: colorScheme.onSurfaceVariant, + ), ), ), ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () { - widget.onDismiss(); - Navigator.pop(context); - }, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () { + widget.onDismiss(); + Navigator.pop(context); + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), + child: Text(context.l10n.updateLater), ), - child: Text(context.l10n.updateLater), ), - ), - ], - ), - ], - ), - ], + ], + ), + ], + ), + ], + ), ), ), ),