mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 16:53:58 +01:00
Split settings_screen.dart's widgets into part files (771 -> 503)
settings_screen_rows.dart section header, reps row, weight row settings_screen_thresholds.dart stepper button, threshold card and row Still over the cap: the remaining 503 lines are the State class, whose ~190 line build() would have to be broken up to go further. Two routes were tried and reverted rather than shipped half-working: - an extension holding the async actions -- rejected by the analyzer, because setState is @protected and cannot be called from an extension. Worth recording: it rules out moving any State method that calls setState. - an extension exposing the SYNC/OFFLINE BACKUP rows as a widget-list getter. That one is viable (those rows never call setState) but slicing a balanced chunk out of a nested list literal is not a mechanical move, and this screen ships to the phone. 402 Dart tests pass; coverage 100%; dart analyze clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
This commit is contained in:
parent
d418994f6f
commit
338ef5ed9b
@ -22,6 +22,9 @@ import 'package:workout_app/services/progression_sync_service.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
import 'package:workout_app/ui/theme.dart';
|
||||
|
||||
part 'settings_screen_rows.dart';
|
||||
part 'settings_screen_thresholds.dart';
|
||||
|
||||
/// Screen for editing per-exercise thresholds and manual weight overrides.
|
||||
///
|
||||
/// No [BackupSlot] is wired into the shared Sync settings screen: unlike the
|
||||
@ -498,274 +501,3 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.caption,
|
||||
letterSpacing: 1.4,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RepsRow extends StatelessWidget {
|
||||
const _RepsRow({
|
||||
required this.name,
|
||||
required this.reps,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int reps;
|
||||
final ValueChanged<int> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StepperButton(
|
||||
icon: Icons.remove,
|
||||
onTap: () => onChanged((reps - 1).clamp(1, 999)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text(
|
||||
'$reps reps',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: AppTextSize.label,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StepperButton(
|
||||
icon: Icons.add,
|
||||
onTap: () => onChanged((reps + 1).clamp(1, 999)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WeightRow extends StatelessWidget {
|
||||
const _WeightRow({
|
||||
required this.name,
|
||||
required this.weight,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final double weight;
|
||||
final ValueChanged<double> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StepperButton(
|
||||
icon: Icons.remove,
|
||||
onTap: () => onChanged(
|
||||
(weight - kWeightIncrement).clamp(0.0, 999.0),
|
||||
),
|
||||
),
|
||||
// Fixed-width container supports up to "999.9kg" (7 chars).
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text(
|
||||
'${weight}kg',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: AppTextSize.label,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StepperButton(
|
||||
icon: Icons.add,
|
||||
onTap: () => onChanged(weight + kWeightIncrement),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StepperButton extends StatelessWidget {
|
||||
const _StepperButton({required this.icon, required this.onTap});
|
||||
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, color: colorScheme.onSurface, size: 18),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExerciseThresholdCard extends StatelessWidget {
|
||||
const _ExerciseThresholdCard({
|
||||
required this.name,
|
||||
required this.successThreshold,
|
||||
required this.failThreshold,
|
||||
required this.onSuccessChanged,
|
||||
required this.onFailChanged,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int successThreshold;
|
||||
final int failThreshold;
|
||||
final ValueChanged<int> onSuccessChanged;
|
||||
final ValueChanged<int> onFailChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final status = Theme.of(context).extension<AppStatusColors>()!;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: AppTextSize.label,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_ThresholdRow(
|
||||
label: '↑ Increase after N successes',
|
||||
value: successThreshold,
|
||||
color: status.success,
|
||||
onChanged: onSuccessChanged,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_ThresholdRow(
|
||||
label: '↓ Decrease after N failures',
|
||||
value: failThreshold,
|
||||
color: colorScheme.error,
|
||||
onChanged: onFailChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThresholdRow extends StatelessWidget {
|
||||
const _ThresholdRow({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int value;
|
||||
final Color color;
|
||||
final ValueChanged<int> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.caption,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
for (int i = 1; i <= 5; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: GestureDetector(
|
||||
onTap: () => onChanged(i),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: i == value
|
||||
? color
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'$i',
|
||||
style: TextStyle(
|
||||
// on-fill on the selected (filled) circle.
|
||||
color: i == value
|
||||
? colorScheme.onPrimary
|
||||
: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: AppTextSize.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,157 @@
|
||||
// Section header and the reps/weight adjustment rows.
|
||||
//
|
||||
// `part` files: these widgets are library-private and stay that way.
|
||||
// See history_screen_charts.dart for the full reasoning.
|
||||
part of 'settings_screen.dart';
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.caption,
|
||||
letterSpacing: 1.4,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RepsRow extends StatelessWidget {
|
||||
const _RepsRow({
|
||||
required this.name,
|
||||
required this.reps,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int reps;
|
||||
final ValueChanged<int> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StepperButton(
|
||||
icon: Icons.remove,
|
||||
onTap: () => onChanged((reps - 1).clamp(1, 999)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text(
|
||||
'$reps reps',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: AppTextSize.label,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StepperButton(
|
||||
icon: Icons.add,
|
||||
onTap: () => onChanged((reps + 1).clamp(1, 999)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WeightRow extends StatelessWidget {
|
||||
const _WeightRow({
|
||||
required this.name,
|
||||
required this.weight,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final double weight;
|
||||
final ValueChanged<double> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StepperButton(
|
||||
icon: Icons.remove,
|
||||
onTap: () => onChanged(
|
||||
(weight - kWeightIncrement).clamp(0.0, 999.0),
|
||||
),
|
||||
),
|
||||
// Fixed-width container supports up to "999.9kg" (7 chars).
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text(
|
||||
'${weight}kg',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: AppTextSize.label,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StepperButton(
|
||||
icon: Icons.add,
|
||||
onTap: () => onChanged(weight + kWeightIncrement),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StepperButton extends StatelessWidget {
|
||||
const _StepperButton({required this.icon, required this.onTap});
|
||||
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, color: colorScheme.onSurface, size: 18),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
// The stepper button and the per-exercise threshold card.
|
||||
//
|
||||
// See settings_screen_rows.dart for why these are `part` files.
|
||||
part of 'settings_screen.dart';
|
||||
|
||||
class _ExerciseThresholdCard extends StatelessWidget {
|
||||
const _ExerciseThresholdCard({
|
||||
required this.name,
|
||||
required this.successThreshold,
|
||||
required this.failThreshold,
|
||||
required this.onSuccessChanged,
|
||||
required this.onFailChanged,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int successThreshold;
|
||||
final int failThreshold;
|
||||
final ValueChanged<int> onSuccessChanged;
|
||||
final ValueChanged<int> onFailChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final status = Theme.of(context).extension<AppStatusColors>()!;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: AppTextSize.label,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_ThresholdRow(
|
||||
label: '↑ Increase after N successes',
|
||||
value: successThreshold,
|
||||
color: status.success,
|
||||
onChanged: onSuccessChanged,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_ThresholdRow(
|
||||
label: '↓ Decrease after N failures',
|
||||
value: failThreshold,
|
||||
color: colorScheme.error,
|
||||
onChanged: onFailChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThresholdRow extends StatelessWidget {
|
||||
const _ThresholdRow({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int value;
|
||||
final Color color;
|
||||
final ValueChanged<int> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.caption,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
for (int i = 1; i <= 5; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: GestureDetector(
|
||||
onTap: () => onChanged(i),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: i == value
|
||||
? color
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'$i',
|
||||
style: TextStyle(
|
||||
// on-fill on the selected (filled) circle.
|
||||
color: i == value
|
||||
? colorScheme.onPrimary
|
||||
: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: AppTextSize.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user