fix(settings): align mobile option typography

This commit is contained in:
edde746
2026-07-13 22:14:27 +02:00
parent 7400a10c59
commit d6ad6a9506
4 changed files with 102 additions and 20 deletions
@@ -267,7 +267,7 @@ class AppearanceSettingsScreen extends StatelessWidget {
children: [ children: [
const AppIcon(Symbols.grid_view_rounded, fill: 1), const AppIcon(Symbols.grid_view_rounded, fill: 1),
const SizedBox(width: 16), const SizedBox(width: 16),
Text(t.settings.libraryDensity, style: theme.textTheme.bodyLarge), Text(t.settings.libraryDensity, style: settingsOptionTitleStyle(context)),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
+25 -18
View File
@@ -5,6 +5,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../screens/settings/settings_utils.dart'; import '../screens/settings/settings_utils.dart';
import '../services/settings_service.dart'; import '../services/settings_service.dart';
import '../utils/platform_detector.dart';
import 'app_icon.dart'; import 'app_icon.dart';
import 'focusable_list_tile.dart'; import 'focusable_list_tile.dart';
import 'settings_section.dart'; import 'settings_section.dart';
@@ -40,17 +41,18 @@ class SettingSwitchTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = PlatformDetector.isMobile(context);
final svc = _TileBase._svc; final svc = _TileBase._svc;
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
valueListenable: svc.listenable(pref), valueListenable: svc.listenable(pref),
builder: (_, value, _) => FocusableSwitchListTile( builder: (_, value, _) => FocusableSwitchListTile(
focusNode: focusNode, focusNode: focusNode,
secondary: AppIcon(icon, fill: 1), secondary: AppIcon(icon, fill: 1),
title: Text(title), title: Text(title, style: settingsOptionTitleStyle(context)),
subtitle: subtitle != null ? Text(subtitle!) : null, subtitle: subtitle != null ? Text(subtitle!) : null,
value: value, value: value,
dense: false, dense: isMobile,
visualDensity: VisualDensity.standard, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
onChanged: enabled onChanged: enabled
? (v) async { ? (v) async {
await svc.write(pref, v); await svc.write(pref, v);
@@ -86,15 +88,16 @@ class SettingNavigationTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = PlatformDetector.isMobile(context);
return FocusableListTile( return FocusableListTile(
focusNode: focusNode, focusNode: focusNode,
leading: AppIcon(icon, fill: 1), leading: AppIcon(icon, fill: 1),
title: Text(title), title: Text(title, style: settingsOptionTitleStyle(context)),
subtitle: subtitle != null ? Text(subtitle!) : null, subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: AppIcon(trailingIcon, fill: 1), trailing: AppIcon(trailingIcon, fill: 1),
onTap: onTap ?? () => Navigator.push(context, MaterialPageRoute(builder: destinationBuilder!)), onTap: onTap ?? () => Navigator.push(context, MaterialPageRoute(builder: destinationBuilder!)),
dense: false, dense: isMobile,
visualDensity: VisualDensity.standard, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
); );
} }
} }
@@ -126,16 +129,17 @@ class SettingNumberTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = PlatformDetector.isMobile(context);
final svc = _TileBase._svc; final svc = _TileBase._svc;
return ValueListenableBuilder<int>( return ValueListenableBuilder<int>(
valueListenable: svc.listenable(pref), valueListenable: svc.listenable(pref),
builder: (_, value, _) => FocusableListTile( builder: (_, value, _) => FocusableListTile(
leading: AppIcon(icon, fill: 1), leading: AppIcon(icon, fill: 1),
title: Text(title), title: Text(title, style: settingsOptionTitleStyle(context)),
subtitle: Text(subtitleBuilder(value)), subtitle: Text(subtitleBuilder(value)),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
dense: false, dense: isMobile,
visualDensity: VisualDensity.standard, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
onTap: () => showNumericInputDialog( onTap: () => showNumericInputDialog(
context: context, context: context,
title: title, title: title,
@@ -182,6 +186,7 @@ class SettingSelectionTile<T, S> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = PlatformDetector.isMobile(context);
final svc = _TileBase._svc; final svc = _TileBase._svc;
return ValueListenableBuilder<S>( return ValueListenableBuilder<S>(
valueListenable: svc.listenable(pref), valueListenable: svc.listenable(pref),
@@ -189,11 +194,11 @@ class SettingSelectionTile<T, S> extends StatelessWidget {
final value = decode(raw); final value = decode(raw);
return FocusableListTile( return FocusableListTile(
leading: AppIcon(icon, fill: 1), leading: AppIcon(icon, fill: 1),
title: Text(title), title: Text(title, style: settingsOptionTitleStyle(context)),
subtitle: Text(subtitleBuilder(value)), subtitle: Text(subtitleBuilder(value)),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
dense: false, dense: isMobile,
visualDensity: VisualDensity.standard, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
onTap: () async { onTap: () async {
final picked = await showSelectionDialog<T>( final picked = await showSelectionDialog<T>(
context: context, context: context,
@@ -233,16 +238,17 @@ class SettingRegexTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = PlatformDetector.isMobile(context);
final svc = _TileBase._svc; final svc = _TileBase._svc;
return ValueListenableBuilder<String>( return ValueListenableBuilder<String>(
valueListenable: svc.listenable(pref), valueListenable: svc.listenable(pref),
builder: (_, value, _) => FocusableListTile( builder: (_, value, _) => FocusableListTile(
leading: AppIcon(icon, fill: 1), leading: AppIcon(icon, fill: 1),
title: Text(title), title: Text(title, style: settingsOptionTitleStyle(context)),
subtitle: Text(subtitle), subtitle: Text(subtitle),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
dense: false, dense: isMobile,
visualDensity: VisualDensity.standard, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
onTap: () => showRegexInputDialog( onTap: () => showRegexInputDialog(
context: context, context: context,
title: title, title: title,
@@ -325,12 +331,13 @@ class SettingColorTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = PlatformDetector.isMobile(context);
final svc = _TileBase._svc; final svc = _TileBase._svc;
return ValueListenableBuilder<String>( return ValueListenableBuilder<String>(
valueListenable: svc.listenable(pref), valueListenable: svc.listenable(pref),
builder: (_, hex, _) => FocusableListTile( builder: (_, hex, _) => FocusableListTile(
leading: AppIcon(icon, fill: 1), leading: AppIcon(icon, fill: 1),
title: Text(title), title: Text(title, style: settingsOptionTitleStyle(context)),
subtitle: subtitle != null ? Text(subtitle!) : null, subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: Container( trailing: Container(
width: 28, width: 28,
@@ -341,8 +348,8 @@ class SettingColorTile extends StatelessWidget {
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
), ),
), ),
dense: false, dense: isMobile,
visualDensity: VisualDensity.standard, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard,
onTap: () => showColorInputDialog( onTap: () => showColorInputDialog(
context: context, context: context,
title: title, title: title,
+12 -1
View File
@@ -1,8 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/mono_tokens.dart'; import '../theme/mono_tokens.dart';
import '../utils/platform_detector.dart';
import 'app_icon.dart'; import 'app_icon.dart';
import 'expressive_button_group.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 { class SettingsSectionHeader extends StatelessWidget {
final String title; final String title;
@@ -109,7 +120,7 @@ class SegmentedSetting<T> extends StatelessWidget {
children: [ children: [
AppIcon(icon, fill: 1), AppIcon(icon, fill: 1),
const SizedBox(width: 16), const SizedBox(width: 16),
Text(title, style: Theme.of(context).textTheme.bodyLarge), Text(title, style: settingsOptionTitleStyle(context)),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
+64
View File
@@ -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: (_) {},
),
],
),
),
);
}