You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Exercise Reordering Dropped Items at the Wrong Index (Live Workout):LiveWorkoutViewModel.reorderExercise applied the legacy if (oldIndex < newIndex) newIndex -= 1; correction, but its only call site (live_workout_screen.dart) uses ReorderableListView.onReorderItem, which — unlike the obsolete onReorder — already reports newIndex in post-removal coordinates. The decrement was therefore applied twice and every exercise dragged downwards landed one position too high (dragging index 0 to index 2 put it at index 1); upward drags were unaffected, as was the persisted logOrder, which is recomputed from the resulting list. edit_routine_screen.dart and workout_log_detail_screen.dart were already correct. Covered by an updated test plus a new upward-drag case asserting the exact resulting order.
Exercise Cards Jumped and Stuttered After a Drag Was Released: Dropping an exercise triggered three competing layout upheavals inside 250ms across all three reorder screens. onPointerUp expanded every collapsed card the instant the finger lifted; onReorderEnd expanded them again and issued a jumpTo — but that callback fires at the start of the drop animation, and its index is the raw insert index (one too high for downward drags, since the dragged item is still counted at its old position); the reorder itself only lands ~250ms later in _dropCompleted, causing a third relayout. Simultaneously the 800px bottom padding that keeps scroll room during a drag disappeared, clamping the offset. The sequence is now serialized: the list stays collapsed and stable while the SDK animates the card into place, and expands in a single frame once the drop and the reorder have settled (kReorderDropSettleDuration). Since _dragCancel does not emit onReorderEnd, onPointerUp/onPointerCancel schedule the same expansion as a fallback. Applies to edit_routine_screen.dart, live_workout_screen.dart and workout_log_detail_screen.dart.
Scroll Position After Collapse/Expand Was Estimated From Hardcoded Card Heights:_calculateScrollPosition and _calculateScrollAdjustment (duplicated in all three reorder screens) summed magic numbers — 70.0 per ListTile, 48.0 per set row, 48.0 for the add-set button, 80.0 for a notes container, 16.0 for padding, plus a 320.0 header allowance in the log detail screen — to guess where the list would end up. The guess never matched the real layout, so the post-drag jumpTo landed beside the intended position and the list visibly settled into place afterwards. Replaced by ReorderScrollAnchor, which measures the touched card's actual RenderBox position before the relayout and corrects the scroll offset by the exact delta afterwards, keeping the card pinned under the finger both when collapsing (drag start) and expanding (after drop). ~140 lines of height estimation removed.
Recovery Zone Cards Squashed Their Text While Collapsing: The Recovering/Ready/Fresh sections in recovery_tracker_screen.dart used AnimatedCrossFade, which lays both children on top of each other and scales between their sizes. Collapsing therefore compressed the muscle list rather than hiding it: the text reflowed on every frame and the lines visibly stacked on top of one another as the card shrank upwards. Replaced by a new _ExpandReveal widget that keeps the content at its natural layout for the whole animation and only clips it (ClipRect + Align(heightFactor:)), so no reflow can occur. Size uses easeOutCubic opening / easeInCubic closing over 280ms; opacity is offset against it via Interval — fading in only after the section has already opened 30%, and fading out during the first ~45% of the collapse so the content is gone before the clip edge reaches it.
Tapping a Muscle on the Body Model Scrolled to the Wrong Position:_scrollToMuscle ran Scrollable.ensureVisible from an addPostFrameCallback fired immediately after the setState that expands the target zone card — i.e. while the expansion was still animating, so the destination was measured against a layout that was still growing. If the card had been collapsed beforehand the muscle's GlobalKey had no currentContext at all in that frame and the jump silently did nothing. Independently, the screen sets extendBodyBehindAppBar: true, so viewport offset 0 sits behind the app bar; alignment: 0.1 left the muscle's title hidden underneath it. The scroll now waits for the expand animation to settle (or one frame when the card was already open) and computes the target itself via RenderAbstractViewport.getOffsetToReveal, subtracting status bar height + kToolbarHeight + spacing and clamping to the scroll extents, so the muscle heading lands directly below the app bar. Also replaced firstWhere(..., orElse: () => muscles.first), which jumped to an unrelated muscle for an unknown slug and threw on an empty list, with an indexWhere early return.
Undisposed Timers in WorkoutLogDetailScreen:_collapseTimer was never cancelled in dispose(), leaving a pending callback able to fire setState on an unmounted state after leaving the screen mid-press.
Nutrition Hub Scroll Jank on Physical Devices: Fixed janky scrolling on the Nutrition Hub screen, not reproducible on desktop/simulator due to excess GPU memory bandwidth masking the cost. Root cause was AppButton's liquid glass: it builds via AdaptiveGlass without passing quality, so it always runs GlassQuality.standard → LightweightLiquidGlass, which pushes one BackdropFilterLayer per button whenever blur > 0 — a full framebuffer store/load on a tile-based GPU. This is invisible to the app-wide glass-quality knobs (DesignConstants.defaultGlassQuality / minGlassQuality, the adaptive scope wired up in main.dart), which never reach buttons at all, so lowering glass quality previously had zero effect on this screen. Cost scales with the number of buttons visible at once rather than with data volume, which is why Nutrition Hub (recalculate + apply + goals + one button per recipe card) janked while data-heavier screens like Statistics and Diary did not. AppButton now wraps its glass in InheritedLiquidGlass(isBlurProvidedByAncestor: true) with allowElevation: false, taking the package's skipBlur path: the shader still paints tint, rim and Fresnel exactly as before, but no backdrop layer is pushed. Measured on-device via a temporary FrameTiming probe (debug build; iOS Simulators reject --profile): Nutrition Hub raster p50/p90 dropped from ~7.3–8.8/9.7–10.5ms to ~4.3–6.0/5.8–6.7ms, with Workout Hub improving similarly (6.5/8.0 → 4.4/5.4ms). Verified pixel-identical via before/after screenshots diffed with PIL in both Light and Dark mode — the only measurable delta is a single physical anti-aliasing pixel (⅓ logical px at 3x) on the button's outer edge, safe because every button in the app sits on a flat, near-neutral surface (checked scanner, AI capture, and tour overlay screens, which place buttons over camera previews or images, and confirmed none use AppButton).
GlassProgressBar Drop Shadow Ignored the Squircle Silhouette: The bar's outer shadow container used BoxDecoration(borderRadius: BorderRadius.circular(borderRadius)) while the bar body itself is clipped to a SmoothRectangleBorder superellipse. The shadow was therefore cast from a plain circular-radius rectangle whose corners sit slightly outside the squircle, making the corner edges read as ordinary rounded corners rather than the Apple squircle used by SummaryCard. Replaced with ShapeDecoration(shape: squircle) so shadow, background fill and clip path all derive from the same shape object. Visible in Light Mode only — Dark Mode disables the shadow entirely (disableShadow || isDark). The squircle parameters themselves remain 1:1 with SummaryCard (cornerRadius: DesignConstants.borderRadiusL, cornerSmoothing: 0.6).
Added
ReorderScrollAnchor Scroll-Anchoring Utility & Tests: Added lib/features/workout/presentation/reorder_scroll_anchor.dart, a small helper that pins one list item in place while the list relayouts around it. capture(id) records the item's on-screen position via its RenderBox, restore() — called from a post-frame callback — scrolls by exactly the distance it moved, clamped to the scroll extents; keyFor(id) hands out the GlobalKey the card must carry to be measurable. Shared by the three workout screens that collapse their cards during a drag. The accompanying kReorderDropSettleDuration documents the 250ms drop animation SliverReorderableList runs before applying the reorder, which is private in the SDK. Covered by test/reorder_scroll_anchor_test.dart (4 widget tests) against a list with deliberately non-uniform item heights, so a uniform-height shortcut cannot pass by luck, including an assertion that the viewport actually moves — a no-op anchor would otherwise satisfy the pinning check.
GlassProgressBar Intrinsic-Height Regression Test: Added test/widgets/glass_progress_bar_test.dart, which lays the bars out inside the same IntrinsicHeight → Row → Expanded structure that NutritionSummaryWidget uses for the "Today in Focus" grid and asserts a non-zero rendered size. LayoutBuilder throws "LayoutBuilder does not support returning intrinsic dimensions" and resolves to 0 when queried for an intrinsic height, so introducing one at the root of GlassProgressBar.build silently collapses the entire grid to zero height while every other screen keeps rendering normally. A code comment at the shape construction site records the constraint.
Changed
Glass Quality No Longer Degrades on iOS/macOS: The adaptive quality logic behind the floating bottom bar and every other glass surface is provided by GlassAdaptiveScope (liquid_glass_widgets), which benchmarks raster frame times at warmup and keeps stepping quality down at runtime when P95 exceeds the frame budget (thermal throttling, background load). It ran on all platforms: iOS was capped at GlassQuality.premium via maxQuality, but nothing stopped it from dropping to standard or even minimal mid-session, and the settled tier is persisted to SharedPreferences (glass_quality), so a single bad measurement carried over into subsequent cold starts. Added DesignConstants.minGlassQuality, which returns defaultGlassQuality (premium) on iOS/macOS and GlassQuality.minimal elsewhere, and passed it as minQuality to the root GlassAdaptiveScopeConfig in main.dart plus all six nested scopes (main_screen.dart, running_workout_overlay.dart, speed_dial_menu_overlay.dart ×2, live_workout_screen.dart ×2, glass_fab.dart). With min == max == premium on Apple platforms the adapter's floor clamp neutralises every downgrade path — the static capability probe, the warmup decision, runtime degradation and a previously persisted lower tier are all raised back to premium. Android behaviour is unchanged: it stays capped at standard and may still degrade to minimal.