mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
refactor(ui): unify sliver page headers
This commit is contained in:
@@ -8,7 +8,7 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
|
||||
import 'package:spotiflac_android/widgets/animation_utils.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class FavoriteArtistsScreen extends ConsumerWidget {
|
||||
const FavoriteArtistsScreen({super.key});
|
||||
@@ -24,7 +24,7 @@ class FavoriteArtistsScreen extends ConsumerWidget {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(title: context.l10n.collectionFavoriteArtists),
|
||||
AppSliverHeader.page(title: context.l10n.collectionFavoriteArtists),
|
||||
if (artists.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class AboutPage extends StatelessWidget {
|
||||
const AboutPage({super.key});
|
||||
@@ -20,7 +20,7 @@ class AboutPage extends StatelessWidget {
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(title: context.l10n.aboutTitle),
|
||||
AppSliverHeader.page(title: context.l10n.aboutTitle),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:spotiflac_android/services/backup_service.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class BackupRestorePage extends ConsumerStatefulWidget {
|
||||
const BackupRestorePage({super.key});
|
||||
@@ -222,7 +222,7 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(title: l10n.backupTitle),
|
||||
AppSliverHeader.page(title: l10n.backupTitle),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: l10n.backupExportSectionTitle),
|
||||
),
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class CacheManagementPage extends ConsumerStatefulWidget {
|
||||
const CacheManagementPage({super.key});
|
||||
@@ -387,7 +387,7 @@ class _CacheManagementPageState extends ConsumerState<CacheManagementPage> {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(
|
||||
AppSliverHeader.page(
|
||||
title: context.l10n.cacheTitle,
|
||||
actions: [
|
||||
IconButton(
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:spotiflac_android/services/app_remote_config_service.dart';
|
||||
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
import 'package:spotiflac_android/widgets/donate_icons.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class DonatePage extends StatefulWidget {
|
||||
final AppRemoteConfigService? remoteConfigService;
|
||||
@@ -61,7 +61,7 @@ class _DonatePageState extends State<DonatePage> {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const SettingsSliverAppBar(title: 'Donate'),
|
||||
AppSliverHeader.page(title: 'Donate'),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
import 'package:spotiflac_android/widgets/discard_changes_dialog.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class DownloadFallbackExtensionsPage extends ConsumerStatefulWidget {
|
||||
const DownloadFallbackExtensionsPage({super.key});
|
||||
@@ -68,7 +68,7 @@ class _DownloadFallbackExtensionsPageState
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(
|
||||
AppSliverHeader.page(
|
||||
title: context.l10n.extensionsFallbackTitle,
|
||||
leading: IconButton(
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:spotiflac_android/providers/repo_provider.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class ExtensionDetailPage extends ConsumerStatefulWidget {
|
||||
final String extensionId;
|
||||
@@ -89,7 +89,7 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(title: extension.displayName),
|
||||
AppSliverHeader.page(title: extension.displayName),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus;
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
final RegExp _domainPattern = RegExp(
|
||||
r'domain:\s*([^\s,]+)',
|
||||
@@ -140,7 +140,7 @@ class _LogScreenState extends State<LogScreen> {
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SettingsSliverAppBar(
|
||||
AppSliverHeader.page(
|
||||
title: context.l10n.logTitle,
|
||||
actions: [
|
||||
IconButton(
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/screens/settings/lyrics_provider_priority_page.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class LyricsSettingsPage extends ConsumerWidget {
|
||||
const LyricsSettingsPage({super.key});
|
||||
@@ -18,7 +18,7 @@ class LyricsSettingsPage extends ConsumerWidget {
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(title: context.l10n.settingsLyrics),
|
||||
AppSliverHeader.page(title: context.l10n.settingsLyrics),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionLyrics),
|
||||
@@ -202,9 +202,6 @@ class LyricsSettingsPage extends ConsumerWidget {
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -277,9 +274,6 @@ class LyricsSettingsPage extends ConsumerWidget {
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/utils/artist_utils.dart';
|
||||
import 'package:spotiflac_android/screens/settings/metadata_provider_priority_page.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class MetadataSettingsPage extends ConsumerWidget {
|
||||
const MetadataSettingsPage({super.key});
|
||||
@@ -19,7 +19,7 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(title: context.l10n.settingsMetadata),
|
||||
AppSliverHeader.page(title: context.l10n.settingsMetadata),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionDownload),
|
||||
@@ -165,9 +165,6 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
import 'package:spotiflac_android/utils/app_bar_layout.dart';
|
||||
|
||||
/// The collapsing header used by every top-level tab and every settings-style
|
||||
/// sub-page.
|
||||
///
|
||||
/// This replaces five hand-rolled copies of the same `SliverAppBar` +
|
||||
/// `LayoutBuilder` + `FlexibleSpaceBar` block. Those copies had drifted into two
|
||||
/// type ramps (tab roots expanded the title to 34pt, sub-pages to 28pt); both
|
||||
/// now expand to [AppTokens.headerExpandedTitleSize], which matches the
|
||||
/// Material 3 large top app bar headline.
|
||||
class AppSliverHeader extends StatelessWidget {
|
||||
/// Root of a navigation tab: no back button, and the title stays aligned with
|
||||
/// the content margin at every collapse ratio.
|
||||
const AppSliverHeader.tabRoot({super.key, required this.title, this.actions})
|
||||
: leading = null,
|
||||
_showLeading = false;
|
||||
|
||||
/// A pushed page: shows [leading] (a back button by default) and slides the
|
||||
/// title clear of it as the header collapses.
|
||||
const AppSliverHeader.page({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.actions,
|
||||
this.leading,
|
||||
}) : _showLeading = true;
|
||||
|
||||
final String title;
|
||||
final List<Widget>? actions;
|
||||
|
||||
/// Overrides the default back button. Only used by the page variant.
|
||||
final Widget? leading;
|
||||
|
||||
final bool _showLeading;
|
||||
|
||||
/// Left inset of the collapsed title when a leading button is present: enough
|
||||
/// to clear a 48dp icon button plus the 8dp toolbar margin.
|
||||
static const double _leadingClearance = 56;
|
||||
|
||||
/// Content margin the expanded title aligns to.
|
||||
static const double _contentMargin = 24;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.tokens;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final topPadding = normalizedHeaderTopPadding(context);
|
||||
final maxHeight = tokens.headerExpandedHeight + topPadding;
|
||||
final minHeight = kToolbarHeight + topPadding;
|
||||
|
||||
return SliverAppBar(
|
||||
expandedHeight: maxHeight,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: _showLeading
|
||||
? leading ??
|
||||
IconButton(
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
)
|
||||
: null,
|
||||
actions: actions,
|
||||
flexibleSpace: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final expandRatio =
|
||||
((constraints.maxHeight - minHeight) / (maxHeight - minHeight))
|
||||
.clamp(0.0, 1.0);
|
||||
final leftPadding = _showLeading
|
||||
? _leadingClearance -
|
||||
((_leadingClearance - _contentMargin) * expandRatio)
|
||||
: _contentMargin;
|
||||
final fontSize =
|
||||
tokens.headerCollapsedTitleSize +
|
||||
(tokens.headerExpandedTitleSize -
|
||||
tokens.headerCollapsedTitleSize) *
|
||||
expandRatio;
|
||||
|
||||
return FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
right: tokens.gapLg,
|
||||
bottom: tokens.gapLg,
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
// The title grows as the header expands; without a cap a long
|
||||
// localized title overflowed instead of ellipsizing.
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
|
||||
class PrioritySettingsScaffold extends StatelessWidget {
|
||||
final bool hasChanges;
|
||||
@@ -55,7 +55,7 @@ class PrioritySettingsScaffold extends StatelessWidget {
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SettingsSliverAppBar(
|
||||
AppSliverHeader.page(
|
||||
title: title,
|
||||
leading: IconButton(
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/utils/app_bar_layout.dart';
|
||||
|
||||
/// The collapsing header shared by settings-style pages: a pinned
|
||||
/// [SliverAppBar] whose title slides toward the leading edge and shrinks
|
||||
/// from 28 to 20 logical pixels as the header collapses.
|
||||
class SettingsSliverAppBar extends StatelessWidget {
|
||||
const SettingsSliverAppBar({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.actions,
|
||||
this.leading,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final List<Widget>? actions;
|
||||
|
||||
/// Defaults to a back button popping the current route.
|
||||
final Widget? leading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final topPadding = normalizedHeaderTopPadding(context);
|
||||
return SliverAppBar(
|
||||
expandedHeight: 120 + topPadding,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
leading:
|
||||
leading ??
|
||||
IconButton(
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
actions: actions,
|
||||
flexibleSpace: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxHeight = 120 + topPadding;
|
||||
final minHeight = kToolbarHeight + topPadding;
|
||||
final expandRatio =
|
||||
((constraints.maxHeight - minHeight) / (maxHeight - minHeight))
|
||||
.clamp(0.0, 1.0);
|
||||
final leftPadding = 56 - (32 * expandRatio);
|
||||
return FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user