mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:27:15 +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
101 lines
3.3 KiB
Dart
101 lines
3.3 KiB
Dart
/// Countdown strip permanently reserved at the top of the workout screen.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:workout_app/ui/theme.dart';
|
|
|
|
/// Fixed-height strip showing a rest countdown and a skip button.
|
|
///
|
|
/// The strip is always laid out, rest or no rest: an appearing banner used
|
|
/// to steal its height from the exercise list and shift every tile down
|
|
/// (2026-09-20). Between rests it renders the same shape muted, so starting
|
|
/// a rest changes colours and the number, never a position.
|
|
class BreakBanner extends StatelessWidget {
|
|
/// Creates a [BreakBanner].
|
|
const BreakBanner({
|
|
required this.active,
|
|
required this.breakRemaining,
|
|
required this.breakLabel,
|
|
required this.onSkip,
|
|
super.key,
|
|
});
|
|
|
|
/// Total strip height; a constant so the layout below never depends on
|
|
/// the text metrics of what the strip happens to show.
|
|
static const double height = 64;
|
|
|
|
/// Whether a rest is running. When false the strip is muted and Skip is
|
|
/// hidden (its space is kept).
|
|
final bool active;
|
|
|
|
/// Seconds remaining in the current break.
|
|
final int breakRemaining;
|
|
|
|
/// Display label for the break (e.g. 'Rest' or 'Warmup rest').
|
|
final String breakLabel;
|
|
|
|
/// Called when the user taps the Skip button.
|
|
final VoidCallback onSkip;
|
|
|
|
String _fmt(int secs) {
|
|
final m = (secs ~/ 60).toString().padLeft(2, '0');
|
|
final s = (secs % 60).toString().padLeft(2, '0');
|
|
return '$m:$s';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final status = Theme.of(context).extension<AppStatusColors>()!;
|
|
return Container(
|
|
height: height,
|
|
// Elevation via fill step (ink-raised-2), not a shadow — differentiates
|
|
// the banner from the page without a saturated attention-grabbing fill.
|
|
color: colorScheme.surfaceContainerHighest,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
breakLabel,
|
|
style: TextStyle(
|
|
color: colorScheme.onSurfaceVariant,
|
|
fontSize: AppTextSize.caption,
|
|
),
|
|
),
|
|
Text(
|
|
_fmt(breakRemaining),
|
|
style: TextStyle(
|
|
// Warning (caution/pending) while the countdown runs;
|
|
// muted between rests so the idle strip recedes.
|
|
color: active
|
|
? status.warning
|
|
: colorScheme.onSurfaceVariant,
|
|
fontSize: AppTextSize.title,
|
|
fontWeight: FontWeight.bold,
|
|
fontFeatures: const [FontFeature.tabularFigures()],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Visibility(
|
|
visible: active,
|
|
maintainSize: true,
|
|
maintainAnimation: true,
|
|
maintainState: true,
|
|
child: TextButton(
|
|
onPressed: onSkip,
|
|
child: Text('Skip', style: TextStyle(color: colorScheme.primary)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|