Skip to content

Commit

Permalink
Merge pull request #72 from doudar/Dark-Mode-Fixes
Browse files Browse the repository at this point in the history
Dark mode fixes
  • Loading branch information
doudar authored Dec 23, 2024
2 parents 7ce8cdf + 1f44e3a commit bb3f568
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 104 deletions.
15 changes: 1 addition & 14 deletions lib/screens/workout_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ class _WorkoutScreenState extends State<WorkoutScreen> with TickerProviderStateM
String? _workoutName;
String? _currentWorkoutContent;
late AnimationController _metricsAndSummaryFadeController;
late AnimationController _textEventFadeController;
late Animation<double> _metricsAndSummaryFadeAnimation;
late Animation<double> _textEventFadeAnimation;
late BLEData bleData;
late WorkoutController _workoutController;
late WorkoutTTSSettings _ttsSettings;
Expand All @@ -53,16 +51,11 @@ class _WorkoutScreenState extends State<WorkoutScreen> with TickerProviderStateM

void _initializeAnimationControllers() {
_metricsAndSummaryFadeController = AnimationController(
duration: WorkoutDurations.fadeAnimation,
vsync: this,
);
_textEventFadeController = AnimationController(
duration: WorkoutDurations.textLinger, // Total duration including delay and fade
duration: const Duration(milliseconds: 500),
vsync: this,
);

_metricsAndSummaryFadeAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(_metricsAndSummaryFadeController);
_textEventFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(_textEventFadeController);

_zoomController = AnimationController(
duration: const Duration(milliseconds: 500),
Expand Down Expand Up @@ -93,11 +86,9 @@ class _WorkoutScreenState extends State<WorkoutScreen> with TickerProviderStateM
} else if (_workoutController.isPlaying && mounted) {
// If workout is already playing, forward the animations
_metricsAndSummaryFadeController.forward();
_textEventFadeController.forward();
_zoomController.forward();
} else {
_metricsAndSummaryFadeController.animateBack(0);
_textEventFadeController.animateBack(0);
_zoomController.animateBack(0);
}
});
Expand All @@ -107,12 +98,10 @@ class _WorkoutScreenState extends State<WorkoutScreen> with TickerProviderStateM

