store new construct uses locally. use a combination of those and stored analytics events to update mini analytics UI.

pull/1384/head
ggurdin 1 year ago
parent a1a1d92f54
commit 4ede7c9bdd
No known key found for this signature in database
GPG Key ID: A01CB41737CBB478

@ -636,7 +636,15 @@ class ChatController extends State<ChatPageWithRoom>
// analytics based on when / how many messages the logged in user send. This
// stream sends the data for newly sent messages.
if (msgEventId != null) {
pangeaController.myAnalytics.setState(data: {'eventID': msgEventId});
pangeaController.myAnalytics.setState(
data: {
'eventID': msgEventId,
'roomID': room.id,
'originalSent': originalSent,
'tokensSent': tokensSent,
'choreo': choreo,
},
);
}
if (previousEdit != null) {

@ -50,8 +50,10 @@ class StartIGCButtonState extends State<StartIGCButton>
_controller?.stop();
_controller?.reverse();
}
if (mounted) {
setState(() => prevState = assistanceState);
}
}
bool get itEnabled => widget.controller.choreographer.itEnabled;
bool get igcEnabled => widget.controller.choreographer.igcEnabled;

@ -0,0 +1,212 @@
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// A minimized version of AnalyticsController that get the logged in user's analytics
class GetAnalyticsController {
late PangeaController _pangeaController;
final List<AnalyticsCacheEntry> _cache = [];
GetAnalyticsController(PangeaController pangeaController) {
_pangeaController = pangeaController;
}
String? get l2Code => _pangeaController.languageController.userL2?.langCode;
// A local cache of eventIds and construct uses for messages sent since the last update
Map<String, List<OneConstructUse>> get messagesSinceUpdate {
try {
final dynamic locallySaved = _pangeaController.pStoreService.read(
PLocalKey.messagesSinceUpdate,
);
if (locallySaved == null) {
_pangeaController.myAnalytics.setMessagesSinceUpdate({});
return {};
}
try {
// try to get the local cache of messages and format them as OneConstructUses
final Map<String, List<dynamic>> cache =
Map<String, List<dynamic>>.from(locallySaved);
final Map<String, List<OneConstructUse>> formattedCache = {};
for (final entry in cache.entries) {
formattedCache[entry.key] =
entry.value.map((e) => OneConstructUse.fromJson(e)).toList();
}
return formattedCache;
} catch (err) {
// if something goes wrong while trying to format the local data, clear it
_pangeaController.myAnalytics.setMessagesSinceUpdate({});
return {};
}
} catch (exception, stackTrace) {
ErrorHandler.logError(
e: PangeaWarningError(
"Failed to get messages since update: $exception",
),
s: stackTrace,
m: 'Failed to retrieve messages since update',
);
return {};
}
}
/// Get a list of all the construct analytics events
/// for the logged in user in their current L2
Future<List<ConstructAnalyticsEvent>?> getConstructs({
bool forceUpdate = false,
ConstructTypeEnum? constructType,
}) async {
debugPrint("getting constructs");
await _pangeaController.matrixState.client.roomsLoading;
final DateTime? lastUpdated = await myAnalyticsLastUpdated();
final List<ConstructAnalyticsEvent>? local = getConstructsLocal(
constructType: constructType,
lastUpdated: lastUpdated,
);
if (local != null && !forceUpdate) {
debugPrint("returning local constructs");
return local;
}
debugPrint("fetching new constructs");
final unfilteredConstructs = await allMyConstructs();
final filteredConstructs = await filterConstructs(
unfilteredConstructs: unfilteredConstructs,
);
if (local == null) {
cacheConstructs(
constructType: constructType,
events: filteredConstructs,
);
}
return filteredConstructs;
}
/// Get the last time the user updated their analytics for their current l2
Future<DateTime?> myAnalyticsLastUpdated() async {
if (l2Code == null) return null;
final Room? analyticsRoom =
_pangeaController.matrixState.client.analyticsRoomLocal(l2Code!);
if (analyticsRoom == null) return null;
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
_pangeaController.matrixState.client.userID!,
);
return lastUpdated;
}
/// Get the cached construct analytics events for the current user, if it exists
List<ConstructAnalyticsEvent>? getConstructsLocal({
DateTime? lastUpdated,
ConstructTypeEnum? constructType,
}) {
final index = _cache.indexWhere(
(e) => e.type == constructType && e.langCode == l2Code,
);
if (index > -1) {
if (_cache[index].needsUpdate(lastUpdated)) {
_cache.removeAt(index);
return null;
}
return _cache[index].events;
}
return null;
}
/// Get all the construct analytics events for the logged in user
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
if (l2Code == null) return [];
final Room? analyticsRoom =
_pangeaController.matrixState.client.analyticsRoomLocal(l2Code!);
if (analyticsRoom == null) return [];
return await analyticsRoom.getAnalyticsEvents(
userId: _pangeaController.matrixState.client.userID!,
) ??
[];
}
/// Filter out constructs that are not relevant to the user, specifically those from
/// rooms in which the user is a teacher and those that are interative translation span constructs
Future<List<ConstructAnalyticsEvent>> filterConstructs({
required List<ConstructAnalyticsEvent> unfilteredConstructs,
}) async {
final List<String> adminSpaceRooms =
await _pangeaController.matrixState.client.teacherRoomIds;
for (final construct in unfilteredConstructs) {
construct.content.uses.removeWhere(
(use) {
if (adminSpaceRooms.contains(use.chatId)) {
return true;
}
return use.lemma == "Try interactive translation" ||
use.lemma == "itStart" ||
use.lemma == MatchRuleIds.interactiveTranslation;
},
);
}
unfilteredConstructs.removeWhere((e) => e.content.uses.isEmpty);
return unfilteredConstructs;
}
/// Cache the construct analytics events for the current user
void cacheConstructs({
required List<ConstructAnalyticsEvent> events,
ConstructTypeEnum? constructType,
}) {
if (l2Code == null) return;
final entry = AnalyticsCacheEntry(
type: constructType,
events: List.from(events),
langCode: l2Code!,
);
_cache.add(entry);
}
}
class AnalyticsCacheEntry {
final String langCode;
final ConstructTypeEnum? type;
final List<ConstructAnalyticsEvent> events;
late final DateTime _createdAt;
AnalyticsCacheEntry({
required this.langCode,
required this.type,
required this.events,
}) {
_createdAt = DateTime.now();
}
bool get isExpired =>
DateTime.now().difference(_createdAt).inMinutes >
ClassDefaultValues.minutesDelayToMakeNewChartAnalytics;
bool needsUpdate(DateTime? lastEventUpdated) {
// cache entry is invalid if it's older than the last event update
// if lastEventUpdated is null, that would indicate that no events
// of this type have been sent to the room. In this case, there
// shouldn't be any cached data.
if (lastEventUpdated == null) {
Sentry.addBreadcrumb(
Breadcrumb(message: "lastEventUpdated is null in needsUpdate"),
);
return false;
}
return _createdAt.isBefore(lastEventUpdated);
}
}

