Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

388 lines
14 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:url_launcher/url_launcher.dart';
import '../constants.dart';
import '../data/gallery_options.dart';
import '../gallery_localizations.dart';
import '../layout/adaptive.dart';
import 'about.dart' as about;
import 'home.dart';
import 'settings_list_item.dart';
enum _ExpandableSetting { textScale, textDirection, locale, platform, theme }
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key, required this.animationController});
final AnimationController animationController;
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
_ExpandableSetting? _expandedSettingId;
late Animation<double> _staggerSettingsItemsAnimation;
void onTapSetting(_ExpandableSetting settingId) {
setState(() {
if (_expandedSettingId == settingId) {
_expandedSettingId = null;
} else {
_expandedSettingId = settingId;
}
});
}
void _closeSettingId(AnimationStatus status) {
if (status.isDismissed) {
setState(() {
_expandedSettingId = null;
});
}
}
@override
void initState() {
super.initState();
// When closing settings, also shrink expanded setting.
widget.animationController.addStatusListener(_closeSettingId);
_staggerSettingsItemsAnimation = CurvedAnimation(
parent: widget.animationController,
curve: const Interval(0.4, 1.0, curve: Curves.ease),
);
}
@override
void dispose() {
super.dispose();
widget.animationController.removeStatusListener(_closeSettingId);
}
/// Given a [Locale], returns a [DisplayOption] with its native name for a
/// title and its name in the currently selected locale for a subtitle. If the
/// native name can't be determined, it is omitted. If the locale can't be
/// determined, the locale code is used.
DisplayOption _getLocaleDisplayOption(BuildContext context, Locale? locale) {
final localeCode = locale.toString();
final String? localeName = LocaleNames.of(context)!.nameOf(localeCode);
if (localeName != null) {
final String? localeNativeName =
LocaleNamesLocalizationsDelegate.nativeLocaleNames[localeCode];
return localeNativeName != null
? DisplayOption(localeNativeName, subtitle: localeName)
: DisplayOption(localeName);
} else {
// gsw, fil, and es_419 aren't in flutter_localized_countries' dataset
// so we handle them separately
switch (localeCode) {
case 'gsw':
return DisplayOption('Schwiizertüütsch', subtitle: 'Swiss German');
case 'fil':
return DisplayOption('Filipino', subtitle: 'Filipino');
case 'es_419':
return DisplayOption('español (Latinoamérica)', subtitle: 'Spanish (Latin America)');
}
}
return DisplayOption(localeCode);
}
/// Create a sorted — by native name map of supported locales to their
/// intended display string, with a system option as the first element.
Map<Locale, DisplayOption> _getLocaleOptions() {
final localeOptions = Map<Locale, DisplayOption>.of(<Locale, DisplayOption>{
systemLocaleOption: DisplayOption(
GalleryLocalizations.of(context)!.settingsSystemDefault +
(deviceLocale != null
? ' - ${_getLocaleDisplayOption(context, deviceLocale).title}'
: ''),
),
});
final supportedLocales = List<Locale>.from(GalleryLocalizations.supportedLocales);
supportedLocales.removeWhere((Locale locale) => locale == deviceLocale);
final List<MapEntry<Locale, DisplayOption>> displayLocales =
Map<Locale, DisplayOption>.fromIterable(
supportedLocales,
value: (dynamic locale) => _getLocaleDisplayOption(context, locale as Locale?),
).entries.toList()..sort(
(MapEntry<Locale, DisplayOption> l1, MapEntry<Locale, DisplayOption> l2) =>
compareAsciiUpperCase(l1.value.title, l2.value.title),
);
localeOptions.addAll(Map<Locale, DisplayOption>.fromEntries(displayLocales));
return localeOptions;
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final GalleryOptions options = GalleryOptions.of(context);
final bool isDesktop = isDisplayDesktop(context);
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final settingsListItems = <Widget>[
SettingsListItem<double?>(
title: localizations.settingsTextScaling,
selectedOption: options.textScaleFactor(context, useSentinel: true),
optionsMap: Map<double?, DisplayOption>.of(<double?, DisplayOption>{
systemTextScaleFactorOption: DisplayOption(localizations.settingsSystemDefault),
0.8: DisplayOption(localizations.settingsTextScalingSmall),
1.0: DisplayOption(localizations.settingsTextScalingNormal),
2.0: DisplayOption(localizations.settingsTextScalingLarge),
3.0: DisplayOption(localizations.settingsTextScalingHuge),
}),
onOptionChanged: (double? newTextScale) =>
GalleryOptions.update(context, options.copyWith(textScaleFactor: newTextScale)),
onTapSetting: () => onTapSetting(_ExpandableSetting.textScale),
isExpanded: _expandedSettingId == _ExpandableSetting.textScale,
),
SettingsListItem<CustomTextDirection?>(
title: localizations.settingsTextDirection,
selectedOption: options.customTextDirection,
optionsMap:
Map<CustomTextDirection?, DisplayOption>.of(<CustomTextDirection?, DisplayOption>{
CustomTextDirection.localeBased: DisplayOption(
localizations.settingsTextDirectionLocaleBased,
),
CustomTextDirection.ltr: DisplayOption(localizations.settingsTextDirectionLTR),
CustomTextDirection.rtl: DisplayOption(localizations.settingsTextDirectionRTL),
}),
onOptionChanged: (CustomTextDirection? newTextDirection) =>
GalleryOptions.update(context, options.copyWith(customTextDirection: newTextDirection)),
onTapSetting: () => onTapSetting(_ExpandableSetting.textDirection),
isExpanded: _expandedSettingId == _ExpandableSetting.textDirection,
),
SettingsListItem<Locale?>(
title: localizations.settingsLocale,
selectedOption: options.locale == deviceLocale ? systemLocaleOption : options.locale,
optionsMap: _getLocaleOptions(),
onOptionChanged: (Locale? newLocale) {
if (newLocale == systemLocaleOption) {
newLocale = deviceLocale;
}
GalleryOptions.update(context, options.copyWith(locale: newLocale));
},
onTapSetting: () => onTapSetting(_ExpandableSetting.locale),
isExpanded: _expandedSettingId == _ExpandableSetting.locale,
),
SettingsListItem<TargetPlatform?>(
title: localizations.settingsPlatformMechanics,
selectedOption: options.platform,
optionsMap: Map<TargetPlatform?, DisplayOption>.of(<TargetPlatform?, DisplayOption>{
TargetPlatform.android: DisplayOption('Android'),
TargetPlatform.iOS: DisplayOption('iOS'),
TargetPlatform.macOS: DisplayOption('macOS'),
TargetPlatform.linux: DisplayOption('Linux'),
TargetPlatform.windows: DisplayOption('Windows'),
}),
onOptionChanged: (TargetPlatform? newPlatform) =>
GalleryOptions.update(context, options.copyWith(platform: newPlatform)),
onTapSetting: () => onTapSetting(_ExpandableSetting.platform),
isExpanded: _expandedSettingId == _ExpandableSetting.platform,
),
SettingsListItem<ThemeMode?>(
title: localizations.settingsTheme,
selectedOption: options.themeMode,
optionsMap: Map<ThemeMode?, DisplayOption>.of(<ThemeMode?, DisplayOption>{
ThemeMode.system: DisplayOption(localizations.settingsSystemDefault),
ThemeMode.dark: DisplayOption(localizations.settingsDarkTheme),
ThemeMode.light: DisplayOption(localizations.settingsLightTheme),
}),
onOptionChanged: (ThemeMode? newThemeMode) =>
GalleryOptions.update(context, options.copyWith(themeMode: newThemeMode)),
onTapSetting: () => onTapSetting(_ExpandableSetting.theme),
isExpanded: _expandedSettingId == _ExpandableSetting.theme,
),
ToggleSetting(
text: GalleryLocalizations.of(context)!.settingsSlowMotion,
value: options.timeDilation != 1.0,
onChanged: (bool isOn) =>
GalleryOptions.update(context, options.copyWith(timeDilation: isOn ? 5.0 : 1.0)),
),
];
return Material(
color: colorScheme.secondaryContainer,
child: Padding(
padding: isDesktop ? EdgeInsets.zero : const EdgeInsets.only(bottom: galleryHeaderHeight),
// Remove ListView top padding as it is already accounted for.
child: MediaQuery.removePadding(
removeTop: isDesktop,
context: context,
child: ListView(
children: <Widget>[
if (isDesktop) const SizedBox(height: firstHeaderDesktopTopPadding),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: ExcludeSemantics(
child: Header(
color: Theme.of(context).colorScheme.onSurface,
text: localizations.settingsTitle,
),
),
),
if (isDesktop)
...settingsListItems
else ...<Widget>[
_AnimateSettingsListItems(
animation: _staggerSettingsItemsAnimation,
children: settingsListItems,
),
const SizedBox(height: 16),
Divider(thickness: 2, height: 0, color: colorScheme.outline),
const SizedBox(height: 12),
const SettingsAbout(),
const SettingsFeedback(),
const SizedBox(height: 12),
Divider(thickness: 2, height: 0, color: colorScheme.outline),
const SettingsAttribution(),
],
],
),
),
),
);
}
}
class SettingsAbout extends StatelessWidget {
const SettingsAbout({super.key});
@override
Widget build(BuildContext context) {
return _SettingsLink(
title: GalleryLocalizations.of(context)!.settingsAbout,
icon: Icons.info_outline,
onTap: () {
about.showAboutDialog(context: context);
},
);
}
}
class SettingsFeedback extends StatelessWidget {
const SettingsFeedback({super.key});
@override
Widget build(BuildContext context) {
return _SettingsLink(
title: GalleryLocalizations.of(context)!.settingsFeedback,
icon: Icons.feedback,
onTap: () async {
final Uri url = Uri.parse('https://github.com/flutter/gallery/issues/new/choose/');
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
},
);
}
}
class SettingsAttribution extends StatelessWidget {
const SettingsAttribution({super.key});
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final verticalPadding = isDesktop ? 0.0 : 28.0;
return MergeSemantics(
child: Padding(
padding: EdgeInsetsDirectional.only(
start: isDesktop ? 24 : 32,
end: isDesktop ? 0 : 32,
top: verticalPadding,
bottom: verticalPadding,
),
child: SelectableText(
GalleryLocalizations.of(context)!.settingsAttribution,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 12,
color: Theme.of(context).colorScheme.onSecondary,
),
textAlign: isDesktop ? TextAlign.end : TextAlign.start,
),
),
);
}
}
class _SettingsLink extends StatelessWidget {
const _SettingsLink({required this.title, this.icon, this.onTap});
final String title;
final IconData? icon;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final bool isDesktop = isDisplayDesktop(context);
return InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: isDesktop ? 24 : 32),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(icon, color: colorScheme.onSecondary.withOpacity(0.5), size: 24),
Flexible(
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 16, top: 12, bottom: 12),
child: Text(
title,
style: textTheme.titleSmall!.apply(color: colorScheme.onSecondary),
textAlign: isDesktop ? TextAlign.end : TextAlign.start,
),
),
),
],
),
),
);
}
}
/// Animate the settings list items to stagger in from above.
class _AnimateSettingsListItems extends StatelessWidget {
const _AnimateSettingsListItems({required this.animation, required this.children});
final Animation<double> animation;
final List<Widget> children;
@override
Widget build(BuildContext context) {
const dividingPadding = 4.0;
final dividerTween = Tween<double>(begin: 0, end: dividingPadding);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
children: <Widget>[
for (final Widget child in children)
AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return Padding(
padding: EdgeInsets.only(top: dividerTween.animate(animation).value),
child: child,
);
},
child: child,
),
],
),
);
}
}