fix(settings): align mobile option typography
This commit is contained in:
@@ -267,7 +267,7 @@ class AppearanceSettingsScreen extends StatelessWidget {
|
||||
children: [
|
||||
const AppIcon(Symbols.grid_view_rounded, fill: 1),
|
||||
const SizedBox(width: 16),
|
||||
Text(t.settings.libraryDensity, style: theme.textTheme.bodyLarge),
|
||||
Text(t.settings.libraryDensity, style: settingsOptionTitleStyle(context)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../screens/settings/settings_utils.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import 'app_icon.dart';
|
||||
import 'focusable_list_tile.dart';
|
||||
import 'settings_section.dart';
|
||||
@@ -40,17 +41,18 @@ class SettingSwitchTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
final svc = _TileBase._svc;
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: svc.listenable(pref),
|
||||
builder: (_, value, _) => FocusableSwitchListTile(
|
||||
focusNode: focusNode,
|
||||
secondary: AppIcon(icon, fill: 1),
|
||||
title: Text(title),
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: subtitle != null ? Text(subtitle!) : null,
|
||||
value: value,
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.standard,
|
||||
dense: isMobile,
|
||||
visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
|
||||
onChanged: enabled
|
||||
? (v) async {
|
||||
await svc.write(pref, v);
|
||||
@@ -86,15 +88,16 @@ class SettingNavigationTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
return FocusableListTile(
|
||||
focusNode: focusNode,
|
||||
leading: AppIcon(icon, fill: 1),
|
||||
title: Text(title),
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: subtitle != null ? Text(subtitle!) : null,
|
||||
trailing: AppIcon(trailingIcon, fill: 1),
|
||||
onTap: onTap ?? () => Navigator.push(context, MaterialPageRoute(builder: destinationBuilder!)),
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.standard,
|
||||
dense: isMobile,
|
||||
visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -126,16 +129,17 @@ class SettingNumberTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
final svc = _TileBase._svc;
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: svc.listenable(pref),
|
||||
builder: (_, value, _) => FocusableListTile(
|
||||
leading: AppIcon(icon, fill: 1),
|
||||
title: Text(title),
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: Text(subtitleBuilder(value)),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.standard,
|
||||
dense: isMobile,
|
||||
visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
|
||||
onTap: () => showNumericInputDialog(
|
||||
context: context,
|
||||
title: title,
|
||||
@@ -182,6 +186,7 @@ class SettingSelectionTile<T, S> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
final svc = _TileBase._svc;
|
||||
return ValueListenableBuilder<S>(
|
||||
valueListenable: svc.listenable(pref),
|
||||
@@ -189,11 +194,11 @@ class SettingSelectionTile<T, S> extends StatelessWidget {
|
||||
final value = decode(raw);
|
||||
return FocusableListTile(
|
||||
leading: AppIcon(icon, fill: 1),
|
||||
title: Text(title),
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: Text(subtitleBuilder(value)),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.standard,
|
||||
dense: isMobile,
|
||||
visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
|
||||
onTap: () async {
|
||||
final picked = await showSelectionDialog<T>(
|
||||
context: context,
|
||||
@@ -233,16 +238,17 @@ class SettingRegexTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
final svc = _TileBase._svc;
|
||||
return ValueListenableBuilder<String>(
|
||||
valueListenable: svc.listenable(pref),
|
||||
builder: (_, value, _) => FocusableListTile(
|
||||
leading: AppIcon(icon, fill: 1),
|
||||
title: Text(title),
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.standard,
|
||||
dense: isMobile,
|
||||
visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
|
||||
onTap: () => showRegexInputDialog(
|
||||
context: context,
|
||||
title: title,
|
||||
@@ -325,12 +331,13 @@ class SettingColorTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
final svc = _TileBase._svc;
|
||||
return ValueListenableBuilder<String>(
|
||||
valueListenable: svc.listenable(pref),
|
||||
builder: (_, hex, _) => FocusableListTile(
|
||||
leading: AppIcon(icon, fill: 1),
|
||||
title: Text(title),
|
||||
title: Text(title, style: settingsOptionTitleStyle(context)),
|
||||
subtitle: subtitle != null ? Text(subtitle!) : null,
|
||||
trailing: Container(
|
||||
width: 28,
|
||||
@@ -341,8 +348,8 @@ class SettingColorTile extends StatelessWidget {
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
|
||||
),
|
||||
),
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.standard,
|
||||
dense: isMobile,
|
||||
visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
|
||||
onTap: () => showColorInputDialog(
|
||||
context: context,
|
||||
title: title,
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/mono_tokens.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import 'app_icon.dart';
|
||||
import 'expressive_button_group.dart';
|
||||
|
||||
/// Standard settings-option title style.
|
||||
///
|
||||
/// Mobile matches Flutter's dense ListTile title size so custom controls and
|
||||
/// ordinary settings rows keep the same hierarchy. Desktop and TV retain the
|
||||
/// larger body style used for D-pad readability.
|
||||
TextStyle? settingsOptionTitleStyle(BuildContext context) {
|
||||
final style = Theme.of(context).textTheme.bodyLarge;
|
||||
return PlatformDetector.isMobile(context) ? style?.copyWith(fontSize: 13) : style;
|
||||
}
|
||||
|
||||
class SettingsSectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
@@ -109,7 +120,7 @@ class SegmentedSetting<T> extends StatelessWidget {
|
||||
children: [
|
||||
AppIcon(icon, fill: 1),
|
||||
const SizedBox(width: 16),
|
||||
Text(title, style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(title, style: settingsOptionTitleStyle(context)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/widgets/focusable_list_tile.dart';
|
||||
import 'package:plezy/widgets/setting_tile.dart';
|
||||
import 'package:plezy/widgets/settings_section.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('mobile settings option titles match compact native rows', (tester) async {
|
||||
await tester.pumpWidget(_harness(TargetPlatform.android, referenceDense: true));
|
||||
|
||||
final referenceHeight = tester.getSize(find.text('Clear Cache')).height;
|
||||
expect(tester.getSize(find.text('View Logs')).height, referenceHeight);
|
||||
expect(tester.getSize(find.text('View Mode')).height, referenceHeight);
|
||||
|
||||
final referenceSubtitleHeight = tester.getSize(find.text('Clear cached data')).height;
|
||||
expect(tester.getSize(find.text('View application logs')).height, referenceSubtitleHeight);
|
||||
});
|
||||
|
||||
testWidgets('desktop settings option titles retain standard row size', (tester) async {
|
||||
await tester.pumpWidget(_harness(TargetPlatform.macOS, referenceDense: false));
|
||||
|
||||
final referenceHeight = tester.getSize(find.text('Clear Cache')).height;
|
||||
expect(tester.getSize(find.text('View Logs')).height, referenceHeight);
|
||||
expect(tester.getSize(find.text('View Mode')).height, referenceHeight);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _harness(TargetPlatform platform, {required bool referenceDense}) {
|
||||
return MaterialApp(
|
||||
theme: monoTheme(dark: false).copyWith(platform: platform),
|
||||
home: Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
SettingNavigationTile(
|
||||
icon: Icons.article,
|
||||
title: 'View Logs',
|
||||
subtitle: 'View application logs',
|
||||
onTap: () {},
|
||||
),
|
||||
FocusableListTile(
|
||||
leading: const Icon(Icons.cleaning_services),
|
||||
title: const Text('Clear Cache'),
|
||||
subtitle: const Text('Clear cached data'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
dense: referenceDense,
|
||||
visualDensity: referenceDense ? const VisualDensity(vertical: -3) : VisualDensity.standard,
|
||||
onTap: () {},
|
||||
),
|
||||
SegmentedSetting<String>(
|
||||
icon: Icons.view_list,
|
||||
title: 'View Mode',
|
||||
segments: const [
|
||||
ButtonSegment(value: 'grid', label: Text('Grid')),
|
||||
ButtonSegment(value: 'list', label: Text('List')),
|
||||
],
|
||||
selected: 'grid',
|
||||
onChanged: (_) {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user