Skip to content

Commit

Permalink
Merge pull request #67 from GabinL21/add-session-stats
Browse files Browse the repository at this point in the history
feat(stats): add session stats
  • Loading branch information
GabinL21 authored Aug 11, 2023
2 parents f21bd2d + 59ff8a7 commit 1d00774
Show file tree
Hide file tree
Showing 24 changed files with 800 additions and 60 deletions.
11 changes: 10 additions & 1 deletion lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:kubrs_app/gui/bloc/gui_bloc.dart';
import 'package:kubrs_app/l10n/l10n.dart';
import 'package:kubrs_app/nav/bloc/navigation_bloc.dart';
import 'package:kubrs_app/scramble/bloc/scramble_bloc.dart';
import 'package:kubrs_app/session/bloc/session_bloc.dart';
import 'package:kubrs_app/solve/bloc/solve_bloc.dart';
import 'package:kubrs_app/solve/repository/solve_repository.dart';
import 'package:kubrs_app/user/bloc/user_bloc.dart';
Expand Down Expand Up @@ -68,6 +69,9 @@ class App extends StatelessWidget {
}

Widget _getScaffold(BuildContext context) {
final solveBloc = SolveBloc(
solveRepository: RepositoryProvider.of<SolveRepository>(context),
);
return MultiBlocProvider(
providers: [
BlocProvider(
Expand All @@ -82,7 +86,12 @@ class App extends StatelessWidget {
),
BlocProvider(
// Keeps the same solve even if you switch to another page
create: (_) => SolveBloc(
create: (_) => solveBloc,
),
BlocProvider(
// Maintains the session start date time
create: (_) => SessionBloc(
solveBloc: solveBloc,
solveRepository: RepositoryProvider.of<SolveRepository>(context),
),
),
Expand Down
2 changes: 1 addition & 1 deletion lib/history/view/solve_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class SolveTile extends StatelessWidget {

Text _getTimeText(BuildContext context) {
return Text(
solve.getTimeToDisplay(),
solve.timeToDisplay,
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w700,
),
Expand Down
28 changes: 28 additions & 0 deletions lib/session/bloc/session_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:kubrs_app/solve/bloc/solve_bloc.dart';
import 'package:kubrs_app/solve/model/solve.dart';
import 'package:kubrs_app/solve/repository/solve_repository.dart';

part 'session_event.dart';
part 'session_state.dart';

class SessionBloc extends Bloc<SessionEvent, SessionState> {
SessionBloc({required this.solveBloc, required this.solveRepository})
: super(SessionInitial()) {
solveRepository
.getStreamOfSolvesSince(sessionStart)
.listen((_) => add(const RefreshSessionSolves()));
on<RefreshSessionSolves>((event, emit) async {
emit(SessionLoading(state.solves));
final sessionSolves = await solveRepository.getSolvesSince(
sessionStart,
offline: true, // Limit Firestore server calls
);
emit(SessionLoaded(sessionSolves));
});
}
final SolveBloc solveBloc;
final SolveRepository solveRepository;
final DateTime sessionStart = DateTime.now();
}
12 changes: 12 additions & 0 deletions lib/session/bloc/session_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
part of 'session_bloc.dart';

abstract class SessionEvent extends Equatable {
const SessionEvent();

@override
List<Object> get props => [];
}

class RefreshSessionSolves extends SessionEvent {
const RefreshSessionSolves();
}
22 changes: 22 additions & 0 deletions lib/session/bloc/session_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
part of 'session_bloc.dart';

abstract class SessionState extends Equatable {
const SessionState(this.solves);

final List<Solve> solves;

@override
List<Object> get props => [solves];
}

class SessionInitial extends SessionState {
SessionInitial() : super(List.empty());
}

class SessionLoading extends SessionState {
const SessionLoading(super.solves);
}

class SessionLoaded extends SessionState {
const SessionLoaded(super.solves);
}
82 changes: 82 additions & 0 deletions lib/session/view/session_stats.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kubrs_app/session/bloc/session_bloc.dart';
import 'package:kubrs_app/solve/model/solve.dart';
import 'package:kubrs_app/stats/utils/stats_calculator.dart';

class SessionStats extends StatelessWidget {
const SessionStats({super.key});

@override
Widget build(BuildContext context) {
return BlocBuilder<SessionBloc, SessionState>(
builder: (context, timerState) {
final solves = timerState.solves;
final textStyle = _getTextStyle(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_getSolveCountStatText(solves, textStyle),
_getBestSolveText(solves, textStyle),
_getWorstSolveText(solves, textStyle),
_getSolveMeanText(solves, textStyle),
_getLastAverageText(solves, textStyle),
],
);
},
);
}

TextStyle? _getTextStyle(BuildContext context) {
return Theme.of(context).textTheme.displaySmall?.copyWith(
color: Theme.of(context).colorScheme.secondary,
);
}

Widget _getSolveCountStatText(List<Solve> solves, TextStyle? textStyle) {
final count = solves.length;
return Text(
'Count: $count',
style: textStyle,
);
}

Widget _getBestSolveText(List<Solve> solves, TextStyle? textStyle) {
final bestStat = StatsCalculator.computeBest(solves);
final statName = bestStat.displayedName;
final statValue = bestStat.displayedValue;
return Text(
'$statName: $statValue',
style: textStyle,
);
}

Widget _getWorstSolveText(List<Solve> solves, TextStyle? textStyle) {
final worstStat = StatsCalculator.computeWorst(solves);
final statName = worstStat.displayedName;
final statValue = worstStat.displayedValue;
return Text(
'$statName: $statValue',
style: textStyle,
);
}

Widget _getSolveMeanText(List<Solve> solves, TextStyle? textStyle) {
final meanStat = StatsCalculator.computeMean(solves);
final statValue = meanStat.displayedValue;
return Text(
'Mean: $statValue',
style: textStyle,
);
}

Widget _getLastAverageText(List<Solve> solves, TextStyle? textStyle) {
final averageStat = StatsCalculator.computeAverage(solves, 5);
final statName = averageStat.displayedName;
final statValue = averageStat.displayedValue;
return Text(
'Last $statName: $statValue',
style: textStyle,
);
}
}
25 changes: 13 additions & 12 deletions lib/solve/model/solve.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ class Solve {
final bool plusTwo;
final bool dnf;

Duration get effectiveTime {
if (dnf) return const Duration(minutes: 10);
if (plusTwo) return time + plusTwoDuration;
return time;
}

String get timeToDisplay {
if (dnf) return 'DNF';
final formattedTime = DurationFormatter.format(time);
if (plusTwo) return '$formattedTime+2';
return formattedTime;
}

Map<String, dynamic> toJson() {
return {
'uid': uid,
Expand All @@ -72,16 +85,4 @@ class Solve {
'dnf': dnf,
};
}

Duration getEffectiveTime() {
if (plusTwo) return time + plusTwoDuration;
return time;
}

String getTimeToDisplay() {
if (dnf) return 'DNF';
final formattedTime = DurationFormatter.format(time);
if (plusTwo) return '$formattedTime+2';
return formattedTime;
}
}
21 changes: 21 additions & 0 deletions lib/solve/repository/solve_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ class SolveRepository {
return snapshot.docs.map((doc) => Solve.fromJson(doc.data())).toList();
}

Future<List<Solve>> getSolvesSince(
DateTime dateTime, {
bool offline = false,
}) async {
final source = offline ? Source.cache : Source.serverAndCache;
final snapshot = await _solvesCollection
.where('uid', isEqualTo: _uid)
.orderBy('timestamp', descending: true)
.endAt([dateTime]).get(GetOptions(source: source));
return snapshot.docs.map((doc) => Solve.fromJson(doc.data())).toList();
}

Stream<QuerySnapshot<Map<String, dynamic>>> getStreamOfSolvesSince(
DateTime dateTime,
) {
return _solvesCollection
.where('uid', isEqualTo: _uid)
.orderBy('timestamp', descending: true)
.endAt([dateTime]).snapshots();
}

Future<void> updateLastSolve(Solve updatedSolve) async {
final lastSolveDocId =
await _getLastSolveDocumentId(updatedSolve.timestamp);
Expand Down
20 changes: 14 additions & 6 deletions lib/solve/utils/duration_formatter.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
class DurationFormatter {
static String format(Duration duration) {
final minutesStr = duration.inMinutes.toString();
final roundedDuration = _roundDuration(duration);
final minutesStr = roundedDuration.inMinutes.toString();
final secondsStr =
duration.inSeconds.remainder(60).toString().padLeft(2, '0');
final millisecondsStr = (duration.inMilliseconds.remainder(1000) / 10)
.floor()
.toString()
.padLeft(2, '0');
roundedDuration.inSeconds.remainder(60).toString().padLeft(2, '0');
final millisecondsStr =
(roundedDuration.inMilliseconds.remainder(1000) / 10)
.round()
.toString()
.padLeft(2, '0');
var textStr = '$secondsStr.$millisecondsStr';
if (duration >= const Duration(minutes: 1)) {
textStr = '$minutesStr:$textStr';
}
return textStr;
}

static Duration _roundDuration(Duration duration) {
final milliseconds = duration.inMilliseconds;
final roundedMilliseconds = (milliseconds / 10).round() * 10;
return Duration(milliseconds: roundedMilliseconds);
}
}
33 changes: 33 additions & 0 deletions lib/stats/model/average_stat.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:equatable/equatable.dart';
import 'package:kubrs_app/solve/utils/duration_formatter.dart';
import 'package:kubrs_app/stats/model/stat.dart';

class AverageStat extends Stat with EquatableMixin {
AverageStat(this._value, this._nbSolves) : _dnf = false;
AverageStat.empty(this._nbSolves)
: _value = null,
_dnf = false;
AverageStat.dnf(this._nbSolves)
: _value = null,
_dnf = true;

final int? _value;
final int _nbSolves;
final bool _dnf;

@override
String get displayedName {
return 'Ao$_nbSolves';
}

@override
String get displayedValue {
if (_dnf) return 'DNF';
if (_value == null) return '-';
final duration = Duration(milliseconds: _value!);
return DurationFormatter.format(duration);
}

@override
List<Object?> get props => [_nbSolves, _value, _dnf];
}
32 changes: 32 additions & 0 deletions lib/stats/model/best_stat.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
import 'package:kubrs_app/solve/utils/duration_formatter.dart';
import 'package:kubrs_app/stats/model/stat.dart';

class BestStat extends Stat with EquatableMixin {
BestStat(this._value) : _dnf = false;
BestStat.empty()
: _value = null,
_dnf = false;
BestStat.dnf()
: _value = null,
_dnf = true;

final int? _value;
final bool _dnf;

@override
String get displayedName {
return 'Best';
}

@override
String get displayedValue {
if (_dnf) return 'DNF';
if (_value == null) return '-';
final duration = Duration(milliseconds: _value!);
return DurationFormatter.format(duration);
}

@override
List<Object?> get props => [_value, _dnf];
}
33 changes: 33 additions & 0 deletions lib/stats/model/mean_stat.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:equatable/equatable.dart';
import 'package:kubrs_app/solve/utils/duration_formatter.dart';
import 'package:kubrs_app/stats/model/stat.dart';

class MeanStat extends Stat with EquatableMixin {
MeanStat(this._value, this._nbSolves) : _dnf = false;
MeanStat.empty(this._nbSolves)
: _value = null,
_dnf = false;
MeanStat.dnf(this._nbSolves)
: _value = null,
_dnf = true;

final int? _value;
final int _nbSolves;
final bool _dnf;

@override
String get displayedName {
return 'Mo$_nbSolves';
}

@override
String get displayedValue {
if (_dnf) return 'DNF';
if (_value == null) return '-';
final duration = Duration(milliseconds: _value!);
return DurationFormatter.format(duration);
}

@override
List<Object?> get props => [_nbSolves, _value, _dnf];
}
11 changes: 11 additions & 0 deletions lib/stats/model/stat.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
abstract class Stat {
String get displayedName;
String get displayedValue;

@override
String toString() {
final name = displayedName;
final score = displayedValue;
return '$name: $score';
}
}
Loading

0 comments on commit 1d00774

Please sign in to comment.