if (_workoutController.isPlaying) {
_metricsAndSummaryFadeController.forward();
_textEventFadeController.forward();
_zoomController.forward();
_updateScrollPosition();
} else {
_metricsAndSummaryFadeController.reverse();
_textEventFadeController.reverse();
_zoomController.reverse();
// Check if workout completed naturally (reached the end)
if (_workoutController.progressPosition >= 1.0) {
Expand Down Expand Up @@ -344,7 +333,6 @@ class _WorkoutScreenState extends State<WorkoutScreen> with TickerProviderStateM
void dispose() {
WakelockPlus.disable();
_metricsAndSummaryFadeController.dispose();
_textEventFadeController.dispose();
_zoomController.dispose();
_connectionStateSubscription?.cancel();
bleData.isReadingOrWriting.removeListener(_rwListener);
Expand Down Expand Up @@ -622,7 +610,6 @@ class _WorkoutScreenState extends State<WorkoutScreen> with TickerProviderStateM
? WorkoutTextEventOverlay(
currentSegment: _workoutController.currentSegment,
secondsIntoSegment: _workoutController.currentSegmentElapsedSeconds,
fadeAnimation: _textEventFadeAnimation,
ttsSettings: _ttsSettings,
workoutController: _workoutController,
)
Expand Down
12 changes: 8 additions & 4 deletions lib/utils/workout/workout_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,6 @@ class WorkoutShadows {

/// Duration constants for animations and intervals
class WorkoutDurations {
/// Duration for fade animations
static const Duration fadeAnimation = Duration(milliseconds: 500);
/// How long workout text is displayed on screen
static const Duration textLinger = Duration(seconds: 5);
/// Interval for progress updates
static const Duration progressUpdateInterval = Duration(milliseconds: 100);
///Length of workout prieview
Expand All @@ -123,6 +119,14 @@ class WorkoutDurations {
static const double playingMinutes = 10;
}

/// Text style constants for workout text overlay
class WorkoutTextStyle {
/// Font size for scrolling workout text
static const double scrollingText = 48.0;
/// Speed of text scrolling in pixels per second
static const double scrollSpeed = 100.0;
}

/// Grid constants for the workout graph
class WorkoutGrid {
/// Interval for power grid lines (in watts)
Expand Down
70 changes: 38 additions & 32 deletions lib/utils/workout/workout_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ class WorkoutController extends ChangeNotifier {
trackPoints.clear();
_workoutStartTime = DateTime.now();
}
// Update target power immediately when resuming
_updateTargetPower();
startProgress();
} else {
progressTimer?.cancel();
Expand Down Expand Up @@ -303,6 +305,41 @@ class WorkoutController extends ChangeNotifier {
}
}

void _updateTargetPower() {
if (segments.isEmpty) return;

double currentTime = progressPosition * totalDuration;
double elapsedTime = 0;

for (var segment in segments) {
if (currentTime >= elapsedTime && currentTime < elapsedTime + segment.duration) {
double segmentProgress = (currentTime - elapsedTime) / segment.duration;
double targetPower;

if (segment.isRamp) {
if (segment.type == SegmentType.cooldown) {
// For cooldowns, start at powerHigh and decrease to powerLow
targetPower = segment.powerHigh - (segment.powerHigh - segment.powerLow) * segmentProgress;
} else {
// For all other ramps, start at powerLow and increase to powerHigh
targetPower = segment.powerLow + (segment.powerHigh - segment.powerLow) * segmentProgress;
}
} else {
targetPower = segment.powerLow;
}

// Calculate target power in watts and update ftmsData
// When target power is 0, the BLEData class will handle switching to simulation mode
bleData.ftmsData.targetERG = (targetPower * ftpValue).round();
currentSegmentTimeRemaining = ((elapsedTime + segment.duration) - currentTime).round();

_handleSegmentCountdown(currentSegmentTimeRemaining);
break;
}
elapsedTime += segment.duration;
}
}

void startProgress() {
progressTimer?.cancel();

Expand Down Expand Up @@ -372,38 +409,7 @@ class WorkoutController extends ChangeNotifier {
}

// Update target watts and remaining time based on current position
if (segments.isNotEmpty) {
double currentTime = progressPosition * totalDuration;
double elapsedTime = 0;

for (var segment in segments) {
if (currentTime >= elapsedTime && currentTime < elapsedTime + segment.duration) {
double segmentProgress = (currentTime - elapsedTime) / segment.duration;
double targetPower;

if (segment.isRamp) {
if (segment.type == SegmentType.cooldown) {
// For cooldowns, start at powerHigh and decrease to powerLow
targetPower = segment.powerHigh - (segment.powerHigh - segment.powerLow) * segmentProgress;
} else {
// For all other ramps, start at powerLow and increase to powerHigh
targetPower = segment.powerLow + (segment.powerHigh - segment.powerLow) * segmentProgress;
}
} else {
targetPower = segment.powerLow;
}

// Calculate target power in watts and update ftmsData
// When target power is 0, the BLEData class will handle switching to simulation mode
bleData.ftmsData.targetERG = (targetPower * ftpValue).round();
currentSegmentTimeRemaining = ((elapsedTime + segment.duration) - currentTime).round();

_handleSegmentCountdown(currentSegmentTimeRemaining);
break;
}
elapsedTime += segment.duration;
}
}
_updateTargetPower();

_saveWorkoutState();
if (!_isDisposed) {
Expand Down
144 changes: 109 additions & 35 deletions lib/utils/workout/workout_text_event_overlay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import 'package:flutter/material.dart';
import 'workout_parser.dart';
import 'workout_tts_settings.dart';
import 'workout_controller.dart';
import 'workout_constants.dart';

class WorkoutTextEventOverlay extends StatefulWidget {
final WorkoutSegment? currentSegment;
final int secondsIntoSegment;
final Animation<double> fadeAnimation;
final WorkoutTTSSettings ttsSettings;
final WorkoutController workoutController;

const WorkoutTextEventOverlay({
Key? key,
required this.currentSegment,
required this.secondsIntoSegment,
required this.fadeAnimation,
required this.ttsSettings,
required this.workoutController,
}) : super(key: key);
Expand All @@ -23,55 +22,130 @@ class WorkoutTextEventOverlay extends StatefulWidget {
State<WorkoutTextEventOverlay> createState() => _WorkoutTextEventOverlayState();
}

class _WorkoutTextEventOverlayState extends State<WorkoutTextEventOverlay> {
class _WorkoutTextEventOverlayState extends State<WorkoutTextEventOverlay> with SingleTickerProviderStateMixin {
late AnimationController _scrollController;
late Animation<Offset> _scrollAnimation;
Set<String> _animatedEvents = {}; // Track which events we've already animated

@override
void initState() {
super.initState();
_scrollController = AnimationController(vsync: this)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
// When animation completes, remove this event from animated set
// so it can be animated again if it appears in a future segment
final currentEvents = widget.currentSegment?.textEvents
.where((event) => event.timeOffset <= widget.secondsIntoSegment)
.map((e) => e.message) ?? {};
_animatedEvents.removeWhere((msg) => !currentEvents.contains(msg));
}
});

// Initialize the scroll animation
_scrollAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: const Offset(-1.0, 0.0),
).animate(CurvedAnimation(
parent: _scrollController,
curve: Curves.linear,
));

// Start the animation if we have a segment
if (widget.currentSegment != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_startScrollAnimation(MediaQuery.of(context).size.width);
});
}
}

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

void _startScrollAnimation(double screenWidth) {
// Calculate duration based on screen width and desired speed
final duration = Duration(
milliseconds: ((screenWidth * 2) / WorkoutTextStyle.scrollSpeed * 1000).round()
);

_scrollController.duration = duration;
_scrollController.reset(); // Ensure animation is fully reset
_scrollController.forward();
}

@override
void didUpdateWidget(WorkoutTextEventOverlay oldWidget) {
super.didUpdateWidget(oldWidget);

// Clear spoken messages when segment changes
// Clear spoken messages and animated events when segment changes
if (widget.currentSegment != oldWidget.currentSegment) {
widget.ttsSettings.clearSpokenMessages();
_animatedEvents.clear(); // Reset tracking for new segment
}

if (widget.currentSegment != null) {
// Get new events that we haven't animated yet
final currentEvents = widget.currentSegment!.textEvents
.where((event) => event.timeOffset <= widget.secondsIntoSegment)
.map((e) => e.message);

final newEvents = currentEvents.where((msg) => !_animatedEvents.contains(msg));

if (newEvents.isNotEmpty) {
// Add new events to our tracking set
_animatedEvents.addAll(newEvents);

// Start animation for new events
WidgetsBinding.instance.addPostFrameCallback((_) {
_startScrollAnimation(MediaQuery.of(context).size.width);
});
}
}
}

@override
Widget build(BuildContext context) {
if (widget.currentSegment == null) return const SizedBox.shrink();

final visibleEvents = widget.currentSegment!.getVisibleTextEventsAt(widget.secondsIntoSegment);
if (visibleEvents.isEmpty) return const SizedBox.shrink();
// Only show the most recent text event from the current segment
final currentEvents = widget.currentSegment!.textEvents
.where((event) => event.timeOffset <= widget.secondsIntoSegment)
.toList();

if (currentEvents.isEmpty) return const SizedBox.shrink();

// Speak all visible messages that haven't been spoken yet
for (final event in visibleEvents) {
widget.ttsSettings.speak(event.message);
}
// Get the most recent event
final latestEvent = currentEvents.last;

// Speak only the latest message if it hasn't been spoken yet
widget.ttsSettings.speak(latestEvent.message);

return FadeTransition(
opacity: widget.fadeAnimation,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: visibleEvents.map((event) {
return Opacity(
opacity: event.getOpacityAt(widget.secondsIntoSegment),
child: Text(
event.message,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: const Color.fromARGB(255, 0, 0, 0),
shadows: [
Shadow(
blurRadius: 3.0,
color: const Color.fromARGB(255, 48, 47, 47).withOpacity(0.75),
offset: const Offset(1.0, 1.0),
),
],
),
textAlign: TextAlign.center,
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SlideTransition(
position: _scrollAnimation,
child: Text(
latestEvent.message,
style: TextStyle(
fontSize: WorkoutTextStyle.scrollingText,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
blurRadius: 3.0,
color: const Color.fromARGB(255, 48, 47, 47).withOpacity(0.75),
offset: const Offset(1.0, 1.0),
),
],
),
);
}).toList(),
),
textAlign: TextAlign.center,
),
),
],
),
);
}
Expand Down
Loading

0 comments on commit bb3f568

Please sign in to comment.