@ -10,16 +10,19 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_e
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// handles the processing of analytics for
/// 1) messages sent by the user and
/// 2) constructs used by the user, both in sending messages and doing practice activities
class MyAnalyticsController extends BaseController {
late PangeaController _pangeaController;
final StreamController analyticsUpdateStream = StreamController.broadcast();
Timer? _updateTimer;
/// the max number of messages that will be cached before
@ -38,10 +41,8 @@ class MyAnalyticsController extends BaseController {
// Listen to a stream that provides the eventIDs
// of new messages sent by the logged in user
stateStream
.where((data) => data is Map && data.containsKey("eventID"))
.listen((data) {
updateAnalyticsTimer(data['eventID']);
stateStream.where((data) => data is Map).listen((data) {
onMessageSent(data as Map<String, dynamic>);
});
}
@ -67,11 +68,9 @@ class MyAnalyticsController extends BaseController {
Client get _client => _pangeaController.matrixState.client;
/// Given an newly sent message, reset the timer
/// and add the event ID to the cache of un-added event IDs
void updateAnalyticsTimer(String newEventId) {
addMessageSinceUpdate(newEventId);
/// Given the data from a newly sent message, format and cache
/// the message's construct data locally and reset the update timer
void onMessageSent(Map<String, dynamic> data) {
// cancel the last timer that was set on message event and
// reset it to fire after _minutesBeforeUpdate minutes
_updateTimer?.cancel();
@ -79,97 +78,88 @@ class MyAnalyticsController extends BaseController {
debugPrint("timer fired, updating analytics");
updateAnalytics();
});
// extract the relevant data about this message
final String? eventID = data['eventID'];
final String? roomID = data['roomID'];
final PangeaRepresentation? originalSent = data['originalSent'];
final PangeaMessageTokens? tokensSent = data['tokensSent'];
final ChoreoRecord? choreo = data['choreo'];
if (roomID == null || eventID == null) return;
// convert that data into construct uses and add it to the cache
final metadata = ConstructUseMetaData(
roomId: roomID,
eventId: eventID,
timeStamp: DateTime.now(),
);
final grammarConstructs = choreo?.grammarConstructUses(metadata: metadata);
final itConstructs = choreo?.itStepsToConstructUses(metadata: metadata);
final vocabUses = tokensSent != null
? originalSent?.vocabUses(
choreo: choreo,
tokens: tokensSent.tokens,
metadata: metadata,
)
: null;
final List<OneConstructUse> constructs = [
...(grammarConstructs ?? []),
...(itConstructs ?? []),
...(vocabUses ?? []),
];
addMessageSinceUpdate(
eventID,
constructs,
);
}
// adds an event ID to the cache of un-added event IDs
// if the event IDs isn't already added
void addMessageSinceUpdate(String eventId) {
/// Add a list of construct uses for a new message to the local
/// cache of recently sent messages
void addMessageSinceUpdate(
String eventID,
List<OneConstructUse> constructs,
) {
try {
final List<String> currentCache = messagesSinceUpdate;
if (!currentCache.contains(eventId)) {
currentCache.add(eventId);
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
currentCache,
);
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
if (!currentCache.containsKey(eventID)) {
currentCache[eventID] = constructs;
setMessagesSinceUpdate(currentCache);
}
// if the cached has reached if max-length, update analytics
if (messagesSinceUpdate.length > _maxMessagesCached) {
if (_pangeaController.analytics.messagesSinceUpdate.length >
_maxMessagesCached) {
debugPrint("reached max messages, updating");
updateAnalytics();
}
} catch (exception, stackTrace) {
} catch (e, s) {
ErrorHandler.logError(
e: PangeaWarningError("Failed to add message since update: $exception"),
s: stackTrace,
m: 'Failed to add message since update for eventId: $eventId',
);
Sentry.captureException(
exception,
stackTrace: stackTrace,
withScope: (scope) {
scope.setExtra(
'extra_info',
'Failed during addMessageSinceUpdate with eventId: $eventId',
);
scope.setTag('where', 'addMessageSinceUpdate');
},
e: PangeaWarningError("Failed to add message since update: $e"),
s: s,
m: 'Failed to add message since update for eventId: $eventID',
);
}
}
// called before updating analytics
/// Clears the local cache of recently sent constructs. Called before updating analytics
void clearMessagesSinceUpdate() {
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
[],
);
setMessagesSinceUpdate({});
}
// a local cache of eventIds for messages sent since the last update
// it's possible for this cache to be invalid or deleted
// It's a proxy measure for messages sent since last update
List<String> get messagesSinceUpdate {
try {
Logs().d('Reading messages since update from local storage');
final dynamic locallySaved = _pangeaController.pStoreService.read(
PLocalKey.messagesSinceUpdate,
);
if (locallySaved == null) {
Logs().d('No locally saved messages found, initializing empty list.');
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
[],
);
return [];
/// Save the local cache of recently sent constructs to the local storage
void setMessagesSinceUpdate(Map<String, List<OneConstructUse>> cache) {
final formattedCache = {};
for (final entry in cache.entries) {
final constructJsons = entry.value.map((e) => e.toJson()).toList();
formattedCache[entry.key] = constructJsons;
}
return locallySaved.cast<String>();
} catch (exception, stackTrace) {
ErrorHandler.logError(
e: PangeaWarningError(
"Failed to get messages since update: $exception",
),
s: stackTrace,
m: 'Failed to retrieve messages since update',
);
Sentry.captureException(
exception,
stackTrace: stackTrace,
withScope: (scope) {
scope.setExtra(
'extra_info',
'Error during messagesSinceUpdate getter',
);
scope.setTag('where', 'messagesSinceUpdate');
},
);
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
[],
formattedCache,
);
return [];
}
analyticsUpdateStream.add(null);
}
Completer<void>? _updateCompleter;
@ -182,6 +172,7 @@ class MyAnalyticsController extends BaseController {
try {
await _updateAnalytics();
clearMessagesSinceUpdate();
analyticsUpdateStream.add(null);
} catch (err, s) {
ErrorHandler.logError(
e: err,

@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/class_controller.dart';
import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart';
import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/language_controller.dart';
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
@ -35,7 +36,6 @@ import '../../config/app_config.dart';
import '../utils/firebase_analytics.dart';
import '../utils/p_store.dart';
import 'it_feedback_controller.dart';
import 'message_analytics_controller.dart';
class PangeaController {
///pangeaControllers
@ -43,7 +43,8 @@ class PangeaController {
late LanguageController languageController;
late ClassController classController;
late PermissionsController permissionsController;
late AnalyticsController analytics;
// late AnalyticsController analytics;
late GetAnalyticsController analytics;
late MyAnalyticsController myAnalytics;
late WordController wordNet;
late MessageDataController messageData;
@ -91,7 +92,8 @@ class PangeaController {
languageController = LanguageController(this);
classController = ClassController(this);
permissionsController = PermissionsController(this);
analytics = AnalyticsController(this);
// analytics = AnalyticsController(this);
analytics = GetAnalyticsController(this);
myAnalytics = MyAnalyticsController(this);
messageData = MessageDataController(this);
wordNet = WordController(this);

@ -0,0 +1,50 @@
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
/// A wrapper around a list of [OneConstructUse]s, used to simplify
/// the process of filtering / sorting / displaying the events.
/// Takes a construct type and a list of events
class ConstructListModel {
ConstructTypeEnum type;
List<OneConstructUse> uses;
ConstructListModel({
required this.type,
required this.uses,
});
/// All unique lemmas used in the construct events
List<String> get lemmas => constructs.map((e) => e.lemma).toSet().toList();
/// A list of ConstructUses, each of which contains a lemma and
/// a list of uses, sorted by the number of uses
List<ConstructUses> get constructs {
final List<OneConstructUse> filtered =
uses.where((use) => use.constructType == type).toList();
final Map<String, List<OneConstructUse>> lemmaToUses = {};
for (final use in filtered) {
if (use.lemma == null) continue;
lemmaToUses[use.lemma!] ??= [];
lemmaToUses[use.lemma!]!.add(use);
}
final constructUses = lemmaToUses.entries
.map(
(entry) => ConstructUses(
lemma: entry.key,
uses: entry.value,
constructType: type,
),
)
.toList();
constructUses.sort((a, b) {
final comp = b.uses.length.compareTo(a.uses.length);
if (comp != 0) return comp;
return a.lemma.compareTo(b.lemma);
});
return constructUses;
}
}

@ -118,7 +118,10 @@ class ChoreoRecord {
String get finalMessage =>
choreoSteps.isNotEmpty ? choreoSteps.last.text : "";
/// get construct uses of type grammar for the message
/// Get construct uses of type grammar for the message from this ChoreoRecord.
/// Takes either an event (typically when the Representation itself is
/// available) or construct use metadata (when the event is not available,
/// i.e. immediately after message send) to create the construct uses.
List<OneConstructUse> grammarConstructUses({
Event? event,
ConstructUseMetaData? metadata,

@ -110,11 +110,7 @@ class ConstructListViewState extends State<ConstructListView> {
widget.pangeaController.analytics
.getConstructs(
constructType: constructType,
removeIT: true,
defaultSelected: widget.defaultSelected,
selected: widget.selected,
forceUpdate: true,
timeSpan: widget.timeSpan,
)
.whenComplete(() => setState(() => fetchingConstructs = false))
.then((value) => setState(() => _constructs = value));
@ -126,11 +122,7 @@ class ConstructListViewState extends State<ConstructListView> {
widget.pangeaController.analytics
.getConstructs(
constructType: constructType,
removeIT: true,
defaultSelected: widget.defaultSelected,
selected: widget.selected,
forceUpdate: true,
timeSpan: widget.timeSpan,
)
.then(
(value) => setState(() {
@ -163,11 +155,11 @@ class ConstructListViewState extends State<ConstructListView> {
) async {
final Client client = Matrix.of(context).client;
PangeaMessageEvent msgEvent;
if (_msgEventCache.containsKey(use.msgId!)) {
return _msgEventCache[use.msgId!]!;
if (_msgEventCache.containsKey(use.msgId)) {
return _msgEventCache[use.msgId]!;
}
final Room? msgRoom = use.getRoom(client);
if (msgRoom == null || use.msgId == null) {
if (msgRoom == null) {
return null;
}
@ -189,7 +181,7 @@ class ConstructListViewState extends State<ConstructListView> {
timeline: timeline,
ownMessage: event.senderId == client.userID,
);
_msgEventCache[use.msgId!] = msgEvent;
_msgEventCache[use.msgId] = msgEvent;
return msgEvent;
}

@ -1,10 +1,12 @@
import 'dart:async';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
@ -30,64 +32,78 @@ class LearningProgressIndicators extends StatefulWidget {
class LearningProgressIndicatorsState
extends State<LearningProgressIndicators> {
final PangeaController _pangeaController = MatrixState.pangeaController;
int? wordsUsed;
int? errorTypes;
/// A stream subscription to listen for updates to
/// the analytics data, either locally or from events
StreamSubscription? _onAnalyticsUpdate;
/// Vocabulary constructs model
ConstructListModel? words;
/// Grammar constructs model
ConstructListModel? errors;
@override
void initState() {
super.initState();
setData();
updateAnalyticsData();
// listen for changes to analytics data and update the UI
_onAnalyticsUpdate = _pangeaController
.myAnalytics.analyticsUpdateStream.stream
.listen((_) => updateAnalyticsData());
}
AnalyticsSelected get defaultSelected => AnalyticsSelected(
_pangeaController.matrixState.client.userID!,
AnalyticsEntryType.student,
"",
);
@override
void dispose() {
_onAnalyticsUpdate?.cancel();
super.dispose();
}
Future<void> setData() async {
await getNumLemmasUsed();
setState(() {});
/// Update the analytics data shown in the UI. This comes from a
/// combination of stored events and locally cached data.
Future<void> updateAnalyticsData() async {
final constructEvents = await _pangeaController.analytics.getConstructs();
final List<OneConstructUse> localUses = [];
for (final uses in _pangeaController.analytics.messagesSinceUpdate.values) {
localUses.addAll(uses);
}
Future<void> getNumLemmasUsed() async {
final constructs = await _pangeaController.analytics.getConstructs(
defaultSelected: defaultSelected,
timeSpan: TimeSpan.forever,
if (constructEvents == null || constructEvents.isEmpty) {
words = ConstructListModel(
type: ConstructTypeEnum.vocab,
uses: localUses,
);
errors = ConstructListModel(
type: ConstructTypeEnum.grammar,
uses: localUses,
);
if (constructs == null) {
errorTypes = 0;
wordsUsed = 0;
return;
}
final List<String> errorLemmas = [];
final List<String> vocabLemmas = [];
for (final event in constructs) {
for (final use in event.content.uses) {
if (use.lemma == null) continue;
switch (use.constructType) {
case ConstructTypeEnum.grammar:
errorLemmas.add(use.lemma!);
break;
case ConstructTypeEnum.vocab:
vocabLemmas.add(use.lemma!);
break;
default:
break;
}
}
}
errorTypes = errorLemmas.toSet().length;
wordsUsed = vocabLemmas.toSet().length;
final List<OneConstructUse> storedConstruct =
constructEvents.expand((e) => e.content.uses).toList();
final List<OneConstructUse> allConstructs = [
...storedConstruct,
...localUses,
];
words = ConstructListModel(
type: ConstructTypeEnum.vocab,
uses: allConstructs,
);
errors = ConstructListModel(
type: ConstructTypeEnum.grammar,
uses: allConstructs,
);
setState(() {});
}
int? getProgressPoints(ProgressIndicatorEnum indicator) {
switch (indicator) {
case ProgressIndicatorEnum.wordsUsed:
return wordsUsed;
return words?.lemmas.length;
case ProgressIndicatorEnum.errorTypes:
return errorTypes;
return errors?.lemmas.length;
case ProgressIndicatorEnum.level:
return level;
}
@ -95,8 +111,8 @@ class LearningProgressIndicatorsState
int get xpPoints {
final points = [
wordsUsed ?? 0,
errorTypes ?? 0,
words?.lemmas.length ?? 0,
errors?.lemmas.length ?? 0,
];
return points.reduce((a, b) => a + b);
}
@ -161,14 +177,36 @@ class LearningProgressIndicatorsState
children: [
SizedBox(
width: FluffyThemes.columnWidth - (36 * 2) - 25,
child: LinearProgressIndicator(
value: (xpPoints % 100) / 100,
color: Theme.of(context).colorScheme.primary,
backgroundColor:
child: Expanded(
child: Stack(
alignment: Alignment.centerLeft,
children: [
Container(
height: 15,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
color:
Theme.of(context).colorScheme.onPrimary,
minHeight: 15,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
height: 15,
width:
(FluffyThemes.columnWidth - (36 * 2) - 25) *
((xpPoints % 100) / 100),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
],

@ -1,3 +1,4 @@
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:flutter/material.dart';
@ -33,8 +34,8 @@ class ProgressIndicatorBadge extends StatelessWidget {
),
const SizedBox(width: 5),
points != null
? Text(
points.toString(),
? AnimatedCount(
count: points!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@ -49,3 +50,47 @@ class ProgressIndicatorBadge extends StatelessWidget {
);
}
}
class AnimatedCount extends ImplicitlyAnimatedWidget {
const AnimatedCount({
super.key,
required this.count,
this.style,
super.duration = const Duration(seconds: 1),
super.curve = FluffyThemes.animationCurve,
});
final int count;
final TextStyle? style;
@override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() {
return _AnimatedCountState();
}
}
class _AnimatedCountState extends AnimatedWidgetBaseState<AnimatedCount> {
IntTween _intCount = IntTween(begin: 0, end: 1);
@override
void initState() {
super.initState();
_intCount = IntTween(begin: 0, end: widget.count.toInt());
controller.forward();
}
@override
Widget build(BuildContext context) {
final String text = _intCount.evaluate(animation).toString();
return Text(text, style: widget.style);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_intCount = visitor(
_intCount,
widget.count,
(dynamic value) => IntTween(begin: value),
) as IntTween;
}
}

Loading…
Cancel
Save