fix(settings): align mobile option typography
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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