mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 18:07:22 +01:00
The rest banner used to be inserted into the column only while a break ran, so its arrival took height from the exercise list, shifted every tile down and pushed the last one into scroll range. The strip is now a permanently reserved 64 dp slot (muted "Rest / 00:00" while idle, Skip keeps its space), the list is a non-scrolling column that scales down as a whole if it would not fit (0.96 on the Pixel 6a with four tiles, pinned by a widget test at the phone's logical size with the status bar simulated), and the activity is locked to portrait. Verified on the real phone: only the tapped circle's pixels change when a rest starts. Verifying that needed a workout screen that today's already-logged workout did not offer, so the app gained a `sandbox` product flavor beside `daily`: com.kuhy.workout_app.sandbox installs side by side with its own data, refuses every network connection at the socket layer, starts no LAN server for the PC to pull from, keeps its /sdcard files beside the daily build's, wears a red SANDBOX ribbon, and offers a SANDBOX section in Settings (wipe, mark today not done, rest length in seconds). MainActivity answers the flavor and carries a `WorkoutSandbox` logcat trace of every tap, break, save and navigation. On Linux the same mode is WORKOUT_SANDBOX=1, driven under Xvfb by scripts/run_sandbox_xvfb.sh. CI builds --flavor daily; .phone-deploy-flavor tells phone_deploy.sh the same. Set circles and the home icon buttons carry semantics labels so they can be addressed by element (and by screen readers). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YbBZqUaNWyhHYfzz6zGC7q
146 lines
4.7 KiB
Dart
146 lines
4.7 KiB
Dart
/// Card widget for a single exercise showing warmup and main-set rep circles.
|
||
library;
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:workout_app/models/exercise.dart';
|
||
import 'package:workout_app/ui/theme.dart';
|
||
import 'package:workout_app/widgets/rep_circle.dart';
|
||
|
||
part 'exercise_tile_rows.dart';
|
||
|
||
/// Card widget displaying warmup and working-set rep circles for one exercise.
|
||
class ExerciseTile extends StatelessWidget {
|
||
/// Creates an [ExerciseTile].
|
||
const ExerciseTile({
|
||
required this.exercise,
|
||
required this.tapped,
|
||
required this.doneReps,
|
||
required this.warmupTapped,
|
||
required this.successThreshold,
|
||
required this.failThreshold,
|
||
required this.onTapCircle,
|
||
required this.onLongPressCircle,
|
||
required this.onTapWarmup,
|
||
required this.onThresholdChanged,
|
||
super.key,
|
||
});
|
||
|
||
/// The exercise definition to display.
|
||
final Exercise exercise;
|
||
|
||
/// Per-set tap state; true when a set circle has been tapped.
|
||
final List<bool> tapped;
|
||
|
||
/// Per-set rep count; may be less than target after repeated taps.
|
||
final List<int> doneReps;
|
||
|
||
/// Whether the warmup circle has been tapped.
|
||
final bool warmupTapped;
|
||
|
||
/// Success streak threshold shown in the inline settings row.
|
||
final int successThreshold;
|
||
|
||
/// Fail streak threshold shown in the inline settings row.
|
||
final int failThreshold;
|
||
|
||
/// Called when a working-set circle is tapped.
|
||
final void Function(int setIdx) onTapCircle;
|
||
|
||
/// Called when a working-set circle is long-pressed (resets to neutral).
|
||
final void Function(int setIdx) onLongPressCircle;
|
||
|
||
/// Called when the warmup circle is tapped.
|
||
final VoidCallback onTapWarmup;
|
||
|
||
/// Called when the user changes thresholds inline (newSuccess, newFail).
|
||
final void Function(int success, int fail) onThresholdChanged;
|
||
|
||
bool get _allCompleted => tapped.every((t) => t);
|
||
|
||
bool get _allSucceeded =>
|
||
_allCompleted && doneReps.every((r) => r >= exercise.reps);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
final status = Theme.of(context).extension<AppStatusColors>()!;
|
||
var headerColor = colorScheme.surfaceContainerHigh;
|
||
if (_allCompleted) {
|
||
headerColor = _allSucceeded ? status.success : colorScheme.error;
|
||
}
|
||
// A filled success/danger card needs on-fill text throughout (tokens.md:
|
||
// one on-fill value for all four fills, never a per-fill judgment call).
|
||
final onHeader = _allCompleted
|
||
? colorScheme.onPrimary
|
||
: colorScheme.onSurface;
|
||
final onHeaderMuted = _allCompleted
|
||
? colorScheme.onPrimary
|
||
: colorScheme.onSurfaceVariant;
|
||
|
||
return Card(
|
||
color: headerColor,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
exercise.name,
|
||
style: TextStyle(
|
||
color: onHeader,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: AppTextSize.body,
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'${exercise.sets}×${exercise.reps}×${exercise.weight}kg',
|
||
style: TextStyle(
|
||
color: onHeaderMuted,
|
||
fontSize: AppTextSize.label,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
if (exercise.hasWarmup) ...[
|
||
_WarmupRow(
|
||
warmupWeight: exercise.warmupWeight,
|
||
tapped: warmupTapped,
|
||
onTap: onTapWarmup,
|
||
),
|
||
const SizedBox(height: 10),
|
||
],
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: List.generate(
|
||
exercise.sets,
|
||
(s) => RepCircle(
|
||
semanticLabel: '${exercise.name} set ${s + 1}',
|
||
targetReps: exercise.reps,
|
||
doneReps: doneReps[s],
|
||
tapped: tapped[s],
|
||
onTap: () => onTapCircle(s),
|
||
onLongPress: () => onLongPressCircle(s),
|
||
),
|
||
),
|
||
),
|
||
// Color inherited from the shared dividerTheme (line-dark).
|
||
const Divider(height: 20),
|
||
_ThresholdRow(
|
||
successThreshold: successThreshold,
|
||
failThreshold: failThreshold,
|
||
onSuccessChanged: (v) => onThresholdChanged(v, failThreshold),
|
||
onFailChanged: (v) => onThresholdChanged(successThreshold, v),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|