use construct use type pointValues to calculate XP and level
This commit is contained in:
parent
4ede7c9bdd
commit
9f69360f24
8 changed files with 260 additions and 189 deletions
|
|
@ -4116,5 +4116,6 @@
|
|||
"error520Desc": "Sorry, we could not understand your message...",
|
||||
"wordsUsed": "Words Used",
|
||||
"errorTypes": "Error Types",
|
||||
"level": "Level"
|
||||
"level": "Level",
|
||||
"canceledSend": "Canceled send"
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ class GetAnalyticsController {
|
|||
|
||||
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
|
||||
Map<String, List<OneConstructUse>> get messagesSinceUpdate {
|
||||
try {
|
||||
|
|
@ -60,17 +62,17 @@ class GetAnalyticsController {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get a list of all the construct analytics events
|
||||
/// for the logged in user in their current L2
|
||||
Future<List<ConstructAnalyticsEvent>?> getConstructs({
|
||||
/// Get a list of all constructs used by the logged in user in their current L2
|
||||
Future<List<OneConstructUse>> getConstructs({
|
||||
bool forceUpdate = false,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) async {
|
||||
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 List<ConstructAnalyticsEvent>? local = getConstructsLocal(
|
||||
final List<OneConstructUse>? local = getConstructsLocal(
|
||||
constructType: constructType,
|
||||
lastUpdated: lastUpdated,
|
||||
);
|
||||
|
|
@ -80,35 +82,46 @@ class GetAnalyticsController {
|
|||
}
|
||||
debugPrint("fetching new constructs");
|
||||
|
||||
final unfilteredConstructs = await allMyConstructs();
|
||||
final filteredConstructs = await filterConstructs(
|
||||
unfilteredConstructs: unfilteredConstructs,
|
||||
// if there is no cached data (or if force updating),
|
||||
// get all the construct events for the user from analytics room
|
||||
// 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) {
|
||||
cacheConstructs(
|
||||
constructType: constructType,
|
||||
events: filteredConstructs,
|
||||
uses: filteredUses,
|
||||
);
|
||||
}
|
||||
|
||||
return filteredConstructs;
|
||||
return filteredUses;
|
||||
}
|
||||
|
||||
/// 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!);
|
||||
final Room? analyticsRoom = client.analyticsRoomLocal(l2Code!);
|
||||
if (analyticsRoom == null) return null;
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
client.userID!,
|
||||
);
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
/// Get the cached construct analytics events for the current user, if it exists
|
||||
List<ConstructAnalyticsEvent>? getConstructsLocal({
|
||||
/// Get the cached construct uses for the current user, if it exists
|
||||
List<OneConstructUse>? getConstructsLocal({
|
||||
DateTime? lastUpdated,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
|
|
@ -121,7 +134,7 @@ class GetAnalyticsController {
|
|||
_cache.removeAt(index);
|
||||
return null;
|
||||
}
|
||||
return _cache[index].events;
|
||||
return _cache[index].uses;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -130,48 +143,34 @@ class GetAnalyticsController {
|
|||
/// 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!);
|
||||
final Room? analyticsRoom = client.analyticsRoomLocal(l2Code!);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
return await analyticsRoom.getAnalyticsEvents(
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
) ??
|
||||
[];
|
||||
return await analyticsRoom.getAnalyticsEvents(userId: 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,
|
||||
Future<List<OneConstructUse>> filterConstructs({
|
||||
required List<OneConstructUse> 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;
|
||||
final List<String> adminSpaceRooms = await client.teacherRoomIds;
|
||||
return unfilteredConstructs.where((use) {
|
||||
if (adminSpaceRooms.contains(use.chatId)) return false;
|
||||
return use.lemma != "Try interactive translation" &&
|
||||
use.lemma != "itStart" ||
|
||||
use.lemma != MatchRuleIds.interactiveTranslation;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Cache the construct analytics events for the current user
|
||||
/// Cache the construct uses for the current user
|
||||
void cacheConstructs({
|
||||
required List<ConstructAnalyticsEvent> events,
|
||||
required List<OneConstructUse> uses,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
if (l2Code == null) return;
|
||||
final entry = AnalyticsCacheEntry(
|
||||
type: constructType,
|
||||
events: List.from(events),
|
||||
uses: List.from(uses),
|
||||
langCode: l2Code!,
|
||||
);
|
||||
_cache.add(entry);
|
||||
|
|
@ -181,13 +180,13 @@ class GetAnalyticsController {
|
|||
class AnalyticsCacheEntry {
|
||||
final String langCode;
|
||||
final ConstructTypeEnum? type;
|
||||
final List<ConstructAnalyticsEvent> events;
|
||||
final List<OneConstructUse> uses;
|
||||
late final DateTime _createdAt;
|
||||
|
||||
AnalyticsCacheEntry({
|
||||
required this.langCode,
|
||||
required this.type,
|
||||
required this.events,
|
||||
required this.uses,
|
||||
}) {
|
||||
_createdAt = DateTime.now();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,13 @@ class MyAnalyticsController extends BaseController {
|
|||
final StreamController analyticsUpdateStream = StreamController.broadcast();
|
||||
Timer? _updateTimer;
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
||||
String? get userL2 => _pangeaController.languageController.activeL2Code();
|
||||
|
||||
/// the max number of messages that will be cached before
|
||||
/// an automatic update is triggered
|
||||
final int _maxMessagesCached = 10;
|
||||
final int _maxMessagesCached = 1;
|
||||
|
||||
/// the number of minutes before an automatic update is triggered
|
||||
final int _minutesBeforeUpdate = 5;
|
||||
|
|
@ -37,7 +41,11 @@ class MyAnalyticsController extends BaseController {
|
|||
|
||||
MyAnalyticsController(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
|
||||
// of new messages sent by the logged in user
|
||||
|
|
@ -66,8 +74,6 @@ class MyAnalyticsController extends BaseController {
|
|||
return lastUpdated;
|
||||
}
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
||||
/// 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) {
|
||||
|
|
@ -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,
|
||||
/// convert them into the correct formats, and send them to the analytics room
|
||||
Future<void> _updateAnalytics() async {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
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';
|
||||
|
||||
/// 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;
|
||||
final ConstructTypeEnum type;
|
||||
final List<OneConstructUse> _uses;
|
||||
|
||||
ConstructListModel({
|
||||
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
|
||||
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 uses, sorted by the number of uses
|
||||
List<ConstructUses> get constructs {
|
||||
final List<OneConstructUse> filtered =
|
||||
uses.where((use) => use.constructType == type).toList();
|
||||
|
||||
// the list of uses doesn't change so we don't have to re-calculate this
|
||||
if (_constructs != null) return _constructs!;
|
||||
final Map<String, List<OneConstructUse>> lemmaToUses = {};
|
||||
for (final use in filtered) {
|
||||
for (final use in uses) {
|
||||
if (use.lemma == null) continue;
|
||||
lemmaToUses[use.lemma!] ??= [];
|
||||
lemmaToUses[use.lemma!]!.add(use);
|
||||
|
|
@ -45,6 +51,79 @@ class ConstructListModel {
|
|||
return a.lemma.compareTo(b.lemma);
|
||||
});
|
||||
|
||||
_constructs = 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 {
|
||||
String? lemma;
|
||||
ConstructTypeEnum? constructType;
|
||||
|
|
@ -148,6 +136,8 @@ class OneConstructUse {
|
|||
if (room == null || metadata.eventId == null) return null;
|
||||
return room.getEventById(metadata.eventId!);
|
||||
}
|
||||
|
||||
int get pointValue => useType.pointValue;
|
||||
}
|
||||
|
||||
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/matrix_event_wrappers/pangea_message_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/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
|
|
@ -113,7 +113,14 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
forceUpdate: true,
|
||||
)
|
||||
.whenComplete(() => setState(() => fetchingConstructs = false))
|
||||
.then((value) => setState(() => _constructs = value));
|
||||
.then(
|
||||
(value) => setState(
|
||||
() => constructs = ConstructListModel(
|
||||
type: constructType,
|
||||
uses: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) {
|
||||
// postframe callback to let widget rebuild with the new selected parameter
|
||||
|
|
@ -126,7 +133,10 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
)
|
||||
.then(
|
||||
(value) => setState(() {
|
||||
_constructs = value;
|
||||
ConstructListModel(
|
||||
type: constructType,
|
||||
uses: value,
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -144,12 +154,6 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
int get lemmaIndex =>
|
||||
constructs?.indexWhere(
|
||||
(element) => element.lemma == currentLemma,
|
||||
) ??
|
||||
-1;
|
||||
|
||||
Future<PangeaMessageEvent?> getMessageEvent(
|
||||
OneConstructUse use,
|
||||
) async {
|
||||
|
|
@ -187,14 +191,19 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
|
||||
Future<void> fetchUses() async {
|
||||
if (fetchingUses) return;
|
||||
if (currentConstruct == null) {
|
||||
if (currentLemma == null) {
|
||||
setState(() => _msgEvents.clear());
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => fetchingUses = true);
|
||||
try {
|
||||
final List<OneConstructUse> uses = currentConstruct!.uses;
|
||||
final List<OneConstructUse> uses = constructs?.constructs
|
||||
.firstWhereOrNull(
|
||||
(element) => element.lemma == currentLemma,
|
||||
)
|
||||
?.uses ??
|
||||
[];
|
||||
_msgEvents.clear();
|
||||
|
||||
for (final OneConstructUse use in uses) {
|
||||
|
|
@ -213,54 +222,12 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
m: "Failed to fetch uses for current construct ${currentConstruct?.lemma}",
|
||||
m: "Failed to fetch uses for current construct $currentLemma",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<ConstructAnalyticsEvent>? _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,
|
||||
);
|
||||
ConstructListModel? constructs;
|
||||
|
||||
// given the current lemma and list of message events, return a list of
|
||||
// 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(
|
||||
child: Center(child: Text(L10n.of(context)!.noDataFound)),
|
||||
);
|
||||
|
|
@ -317,17 +284,17 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: constructs!.length,
|
||||
itemCount: constructs!.constructs.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
constructs![index].lemma,
|
||||
constructs!.constructs[index].lemma,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${L10n.of(context)!.total} ${constructs![index].uses.length}',
|
||||
'${L10n.of(context)!.total} ${constructs!.constructs[index].uses.length}',
|
||||
),
|
||||
onTap: () async {
|
||||
final String lemma = constructs![index].lemma;
|
||||
final String lemma = constructs!.constructs[index].lemma;
|
||||
setCurrentLemma(lemma);
|
||||
fetchUses().then((_) => showConstructMessagesDialog());
|
||||
},
|
||||
|
|
@ -347,17 +314,17 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.currentLemma == null ||
|
||||
controller.constructs == null ||
|
||||
controller.lemmaIndex < 0 ||
|
||||
controller.lemmaIndex >= controller.constructs!.length) {
|
||||
if (controller.currentLemma == null || controller.constructs == null) {
|
||||
return const AlertDialog(content: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final msgEventMatches = controller.getMessageEventMatches();
|
||||
|
||||
final noData = controller.constructs![controller.lemmaIndex].uses.length >
|
||||
controller._msgEvents.length;
|
||||
final currentConstruct = controller.constructs!.constructs.firstWhereOrNull(
|
||||
(construct) => construct.lemma == controller.currentLemma,
|
||||
);
|
||||
final noData = currentConstruct == null ||
|
||||
currentConstruct.uses.length > controller._msgEvents.length;
|
||||
|
||||
return AlertDialog(
|
||||
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/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';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -62,13 +61,14 @@ class LearningProgressIndicatorsState
|
|||
/// 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> storedUses =
|
||||
await _pangeaController.analytics.getConstructs();
|
||||
final List<OneConstructUse> localUses = [];
|
||||
for (final uses in _pangeaController.analytics.messagesSinceUpdate.values) {
|
||||
localUses.addAll(uses);
|
||||
}
|
||||
|
||||
if (constructEvents == null || constructEvents.isEmpty) {
|
||||
if (storedUses.isEmpty) {
|
||||
words = ConstructListModel(
|
||||
type: ConstructTypeEnum.vocab,
|
||||
uses: localUses,
|
||||
|
|
@ -80,10 +80,8 @@ class LearningProgressIndicatorsState
|
|||
return;
|
||||
}
|
||||
|
||||
final List<OneConstructUse> storedConstruct =
|
||||
constructEvents.expand((e) => e.content.uses).toList();
|
||||
final List<OneConstructUse> allConstructs = [
|
||||
...storedConstruct,
|
||||
...storedUses,
|
||||
...localUses,
|
||||
];
|
||||
|
||||
|
|
@ -98,6 +96,7 @@ class LearningProgressIndicatorsState
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
/// Get the number of points for a given progress indicator
|
||||
int? getProgressPoints(ProgressIndicatorEnum indicator) {
|
||||
switch (indicator) {
|
||||
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 {
|
||||
final points = [
|
||||
words?.lemmas.length ?? 0,
|
||||
errors?.lemmas.length ?? 0,
|
||||
];
|
||||
return points.reduce((a, b) => a + b);
|
||||
return (words?.points ?? 0) + (errors?.points ?? 0);
|
||||
}
|
||||
|
||||
int get level => xpPoints ~/ 100;
|
||||
/// Get the current level based on the number of xp points
|
||||
int get level => xpPoints ~/ 500;
|
||||
|
||||
double get levelBarWidth => FluffyThemes.columnWidth - (36 * 2) - 25;
|
||||
double get pointsBarWidth {
|
||||
final percent = (xpPoints % 500) / 500;
|
||||
return levelBarWidth * percent;
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -130,7 +145,7 @@ class LearningProgressIndicatorsState
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future:
|
||||
|
|
@ -143,25 +158,25 @@ class LearningProgressIndicatorsState
|
|||
return Avatar(
|
||||
name: snapshot.data?.displayName ?? mxid.localpart ?? mxid,
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
size: 40,
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: ProgressIndicatorEnum.values
|
||||
.where(
|
||||
(indicator) => indicator != ProgressIndicatorEnum.level,
|
||||
)
|
||||
.map(
|
||||
(indicator) => ProgressIndicatorBadge(
|
||||
points: getProgressPoints(indicator),
|
||||
onTap: () {},
|
||||
progressIndicator: indicator,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: ProgressIndicatorEnum.values
|
||||
.where(
|
||||
(indicator) => indicator != ProgressIndicatorEnum.level,
|
||||
)
|
||||
.map(
|
||||
(indicator) => ProgressIndicatorBadge(
|
||||
points: getProgressPoints(indicator),
|
||||
onTap: () {},
|
||||
progressIndicator: indicator,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -173,31 +188,41 @@ class LearningProgressIndicatorsState
|
|||
children: [
|
||||
Positioned(
|
||||
right: 0,
|
||||
left: 10,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: FluffyThemes.columnWidth - (36 * 2) - 25,
|
||||
width: levelBarWidth,
|
||||
child: Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Container(
|
||||
height: 15,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.5),
|
||||
width: 2,
|
||||
),
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight:
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
bottomRight:
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: 15,
|
||||
width:
|
||||
(FluffyThemes.columnWidth - (36 * 2) - 25) *
|
||||
((xpPoints % 100) / 100),
|
||||
height: 16,
|
||||
width: pointsBarWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
|
|
@ -214,12 +239,18 @@ class LearningProgressIndicatorsState
|
|||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: "$level $xpPoints".lightColorAvatar,
|
||||
radius: 16,
|
||||
child: Text(
|
||||
"$level",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: levelColor(level),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"$level",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -346,8 +346,8 @@ class MatrixLocals extends MatrixLocalizations {
|
|||
l10n.startedKeyVerification(senderName);
|
||||
|
||||
@override
|
||||
String invitedBy(String senderName) {
|
||||
// TODO: implement invitedBy
|
||||
throw UnimplementedError();
|
||||
}
|
||||
String invitedBy(String senderName) => l10n.youInvitedBy(senderName);
|
||||
|
||||
@override
|
||||
String get cancelledSend => l10n.canceledSend;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue