use construct use type pointValues to calculate XP and level

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

@ -4116,5 +4116,6 @@
"error520Desc": "Sorry, we could not understand your message...", "error520Desc": "Sorry, we could not understand your message...",
"wordsUsed": "Words Used", "wordsUsed": "Words Used",
"errorTypes": "Error Types", "errorTypes": "Error Types",
"level": "Level" "level": "Level",
"canceledSend": "Canceled send"
} }

@ -23,6 +23,8 @@ class GetAnalyticsController {
String? get l2Code => _pangeaController.languageController.userL2?.langCode; String? get l2Code => _pangeaController.languageController.userL2?.langCode;
Client get client => _pangeaController.matrixState.client;
// A local cache of eventIds and construct uses for messages sent since the last update // A local cache of eventIds and construct uses for messages sent since the last update
Map<String, List<OneConstructUse>> get messagesSinceUpdate { Map<String, List<OneConstructUse>> get messagesSinceUpdate {
try { try {
@ -60,17 +62,17 @@ class GetAnalyticsController {
} }
} }
/// Get a list of all the construct analytics events /// Get a list of all constructs used by the logged in user in their current L2
/// for the logged in user in their current L2 Future<List<OneConstructUse>> getConstructs({
Future<List<ConstructAnalyticsEvent>?> getConstructs({
bool forceUpdate = false, bool forceUpdate = false,
ConstructTypeEnum? constructType, ConstructTypeEnum? constructType,
}) async { }) async {
debugPrint("getting constructs"); debugPrint("getting constructs");
await _pangeaController.matrixState.client.roomsLoading; await client.roomsLoading;
// first, try to get a cached list of all uses, if it exists and is valid
final DateTime? lastUpdated = await myAnalyticsLastUpdated(); final DateTime? lastUpdated = await myAnalyticsLastUpdated();
final List<ConstructAnalyticsEvent>? local = getConstructsLocal( final List<OneConstructUse>? local = getConstructsLocal(
constructType: constructType, constructType: constructType,
lastUpdated: lastUpdated, lastUpdated: lastUpdated,
); );
@ -80,35 +82,46 @@ class GetAnalyticsController {
} }
debugPrint("fetching new constructs"); debugPrint("fetching new constructs");
final unfilteredConstructs = await allMyConstructs(); // if there is no cached data (or if force updating),
final filteredConstructs = await filterConstructs( // get all the construct events for the user from analytics room
unfilteredConstructs: unfilteredConstructs, // and convert their content into a list of construct uses
final List<ConstructAnalyticsEvent> constructEvents =
await allMyConstructs();
final List<OneConstructUse> unfilteredUses = [];
for (final event in constructEvents) {
unfilteredUses.addAll(event.content.uses);
}
// filter out any constructs that are not relevant to the user
final List<OneConstructUse> filteredUses = await filterConstructs(
unfilteredConstructs: unfilteredUses,
); );
// if there isn't already a valid, local cache, cache the filtered uses
if (local == null) { if (local == null) {
cacheConstructs( cacheConstructs(
constructType: constructType, constructType: constructType,
events: filteredConstructs, uses: filteredUses,
); );
} }
return filteredConstructs; return filteredUses;
} }
/// Get the last time the user updated their analytics for their current l2 /// Get the last time the user updated their analytics for their current l2
Future<DateTime?> myAnalyticsLastUpdated() async { Future<DateTime?> myAnalyticsLastUpdated() async {
if (l2Code == null) return null; if (l2Code == null) return null;
final Room? analyticsRoom = final Room? analyticsRoom = client.analyticsRoomLocal(l2Code!);
_pangeaController.matrixState.client.analyticsRoomLocal(l2Code!);
if (analyticsRoom == null) return null; if (analyticsRoom == null) return null;
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated( final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
_pangeaController.matrixState.client.userID!, client.userID!,
); );
return lastUpdated; return lastUpdated;
} }
/// Get the cached construct analytics events for the current user, if it exists /// Get the cached construct uses for the current user, if it exists
List<ConstructAnalyticsEvent>? getConstructsLocal({ List<OneConstructUse>? getConstructsLocal({
DateTime? lastUpdated, DateTime? lastUpdated,
ConstructTypeEnum? constructType, ConstructTypeEnum? constructType,
}) { }) {
@ -121,7 +134,7 @@ class GetAnalyticsController {
_cache.removeAt(index); _cache.removeAt(index);
return null; return null;
} }
return _cache[index].events; return _cache[index].uses;
} }
return null; return null;
@ -130,48 +143,34 @@ class GetAnalyticsController {
/// Get all the construct analytics events for the logged in user /// Get all the construct analytics events for the logged in user
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async { Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
if (l2Code == null) return []; if (l2Code == null) return [];
final Room? analyticsRoom = final Room? analyticsRoom = client.analyticsRoomLocal(l2Code!);
_pangeaController.matrixState.client.analyticsRoomLocal(l2Code!);
if (analyticsRoom == null) return []; if (analyticsRoom == null) return [];
return await analyticsRoom.getAnalyticsEvents(userId: client.userID!) ?? [];
return await analyticsRoom.getAnalyticsEvents(
userId: _pangeaController.matrixState.client.userID!,
) ??
[];
} }
/// Filter out constructs that are not relevant to the user, specifically those from /// 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 /// rooms in which the user is a teacher and those that are interative translation span constructs
Future<List<ConstructAnalyticsEvent>> filterConstructs({ Future<List<OneConstructUse>> filterConstructs({
required List<ConstructAnalyticsEvent> unfilteredConstructs, required List<OneConstructUse> unfilteredConstructs,
}) async { }) async {
final List<String> adminSpaceRooms = final List<String> adminSpaceRooms = await client.teacherRoomIds;
await _pangeaController.matrixState.client.teacherRoomIds; return unfilteredConstructs.where((use) {
for (final construct in unfilteredConstructs) { if (adminSpaceRooms.contains(use.chatId)) return false;
construct.content.uses.removeWhere( return use.lemma != "Try interactive translation" &&
(use) { use.lemma != "itStart" ||
if (adminSpaceRooms.contains(use.chatId)) { use.lemma != MatchRuleIds.interactiveTranslation;
return true; }).toList();
}
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 /// Cache the construct uses for the current user
void cacheConstructs({ void cacheConstructs({
required List<ConstructAnalyticsEvent> events, required List<OneConstructUse> uses,
ConstructTypeEnum? constructType, ConstructTypeEnum? constructType,
}) { }) {
if (l2Code == null) return; if (l2Code == null) return;
final entry = AnalyticsCacheEntry( final entry = AnalyticsCacheEntry(
type: constructType, type: constructType,
events: List.from(events), uses: List.from(uses),
langCode: l2Code!, langCode: l2Code!,
); );
_cache.add(entry); _cache.add(entry);
@ -181,13 +180,13 @@ class GetAnalyticsController {
class AnalyticsCacheEntry { class AnalyticsCacheEntry {
final String langCode; final String langCode;
final ConstructTypeEnum? type; final ConstructTypeEnum? type;
final List<ConstructAnalyticsEvent> events; final List<OneConstructUse> uses;
late final DateTime _createdAt; late final DateTime _createdAt;
AnalyticsCacheEntry({ AnalyticsCacheEntry({
required this.langCode, required this.langCode,
required this.type, required this.type,
required this.events, required this.uses,
}) { }) {
_createdAt = DateTime.now(); _createdAt = DateTime.now();
} }

@ -25,9 +25,13 @@ class MyAnalyticsController extends BaseController {
final StreamController analyticsUpdateStream = StreamController.broadcast(); final StreamController analyticsUpdateStream = StreamController.broadcast();
Timer? _updateTimer; Timer? _updateTimer;
Client get _client => _pangeaController.matrixState.client;
String? get userL2 => _pangeaController.languageController.activeL2Code();
/// the max number of messages that will be cached before /// the max number of messages that will be cached before
/// an automatic update is triggered /// an automatic update is triggered
final int _maxMessagesCached = 10; final int _maxMessagesCached = 1;
/// the number of minutes before an automatic update is triggered /// the number of minutes before an automatic update is triggered
final int _minutesBeforeUpdate = 5; final int _minutesBeforeUpdate = 5;
@ -37,7 +41,11 @@ class MyAnalyticsController extends BaseController {
MyAnalyticsController(PangeaController pangeaController) { MyAnalyticsController(PangeaController pangeaController) {
_pangeaController = pangeaController; _pangeaController = pangeaController;
_refreshAnalyticsIfOutdated();
// Wait for the next sync in the stream to ensure that the pangea controller
// is fully initialized. It will throw an error if it is not.
_pangeaController.matrixState.client.onSync.stream.first
.then((_) => _refreshAnalyticsIfOutdated());
// Listen to a stream that provides the eventIDs // Listen to a stream that provides the eventIDs
// of new messages sent by the logged in user // of new messages sent by the logged in user
@ -66,8 +74,6 @@ class MyAnalyticsController extends BaseController {
return lastUpdated; return lastUpdated;
} }
Client get _client => _pangeaController.matrixState.client;
/// Given the data from a newly sent message, format and cache /// Given the data from a newly sent message, format and cache
/// the message's construct data locally and reset the update timer /// the message's construct data locally and reset the update timer
void onMessageSent(Map<String, dynamic> data) { void onMessageSent(Map<String, dynamic> data) {
@ -185,8 +191,6 @@ class MyAnalyticsController extends BaseController {
} }
} }
String? get userL2 => _pangeaController.languageController.activeL2Code();
/// top level analytics sending function. Gather recent messages and activity records, /// top level analytics sending function. Gather recent messages and activity records,
/// convert them into the correct formats, and send them to the analytics room /// convert them into the correct formats, and send them to the analytics room
Future<void> _updateAnalytics() async { Future<void> _updateAnalytics() async {

@ -1,17 +1,24 @@
import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
/// A wrapper around a list of [OneConstructUse]s, used to simplify /// A wrapper around a list of [OneConstructUse]s, used to simplify
/// the process of filtering / sorting / displaying the events. /// the process of filtering / sorting / displaying the events.
/// Takes a construct type and a list of events /// Takes a construct type and a list of events
class ConstructListModel { class ConstructListModel {
ConstructTypeEnum type; final ConstructTypeEnum type;
List<OneConstructUse> uses; final List<OneConstructUse> _uses;
ConstructListModel({ ConstructListModel({
required this.type, required this.type,
required this.uses, uses,
}); }) : _uses = uses ?? [];
List<ConstructUses>? _constructs;
List<ConstructUseTypeUses>? _typedConstructs;
List<OneConstructUse> get uses =>
_uses.where((use) => use.constructType == type).toList();
/// All unique lemmas used in the construct events /// All unique lemmas used in the construct events
List<String> get lemmas => constructs.map((e) => e.lemma).toSet().toList(); List<String> get lemmas => constructs.map((e) => e.lemma).toSet().toList();
@ -19,11 +26,10 @@ class ConstructListModel {
/// A list of ConstructUses, each of which contains a lemma and /// A list of ConstructUses, each of which contains a lemma and
/// a list of uses, sorted by the number of uses /// a list of uses, sorted by the number of uses
List<ConstructUses> get constructs { List<ConstructUses> get constructs {
final List<OneConstructUse> filtered = // the list of uses doesn't change so we don't have to re-calculate this
uses.where((use) => use.constructType == type).toList(); if (_constructs != null) return _constructs!;
final Map<String, List<OneConstructUse>> lemmaToUses = {}; final Map<String, List<OneConstructUse>> lemmaToUses = {};
for (final use in filtered) { for (final use in uses) {
if (use.lemma == null) continue; if (use.lemma == null) continue;
lemmaToUses[use.lemma!] ??= []; lemmaToUses[use.lemma!] ??= [];
lemmaToUses[use.lemma!]!.add(use); lemmaToUses[use.lemma!]!.add(use);
@ -45,6 +51,79 @@ class ConstructListModel {
return a.lemma.compareTo(b.lemma); return a.lemma.compareTo(b.lemma);
}); });
_constructs = constructUses;
return constructUses; return constructUses;
} }
/// A list of ConstructUseTypeUses, each of which
/// contains a lemma, a use type, and a list of uses
List<ConstructUseTypeUses> get typedConstructs {
if (_typedConstructs != null) return _typedConstructs!;
final List<ConstructUseTypeUses> typedConstructs = [];
for (final construct in constructs) {
final typeToUses = <ConstructUseTypeEnum, List<OneConstructUse>>{};
for (final use in construct.uses) {
typeToUses[use.useType] ??= [];
typeToUses[use.useType]!.add(use);
}
for (final typeEntry in typeToUses.entries) {
typedConstructs.add(
ConstructUseTypeUses(
lemma: construct.lemma,
constructType: type,
useType: typeEntry.key,
uses: typeEntry.value,
),
);
}
}
return typedConstructs;
}
/// The total number of points for all uses of this construct type
int get points {
double totalPoints = 0;
// Minimize the amount of points given for repeated uses of the same lemma.
// i.e., if a lemma is used 4 times without assistance, the point value for
// a use without assistance is 3. So the points would be
// 3/1 + 3/2 + 3/3 + 3/4 = 3 + 1.5 + 1 + 0.75 = 5.25 (instead of 12)
for (final typedConstruct in typedConstructs) {
final pointValue = typedConstruct.useType.pointValue;
double calc = 0.0;
for (int k = 1; k <= typedConstruct.uses.length; k++) {
calc += pointValue / k;
}
totalPoints += calc;
}
return totalPoints.round();
}
}
/// One lemma and a list of construct uses for that lemma
class ConstructUses {
final List<OneConstructUse> uses;
final ConstructTypeEnum constructType;
final String lemma;
ConstructUses({
required this.uses,
required this.constructType,
required this.lemma,
});
}
/// One lemma, a use type, and a list of uses
/// for that lemma and use type
class ConstructUseTypeUses {
final ConstructUseTypeEnum useType;
final ConstructTypeEnum constructType;
final String lemma;
final List<OneConstructUse> uses;
ConstructUseTypeUses({
required this.useType,
required this.constructType,
required this.lemma,
required this.uses,
});
} }

@ -71,18 +71,6 @@ class ConstructAnalyticsModel {
} }
} }
class ConstructUses {
final List<OneConstructUse> uses;
final ConstructTypeEnum constructType;
final String lemma;
ConstructUses({
required this.uses,
required this.constructType,
required this.lemma,
});
}
class OneConstructUse { class OneConstructUse {
String? lemma; String? lemma;
ConstructTypeEnum? constructType; ConstructTypeEnum? constructType;
@ -148,6 +136,8 @@ class OneConstructUse {
if (room == null || metadata.eventId == null) return null; if (room == null || metadata.eventId == null) return null;
return room.getEventById(metadata.eventId!); return room.getEventById(metadata.eventId!);
} }
int get pointValue => useType.pointValue;
} }
class ConstructUseMetaData { class ConstructUseMetaData {

@ -7,7 +7,7 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/time_span.dart'; import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
@ -113,7 +113,14 @@ class ConstructListViewState extends State<ConstructListView> {
forceUpdate: true, forceUpdate: true,
) )
.whenComplete(() => setState(() => fetchingConstructs = false)) .whenComplete(() => setState(() => fetchingConstructs = false))
.then((value) => setState(() => _constructs = value)); .then(
(value) => setState(
() => constructs = ConstructListModel(
type: constructType,
uses: value,
),
),
);
refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) { refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) {
// postframe callback to let widget rebuild with the new selected parameter // postframe callback to let widget rebuild with the new selected parameter
@ -126,7 +133,10 @@ class ConstructListViewState extends State<ConstructListView> {
) )
.then( .then(
(value) => setState(() { (value) => setState(() {
_constructs = value; ConstructListModel(
type: constructType,
uses: value,
);
}), }),
); );
}); });
@ -144,12 +154,6 @@ class ConstructListViewState extends State<ConstructListView> {
setState(() {}); setState(() {});
} }
int get lemmaIndex =>
constructs?.indexWhere(
(element) => element.lemma == currentLemma,
) ??
-1;
Future<PangeaMessageEvent?> getMessageEvent( Future<PangeaMessageEvent?> getMessageEvent(
OneConstructUse use, OneConstructUse use,
) async { ) async {
@ -187,14 +191,19 @@ class ConstructListViewState extends State<ConstructListView> {
Future<void> fetchUses() async { Future<void> fetchUses() async {
if (fetchingUses) return; if (fetchingUses) return;
if (currentConstruct == null) { if (currentLemma == null) {
setState(() => _msgEvents.clear()); setState(() => _msgEvents.clear());
return; return;
} }
setState(() => fetchingUses = true); setState(() => fetchingUses = true);
try { try {
final List<OneConstructUse> uses = currentConstruct!.uses; final List<OneConstructUse> uses = constructs?.constructs
.firstWhereOrNull(
(element) => element.lemma == currentLemma,
)
?.uses ??
[];
_msgEvents.clear(); _msgEvents.clear();
for (final OneConstructUse use in uses) { for (final OneConstructUse use in uses) {
@ -213,54 +222,12 @@ class ConstructListViewState extends State<ConstructListView> {
ErrorHandler.logError( ErrorHandler.logError(
e: err, e: err,
s: s, s: s,
m: "Failed to fetch uses for current construct ${currentConstruct?.lemma}", m: "Failed to fetch uses for current construct $currentLemma",
); );
} }
} }
List<ConstructAnalyticsEvent>? _constructs; ConstructListModel? constructs;
List<ConstructUses>? get constructs {
if (_constructs == null) {
return null;
}
final List<OneConstructUse> filtered = List.from(_constructs!)
.map((event) => event.content.uses)
.expand((uses) => uses)
.cast<OneConstructUse>()
.where((use) => use.constructType == constructType)
.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: constructType,
),
)
.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;
}
ConstructUses? get currentConstruct => constructs?.firstWhereOrNull(
(element) => element.lemma == currentLemma,
);
// given the current lemma and list of message events, return a list of // given the current lemma and list of message events, return a list of
// MessageEventMatch objects, which contain one PangeaMessageEvent to one PangeaMatch // MessageEventMatch objects, which contain one PangeaMessageEvent to one PangeaMatch
@ -309,7 +276,7 @@ class ConstructListViewState extends State<ConstructListView> {
); );
} }
if (constructs?.isEmpty ?? true) { if (constructs?.constructs.isEmpty ?? true) {
return Expanded( return Expanded(
child: Center(child: Text(L10n.of(context)!.noDataFound)), child: Center(child: Text(L10n.of(context)!.noDataFound)),
); );
@ -317,17 +284,17 @@ class ConstructListViewState extends State<ConstructListView> {
return Expanded( return Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: constructs!.length, itemCount: constructs!.constructs.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return ListTile( return ListTile(
title: Text( title: Text(
constructs![index].lemma, constructs!.constructs[index].lemma,
), ),
subtitle: Text( subtitle: Text(
'${L10n.of(context)!.total} ${constructs![index].uses.length}', '${L10n.of(context)!.total} ${constructs!.constructs[index].uses.length}',
), ),
onTap: () async { onTap: () async {
final String lemma = constructs![index].lemma; final String lemma = constructs!.constructs[index].lemma;
setCurrentLemma(lemma); setCurrentLemma(lemma);
fetchUses().then((_) => showConstructMessagesDialog()); fetchUses().then((_) => showConstructMessagesDialog());
}, },
@ -347,17 +314,17 @@ class ConstructMessagesDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (controller.currentLemma == null || if (controller.currentLemma == null || controller.constructs == null) {
controller.constructs == null ||
controller.lemmaIndex < 0 ||
controller.lemmaIndex >= controller.constructs!.length) {
return const AlertDialog(content: CircularProgressIndicator.adaptive()); return const AlertDialog(content: CircularProgressIndicator.adaptive());
} }
final msgEventMatches = controller.getMessageEventMatches(); final msgEventMatches = controller.getMessageEventMatches();
final noData = controller.constructs![controller.lemmaIndex].uses.length > final currentConstruct = controller.constructs!.constructs.firstWhereOrNull(
controller._msgEvents.length; (construct) => construct.lemma == controller.currentLemma,
);
final noData = currentConstruct == null ||
currentConstruct.uses.length > controller._msgEvents.length;
return AlertDialog( return AlertDialog(
title: Center(child: Text(controller.currentLemma!)), title: Center(child: Text(controller.currentLemma!)),

@ -8,7 +8,6 @@ import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_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/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -62,13 +61,14 @@ class LearningProgressIndicatorsState
/// Update the analytics data shown in the UI. This comes from a /// Update the analytics data shown in the UI. This comes from a
/// combination of stored events and locally cached data. /// combination of stored events and locally cached data.
Future<void> updateAnalyticsData() async { Future<void> updateAnalyticsData() async {
final constructEvents = await _pangeaController.analytics.getConstructs(); final List<OneConstructUse> storedUses =
await _pangeaController.analytics.getConstructs();
final List<OneConstructUse> localUses = []; final List<OneConstructUse> localUses = [];
for (final uses in _pangeaController.analytics.messagesSinceUpdate.values) { for (final uses in _pangeaController.analytics.messagesSinceUpdate.values) {
localUses.addAll(uses); localUses.addAll(uses);
} }
if (constructEvents == null || constructEvents.isEmpty) { if (storedUses.isEmpty) {
words = ConstructListModel( words = ConstructListModel(
type: ConstructTypeEnum.vocab, type: ConstructTypeEnum.vocab,
uses: localUses, uses: localUses,
@ -80,10 +80,8 @@ class LearningProgressIndicatorsState
return; return;
} }
final List<OneConstructUse> storedConstruct =
constructEvents.expand((e) => e.content.uses).toList();
final List<OneConstructUse> allConstructs = [ final List<OneConstructUse> allConstructs = [
...storedConstruct, ...storedUses,
...localUses, ...localUses,
]; ];
@ -98,6 +96,7 @@ class LearningProgressIndicatorsState
setState(() {}); setState(() {});
} }
/// Get the number of points for a given progress indicator
int? getProgressPoints(ProgressIndicatorEnum indicator) { int? getProgressPoints(ProgressIndicatorEnum indicator) {
switch (indicator) { switch (indicator) {
case ProgressIndicatorEnum.wordsUsed: case ProgressIndicatorEnum.wordsUsed:
@ -109,15 +108,31 @@ class LearningProgressIndicatorsState
} }
} }
/// Get the total number of xp points, based on the point values of use types
int get xpPoints { int get xpPoints {
final points = [ return (words?.points ?? 0) + (errors?.points ?? 0);
words?.lemmas.length ?? 0, }
errors?.lemmas.length ?? 0,
]; /// Get the current level based on the number of xp points
return points.reduce((a, b) => a + b); int get level => xpPoints ~/ 500;
double get levelBarWidth => FluffyThemes.columnWidth - (36 * 2) - 25;
double get pointsBarWidth {
final percent = (xpPoints % 500) / 500;
return levelBarWidth * percent;
} }
int get level => xpPoints ~/ 100; Color levelColor(int level) {
final colors = [
const Color.fromARGB(255, 33, 97, 140), // Dark blue
const Color.fromARGB(255, 186, 104, 200), // Soft purple
const Color.fromARGB(255, 123, 31, 162), // Deep purple
const Color.fromARGB(255, 0, 150, 136), // Teal
const Color.fromARGB(255, 247, 143, 143), // Light pink
const Color.fromARGB(255, 220, 20, 60), // Crimson red
];
return colors[level % colors.length];
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -130,7 +145,7 @@ class LearningProgressIndicatorsState
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
FutureBuilder( FutureBuilder(
future: future:
@ -143,11 +158,12 @@ class LearningProgressIndicatorsState
return Avatar( return Avatar(
name: snapshot.data?.displayName ?? mxid.localpart ?? mxid, name: snapshot.data?.displayName ?? mxid.localpart ?? mxid,
mxContent: snapshot.data?.avatarUrl, mxContent: snapshot.data?.avatarUrl,
size: 40,
); );
}, },
), ),
Expanded( const SizedBox(width: 10),
child: Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: ProgressIndicatorEnum.values children: ProgressIndicatorEnum.values
.where( .where(
@ -162,7 +178,6 @@ class LearningProgressIndicatorsState
) )
.toList(), .toList(),
), ),
),
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -173,31 +188,41 @@ class LearningProgressIndicatorsState
children: [ children: [
Positioned( Positioned(
right: 0, right: 0,
left: 10,
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
width: FluffyThemes.columnWidth - (36 * 2) - 25, width: levelBarWidth,
child: Expanded( child: Expanded(
child: Stack( child: Stack(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
children: [ children: [
Container( Container(
height: 15, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular( border: Border.all(
AppConfig.borderRadius, color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5),
width: 2,
), ),
color: borderRadius: const BorderRadius.only(
Theme.of(context).colorScheme.onPrimary, topRight:
Radius.circular(AppConfig.borderRadius),
bottomRight:
Radius.circular(AppConfig.borderRadius),
),
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.2),
), ),
), ),
AnimatedContainer( AnimatedContainer(
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, height: 16,
height: 15, width: pointsBarWidth,
width:
(FluffyThemes.columnWidth - (36 * 2) - 25) *
((xpPoints % 100) / 100),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
AppConfig.borderRadius, AppConfig.borderRadius,
@ -214,15 +239,21 @@ class LearningProgressIndicatorsState
), ),
Positioned( Positioned(
left: 0, left: 0,
child: CircleAvatar( child: Container(
backgroundColor: "$level $xpPoints".lightColorAvatar, width: 32,
radius: 16, height: 32,
decoration: BoxDecoration(
color: levelColor(level),
borderRadius: BorderRadius.circular(32),
),
child: Center(
child: Text( child: Text(
"$level", "$level",
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
), ),
), ),
),
], ],
), ),
), ),

@ -346,8 +346,8 @@ class MatrixLocals extends MatrixLocalizations {
l10n.startedKeyVerification(senderName); l10n.startedKeyVerification(senderName);
@override @override
String invitedBy(String senderName) { String invitedBy(String senderName) => l10n.youInvitedBy(senderName);
// TODO: implement invitedBy
throw UnimplementedError(); @override
} String get cancelledSend => l10n.canceledSend;
} }

Loading…
Cancel
Save