Merge pull request #937 from pangeachat/analytics-streams
update ConstructListModel to get all analytics metrics
This commit is contained in:
commit
ffc63754e8
15 changed files with 309 additions and 329 deletions
|
|
@ -14,7 +14,6 @@ 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:matrix/src/utils/cached_stream_controller.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// A minimized version of AnalyticsController that get the logged in user's analytics
|
||||
|
|
@ -22,143 +21,70 @@ class GetAnalyticsController {
|
|||
late PangeaController _pangeaController;
|
||||
final List<AnalyticsCacheEntry> _cache = [];
|
||||
StreamSubscription<AnalyticsUpdate>? _analyticsUpdateSubscription;
|
||||
CachedStreamController<AnalyticsStreamUpdate> analyticsStream =
|
||||
CachedStreamController<AnalyticsStreamUpdate>();
|
||||
StreamController<AnalyticsStreamUpdate> analyticsStream =
|
||||
StreamController.broadcast();
|
||||
|
||||
ConstructListModel vocabModel = ConstructListModel(
|
||||
type: ConstructTypeEnum.vocab,
|
||||
uses: [],
|
||||
);
|
||||
ConstructListModel grammarModel = ConstructListModel(
|
||||
type: ConstructTypeEnum.morph,
|
||||
uses: [],
|
||||
);
|
||||
|
||||
List<OneConstructUse> get allConstructUses {
|
||||
final List<OneConstructUse> storedUses = getConstructsLocal() ?? [];
|
||||
final List<OneConstructUse> localUses = locallyCachedConstructs;
|
||||
|
||||
final List<OneConstructUse> allConstructs = [
|
||||
...storedUses,
|
||||
...localUses,
|
||||
];
|
||||
|
||||
return allConstructs;
|
||||
}
|
||||
|
||||
/// The previous XP points of the user, before the last update.
|
||||
/// Used for animating analytics updates.
|
||||
int? prevXP;
|
||||
ConstructListModel constructListModel = ConstructListModel(uses: []);
|
||||
|
||||
GetAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
String? get l2Code => _pangeaController.languageController.userL2?.langCode;
|
||||
Client get client => _pangeaController.matrixState.client;
|
||||
|
||||
int get currentXP => calcXP(allConstructUses);
|
||||
int get localXP => calcXP(locallyCachedConstructs);
|
||||
int get serverXP => currentXP - localXP;
|
||||
|
||||
/// Get the current level based on the number of xp points
|
||||
/// The formula is calculated from XP and modeled on RPG games
|
||||
int get level => 1 + sqrt((1 + 8 * currentXP / 100) / 2).floor();
|
||||
String? get _l2Code => _pangeaController.languageController.userL2?.langCode;
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
||||
// the minimum XP required for a given level
|
||||
double get minXPForLevel {
|
||||
return 12.5 * (2 * pow(level - 1, 2) - 1);
|
||||
double get _minXPForLevel {
|
||||
return 12.5 * (2 * pow(constructListModel.level - 1, 2) - 1);
|
||||
}
|
||||
|
||||
// the minimum XP required for the next level
|
||||
double get minXPForNextLevel {
|
||||
return 12.5 * (2 * pow(level, 2) - 1);
|
||||
double get _minXPForNextLevel {
|
||||
return 12.5 * (2 * pow(constructListModel.level, 2) - 1);
|
||||
}
|
||||
|
||||
// the progress within the current level as a percentage (0.0 to 1.0)
|
||||
double get levelProgress {
|
||||
final progress =
|
||||
(currentXP - minXPForLevel) / (minXPForNextLevel - minXPForLevel);
|
||||
return progress >= 0 ? progress : 0;
|
||||
}
|
||||
|
||||
double get serverLevelProgress {
|
||||
final progress =
|
||||
(serverXP - minXPForLevel) / (minXPForNextLevel - minXPForLevel);
|
||||
final progress = (constructListModel.totalXP - _minXPForLevel) /
|
||||
(_minXPForNextLevel - _minXPForLevel);
|
||||
return progress >= 0 ? progress : 0;
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
_analyticsUpdateSubscription ??= _pangeaController
|
||||
.putAnalytics.analyticsUpdateStream.stream
|
||||
.listen(onAnalyticsUpdate);
|
||||
.listen(_onAnalyticsUpdate);
|
||||
|
||||
_pangeaController.putAnalytics.lastUpdatedCompleter.future.then((_) {
|
||||
getConstructs().then((_) {
|
||||
vocabModel.updateConstructs(allConstructUses);
|
||||
grammarModel.updateConstructs(allConstructUses);
|
||||
updateAnalyticsStream();
|
||||
_getConstructs().then((_) {
|
||||
constructListModel.updateConstructs([
|
||||
...(_getConstructsLocal() ?? []),
|
||||
..._locallyCachedConstructs,
|
||||
]);
|
||||
_updateAnalyticsStream();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear all cached analytics data.
|
||||
void dispose() {
|
||||
constructListModel.dispose();
|
||||
_analyticsUpdateSubscription?.cancel();
|
||||
_analyticsUpdateSubscription = null;
|
||||
_cache.clear();
|
||||
analyticsStream.add(AnalyticsStreamUpdate(constructs: []));
|
||||
prevXP = null;
|
||||
}
|
||||
|
||||
Future<void> onAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async {
|
||||
vocabModel.updateConstructs(analyticsUpdate.newConstructs);
|
||||
grammarModel.updateConstructs(analyticsUpdate.newConstructs);
|
||||
Future<void> _onAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async {
|
||||
if (analyticsUpdate.isLogout) return;
|
||||
constructListModel.updateConstructs(analyticsUpdate.newConstructs);
|
||||
if (analyticsUpdate.type == AnalyticsUpdateType.server) {
|
||||
await getConstructs(forceUpdate: true);
|
||||
await _getConstructs(forceUpdate: true);
|
||||
}
|
||||
updateAnalyticsStream(origin: analyticsUpdate.origin);
|
||||
_updateAnalyticsStream(origin: analyticsUpdate.origin);
|
||||
}
|
||||
|
||||
void updateAnalyticsStream({AnalyticsUpdateOrigin? origin}) {
|
||||
// if there are no construct uses, or if the last update in this
|
||||
// stream has the same length as this update, don't update the stream
|
||||
if (allConstructUses.isEmpty ||
|
||||
allConstructUses.length == analyticsStream.value?.constructs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set the previous XP to the currentXP
|
||||
if (analyticsStream.value != null &&
|
||||
analyticsStream.value!.constructs.isNotEmpty) {
|
||||
prevXP = calcXP(analyticsStream.value!.constructs);
|
||||
}
|
||||
|
||||
// finally, add to the stream
|
||||
analyticsStream.add(
|
||||
AnalyticsStreamUpdate(
|
||||
constructs: allConstructUses,
|
||||
origin: origin,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Calculates the user's xpPoints for their current L2,
|
||||
/// based on matrix analytics event and locally cached data.
|
||||
/// Has to be async because cached matrix events may be out of date,
|
||||
/// and updating those is async.
|
||||
int calcXP(List<OneConstructUse> constructs) {
|
||||
final words = ConstructListModel(
|
||||
uses: constructs,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
);
|
||||
|
||||
final morphs = ConstructListModel(
|
||||
uses: constructs,
|
||||
type: ConstructTypeEnum.morph,
|
||||
);
|
||||
return words.points + morphs.points;
|
||||
void _updateAnalyticsStream({AnalyticsUpdateOrigin? origin}) {
|
||||
analyticsStream.add(AnalyticsStreamUpdate(origin: origin));
|
||||
}
|
||||
|
||||
/// A local cache of eventIds and construct uses for messages sent since the last update.
|
||||
|
|
@ -200,7 +126,7 @@ class GetAnalyticsController {
|
|||
}
|
||||
|
||||
/// A flat list of all locally cached construct uses
|
||||
List<OneConstructUse> get locallyCachedConstructs =>
|
||||
List<OneConstructUse> get _locallyCachedConstructs =>
|
||||
messagesSinceUpdate.values.expand((e) => e).toList();
|
||||
|
||||
/// A flat list of all locally cached construct uses that are not drafts
|
||||
|
|
@ -211,13 +137,13 @@ class GetAnalyticsController {
|
|||
.toList();
|
||||
|
||||
/// Get a list of all constructs used by the logged in user in their current L2
|
||||
Future<List<OneConstructUse>> getConstructs({
|
||||
Future<List<OneConstructUse>> _getConstructs({
|
||||
bool forceUpdate = false,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) async {
|
||||
// if the user isn't logged in, return an empty list
|
||||
if (client.userID == null) return [];
|
||||
await client.roomsLoading;
|
||||
if (_client.userID == null) return [];
|
||||
await _client.roomsLoading;
|
||||
|
||||
// don't try to get constructs until last updated time has been loaded
|
||||
await _pangeaController.putAnalytics.lastUpdatedCompleter.future;
|
||||
|
|
@ -225,7 +151,7 @@ class GetAnalyticsController {
|
|||
// if forcing a refreshing, clear the cache
|
||||
if (forceUpdate) _cache.clear();
|
||||
|
||||
final List<OneConstructUse>? local = getConstructsLocal(
|
||||
final List<OneConstructUse>? local = _getConstructsLocal(
|
||||
constructType: constructType,
|
||||
);
|
||||
|
||||
|
|
@ -239,7 +165,7 @@ class GetAnalyticsController {
|
|||
// 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();
|
||||
await _allMyConstructs();
|
||||
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final event in constructEvents) {
|
||||
|
|
@ -248,7 +174,7 @@ class GetAnalyticsController {
|
|||
|
||||
// if there isn't already a valid, local cache, cache the filtered uses
|
||||
if (local == null) {
|
||||
cacheConstructs(
|
||||
_cacheConstructs(
|
||||
constructType: constructType,
|
||||
uses: uses,
|
||||
);
|
||||
|
|
@ -261,32 +187,33 @@ class GetAnalyticsController {
|
|||
Future<DateTime?> myAnalyticsLastUpdated() async {
|
||||
// this function gets called soon after login, so first
|
||||
// make sure that the user's l2 is loaded, if the user has set their l2
|
||||
if (client.userID != null && l2Code == null) {
|
||||
if (_client.userID != null && _l2Code == null) {
|
||||
await _pangeaController.matrixState.client.waitForAccountData();
|
||||
if (l2Code == null) return null;
|
||||
if (_l2Code == null) return null;
|
||||
}
|
||||
final Room? analyticsRoom = client.analyticsRoomLocal(l2Code!);
|
||||
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2Code!);
|
||||
if (analyticsRoom == null) return null;
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
client.userID!,
|
||||
_client.userID!,
|
||||
);
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
/// Get all the construct analytics events for the logged in user
|
||||
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
|
||||
if (l2Code == null) return [];
|
||||
final Room? analyticsRoom = client.analyticsRoomLocal(l2Code!);
|
||||
Future<List<ConstructAnalyticsEvent>> _allMyConstructs() async {
|
||||
if (_l2Code == null) return [];
|
||||
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2Code!);
|
||||
if (analyticsRoom == null) return [];
|
||||
return await analyticsRoom.getAnalyticsEvents(userId: client.userID!) ?? [];
|
||||
return await analyticsRoom.getAnalyticsEvents(userId: _client.userID!) ??
|
||||
[];
|
||||
}
|
||||
|
||||
/// Get the cached construct uses for the current user, if it exists
|
||||
List<OneConstructUse>? getConstructsLocal({
|
||||
List<OneConstructUse>? _getConstructsLocal({
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
final index = _cache.indexWhere(
|
||||
(e) => e.type == constructType && e.langCode == l2Code,
|
||||
(e) => e.type == constructType && e.langCode == _l2Code,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
|
|
@ -302,15 +229,15 @@ class GetAnalyticsController {
|
|||
}
|
||||
|
||||
/// Cache the construct uses for the current user
|
||||
void cacheConstructs({
|
||||
void _cacheConstructs({
|
||||
required List<OneConstructUse> uses,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
if (l2Code == null) return;
|
||||
if (_l2Code == null) return;
|
||||
final entry = AnalyticsCacheEntry(
|
||||
type: constructType,
|
||||
uses: List.from(uses),
|
||||
langCode: l2Code!,
|
||||
langCode: _l2Code!,
|
||||
);
|
||||
_cache.add(entry);
|
||||
}
|
||||
|
|
@ -350,11 +277,9 @@ class AnalyticsCacheEntry {
|
|||
}
|
||||
|
||||
class AnalyticsStreamUpdate {
|
||||
final List<OneConstructUse> constructs;
|
||||
final AnalyticsUpdateOrigin? origin;
|
||||
|
||||
AnalyticsStreamUpdate({
|
||||
required this.constructs,
|
||||
this.origin,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
|||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/cached_stream_controller.dart';
|
||||
|
||||
enum AnalyticsUpdateType { server, local }
|
||||
|
||||
|
|
@ -21,15 +20,15 @@ enum AnalyticsUpdateType { server, local }
|
|||
/// 2) constructs used by the user, both in sending messages and doing practice activities
|
||||
class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
||||
late PangeaController _pangeaController;
|
||||
CachedStreamController<AnalyticsUpdate> analyticsUpdateStream =
|
||||
CachedStreamController<AnalyticsUpdate>();
|
||||
StreamController<AnalyticsUpdate> analyticsUpdateStream =
|
||||
StreamController.broadcast();
|
||||
StreamSubscription<AnalyticsStream>? _analyticsStream;
|
||||
StreamSubscription? _languageStream;
|
||||
Timer? _updateTimer;
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
||||
String? get userL2 => _pangeaController.languageController.activeL2Code();
|
||||
String? get _userL2 => _pangeaController.languageController.activeL2Code();
|
||||
|
||||
/// the last time that matrix analytics events were updated for the user's current l2
|
||||
DateTime? lastUpdated;
|
||||
|
|
@ -133,7 +132,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
|
||||
if (constructs.isEmpty) return;
|
||||
|
||||
final level = _pangeaController.getAnalytics.level;
|
||||
final level = _pangeaController.getAnalytics.constructListModel.level;
|
||||
|
||||
_addLocalMessage(eventID, constructs).then(
|
||||
(_) {
|
||||
|
|
@ -206,7 +205,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
}
|
||||
}
|
||||
|
||||
final level = _pangeaController.getAnalytics.level;
|
||||
final level = _pangeaController.getAnalytics.constructListModel.level;
|
||||
|
||||
// the list 'uses' gets altered in the _addLocalMessage method,
|
||||
// so copy it here to that the list of new uses is accurate
|
||||
|
|
@ -272,7 +271,8 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
return;
|
||||
}
|
||||
|
||||
final int newLevel = _pangeaController.getAnalytics.level;
|
||||
final int newLevel =
|
||||
_pangeaController.getAnalytics.constructListModel.level;
|
||||
newLevel > prevLevel
|
||||
? sendLocalAnalyticsToAnalyticsRoom()
|
||||
: analyticsUpdateStream.add(
|
||||
|
|
@ -374,7 +374,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
if (cachedConstructs.isEmpty || onlyDraft) return;
|
||||
|
||||
// if missing important info, don't send analytics. Could happen if user just signed up.
|
||||
final l2Code = l2Override ?? userL2;
|
||||
final l2Code = l2Override ?? _userL2;
|
||||
if (l2Code == null || _client.userID == null) return;
|
||||
|
||||
// analytics room for the user and current target language
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:developer';
|
|||
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/enum/analytics/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/analytics/parts_of_speech_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -43,6 +44,15 @@ extension ConstructExtension on ConstructTypeEnum {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ProgressIndicatorEnum get indicator {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.morph:
|
||||
return ProgressIndicatorEnum.morphsUsed;
|
||||
case ConstructTypeEnum.vocab:
|
||||
return ProgressIndicatorEnum.wordsUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructTypeUtil {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
|
@ -54,4 +55,15 @@ extension ProgressIndicatorsExtension on ProgressIndicatorEnum {
|
|||
return L10n.of(context)!.grammar;
|
||||
}
|
||||
}
|
||||
|
||||
ConstructTypeEnum get constructType {
|
||||
switch (this) {
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return ConstructTypeEnum.vocab;
|
||||
case ProgressIndicatorEnum.morphsUsed:
|
||||
return ConstructTypeEnum.morph;
|
||||
default:
|
||||
return ConstructTypeEnum.vocab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
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';
|
||||
|
|
@ -5,20 +7,36 @@ import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activ
|
|||
|
||||
/// 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 {
|
||||
final ConstructTypeEnum? type;
|
||||
void dispose() {
|
||||
_constructMap = {};
|
||||
_constructList = [];
|
||||
prevXP = 0;
|
||||
totalXP = 0;
|
||||
level = 0;
|
||||
vocabLemmas = 0;
|
||||
grammarLemmas = 0;
|
||||
}
|
||||
|
||||
/// A map of lemmas to ConstructUses, each of which contains a lemma
|
||||
/// key = lemmma + constructType.string, value = ConstructUses
|
||||
final Map<String, ConstructUses> _constructMap = {};
|
||||
Map<String, ConstructUses> _constructMap = {};
|
||||
|
||||
/// Storing this to avoid re-running the sort operation each time this needs to
|
||||
/// be accessed. It contains the same information as _constructMap, but sorted.
|
||||
List<ConstructUses> constructList = [];
|
||||
List<ConstructUses> _constructList = [];
|
||||
|
||||
/// A map of categories to lists of ConstructUses
|
||||
Map<String, List<ConstructUses>> _categoriesToUses = {};
|
||||
|
||||
/// Analytics data consumed by widgets. Updated each time new analytics come in.
|
||||
int prevXP = 0;
|
||||
int totalXP = 0;
|
||||
int level = 0;
|
||||
int vocabLemmas = 0;
|
||||
int grammarLemmas = 0;
|
||||
|
||||
ConstructListModel({
|
||||
required this.type,
|
||||
required List<OneConstructUse> uses,
|
||||
}) {
|
||||
updateConstructs(uses);
|
||||
|
|
@ -27,11 +45,10 @@ class ConstructListModel {
|
|||
/// Given a list of new construct uses, update the map of construct
|
||||
/// IDs to ConstructUses and re-sort the list of ConstructUses
|
||||
void updateConstructs(List<OneConstructUse> newUses) {
|
||||
final List<OneConstructUse> filteredUses = newUses
|
||||
.where((use) => use.constructType == type || type == null)
|
||||
.toList();
|
||||
_updateConstructMap(filteredUses);
|
||||
_updateConstructMap(newUses);
|
||||
_updateConstructList();
|
||||
_updateCategoriesToUses();
|
||||
_updateMetrics();
|
||||
}
|
||||
|
||||
/// A map of lemmas to ConstructUses, each of which contains a lemma
|
||||
|
|
@ -55,44 +72,64 @@ class ConstructListModel {
|
|||
/// a list of uses, sorted by the number of uses
|
||||
void _updateConstructList() {
|
||||
// TODO check how expensive this is
|
||||
constructList = _constructMap.values.toList();
|
||||
constructList.sort((a, b) {
|
||||
_constructList = _constructMap.values.toList();
|
||||
_constructList.sort((a, b) {
|
||||
final comp = b.uses.length.compareTo(a.uses.length);
|
||||
if (comp != 0) return comp;
|
||||
return a.lemma.compareTo(b.lemma);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateCategoriesToUses() {
|
||||
_categoriesToUses = {};
|
||||
for (final use in constructList()) {
|
||||
_categoriesToUses[use.category] ??= [];
|
||||
_categoriesToUses[use.category]!.add(use);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateMetrics() {
|
||||
vocabLemmas = constructList(type: ConstructTypeEnum.vocab)
|
||||
.map((e) => e.lemma)
|
||||
.toSet()
|
||||
.length;
|
||||
|
||||
grammarLemmas = constructList(type: ConstructTypeEnum.morph)
|
||||
.map((e) => e.lemma)
|
||||
.toSet()
|
||||
.length;
|
||||
|
||||
prevXP = totalXP;
|
||||
totalXP = _constructList.fold<int>(
|
||||
0,
|
||||
(total, construct) => total + construct.points,
|
||||
);
|
||||
level = 1 + sqrt((1 + 8 * totalXP / 100) / 2).floor();
|
||||
}
|
||||
|
||||
ConstructUses? getConstructUses(ConstructIdentifier identifier) {
|
||||
return _constructMap[identifier.string];
|
||||
}
|
||||
|
||||
List<ConstructUses> get constructListWithPoints =>
|
||||
constructList.where((constructUse) => constructUse.points > 0).toList();
|
||||
List<ConstructUses> constructList({ConstructTypeEnum? type}) => _constructList
|
||||
.where(
|
||||
(constructUse) =>
|
||||
constructUse.points > 0 &&
|
||||
(type == null || constructUse.constructType == type),
|
||||
)
|
||||
.toList();
|
||||
|
||||
/// All unique lemmas used in the construct events with non-zero points
|
||||
List<String> get lemmasWithPoints =>
|
||||
constructListWithPoints.map((e) => e.lemma).toSet().toList();
|
||||
|
||||
Map<String, List<ConstructUses>> get categoriesToUses {
|
||||
final Map<String, List<ConstructUses>> categoriesMap = {};
|
||||
for (final use in constructListWithPoints) {
|
||||
categoriesMap[use.category] ??= [];
|
||||
categoriesMap[use.category]!.add(use);
|
||||
}
|
||||
return categoriesMap;
|
||||
}
|
||||
|
||||
int get maxXPPerLemma =>
|
||||
type?.maxXPPerLemma ?? ConstructTypeEnum.vocab.maxXPPerLemma;
|
||||
|
||||
/// The total number of points for all uses of this construct type
|
||||
int get points {
|
||||
int totalPoints = 0;
|
||||
for (final constructUse in _constructMap.values.toList()) {
|
||||
totalPoints += constructUse.points;
|
||||
}
|
||||
return totalPoints;
|
||||
Map<String, List<ConstructUses>> categoriesToUses({ConstructTypeEnum? type}) {
|
||||
if (type == null) return _categoriesToUses;
|
||||
final entries = _categoriesToUses.entries.toList();
|
||||
return Map.fromEntries(
|
||||
entries.map((entry) {
|
||||
return MapEntry(
|
||||
entry.key,
|
||||
entry.value.where((use) => use.constructType == type).toList(),
|
||||
);
|
||||
}).where((entry) => entry.value.isNotEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ class OneConstructUse {
|
|||
ConstructIdentifier get identifier => ConstructIdentifier(
|
||||
lemma: lemma!,
|
||||
type: constructType,
|
||||
category: category,
|
||||
category: category ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
class ConstructIdentifier {
|
||||
final String lemma;
|
||||
final ConstructTypeEnum type;
|
||||
final String? category;
|
||||
final String category;
|
||||
|
||||
ConstructIdentifier({
|
||||
required this.lemma,
|
||||
required this.type,
|
||||
this.category,
|
||||
required this.category,
|
||||
});
|
||||
|
||||
factory ConstructIdentifier.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -37,7 +37,7 @@ class ConstructIdentifier {
|
|||
type: ConstructTypeEnum.values.firstWhere(
|
||||
(e) => e.string == json['type'],
|
||||
),
|
||||
category: category,
|
||||
category: category ?? "",
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -70,7 +70,7 @@ class ConstructIdentifier {
|
|||
}
|
||||
|
||||
String get string =>
|
||||
"$lemma-${type.string}${category != null ? "-$category" : "-other"}";
|
||||
"$lemma-${type.string}${category != "" ? "-$category" : "-other"}";
|
||||
}
|
||||
|
||||
class CandidateMessage {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,10 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
|||
late Animation<double> _fadeAnimation;
|
||||
|
||||
StreamSubscription? _pointsSubscription;
|
||||
int? get _prevXP => MatrixState.pangeaController.getAnalytics.prevXP;
|
||||
int? get _currentXP => MatrixState.pangeaController.getAnalytics.currentXP;
|
||||
int? get _prevXP =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel.prevXP;
|
||||
int? get _currentXP =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel.totalXP;
|
||||
int? _addedPoints;
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/analytics_popup/analytics_xp_tile.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class AnalyticsPopup extends StatefulWidget {
|
||||
final ProgressIndicatorEnum indicator;
|
||||
final ConstructListModel constructsModel;
|
||||
final ConstructTypeEnum type;
|
||||
final bool showGroups;
|
||||
|
||||
const AnalyticsPopup({
|
||||
required this.indicator,
|
||||
required this.constructsModel,
|
||||
required this.type,
|
||||
this.showGroups = true,
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -23,9 +23,14 @@ class AnalyticsPopup extends StatefulWidget {
|
|||
|
||||
class AnalyticsPopupState extends State<AnalyticsPopup> {
|
||||
String? selectedCategory;
|
||||
ConstructListModel get _constructsModel =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel;
|
||||
|
||||
List<MapEntry<String, List<ConstructUses>>> get categoriesToUses {
|
||||
final entries = widget.constructsModel.categoriesToUses.entries.toList();
|
||||
Map<String, List<ConstructUses>> get _categoriesToUses =>
|
||||
_constructsModel.categoriesToUses(type: widget.type);
|
||||
|
||||
List<MapEntry<String, List<ConstructUses>>> get _sortedEntries {
|
||||
final entries = _categoriesToUses.entries.toList();
|
||||
// Sort the list with custom logic
|
||||
entries.sort((a, b) {
|
||||
// Check if one of the keys is 'Other'
|
||||
|
|
@ -51,7 +56,7 @@ class AnalyticsPopupState extends State<AnalyticsPopup> {
|
|||
});
|
||||
|
||||
String categoryCopy(category) =>
|
||||
widget.constructsModel.type?.getDisplayCopy(
|
||||
widget.type.getDisplayCopy(
|
||||
category,
|
||||
context,
|
||||
) ??
|
||||
|
|
@ -61,10 +66,9 @@ class AnalyticsPopupState extends State<AnalyticsPopup> {
|
|||
Widget build(BuildContext context) {
|
||||
Widget? dialogContent;
|
||||
final bool hasNoData =
|
||||
widget.constructsModel.constructListWithPoints.isEmpty;
|
||||
final bool hasNoCategories =
|
||||
widget.constructsModel.categoriesToUses.length == 1 &&
|
||||
widget.constructsModel.categoriesToUses.keys.first == "Other";
|
||||
_constructsModel.constructList(type: widget.type).isEmpty;
|
||||
final bool hasNoCategories = _categoriesToUses.length == 1 &&
|
||||
_categoriesToUses.entries.first.key == "Other";
|
||||
|
||||
if (selectedCategory != null) {
|
||||
dialogContent = Column(
|
||||
|
|
@ -75,7 +79,7 @@ class AnalyticsPopupState extends State<AnalyticsPopup> {
|
|||
),
|
||||
Expanded(
|
||||
child: ConstructsTileList(
|
||||
widget.constructsModel.categoriesToUses[selectedCategory]!,
|
||||
_categoriesToUses[selectedCategory]!,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -84,13 +88,17 @@ class AnalyticsPopupState extends State<AnalyticsPopup> {
|
|||
dialogContent = Center(child: Text(L10n.of(context)!.noDataFound));
|
||||
} else if (hasNoCategories || !widget.showGroups) {
|
||||
dialogContent = ConstructsTileList(
|
||||
widget.constructsModel.constructListWithPoints,
|
||||
_constructsModel.constructList(type: widget.type).sorted((a, b) {
|
||||
final comp = b.points.compareTo(a.points);
|
||||
if (comp != 0) return comp;
|
||||
return a.lemma.compareTo(b.lemma);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dialogContent = ListView.builder(
|
||||
itemCount: categoriesToUses.length,
|
||||
itemCount: _sortedEntries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categoriesToUses[index];
|
||||
final category = _sortedEntries[index];
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
|
|
@ -115,7 +123,7 @@ class AnalyticsPopupState extends State<AnalyticsPopup> {
|
|||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.indicator.tooltip(context)),
|
||||
title: Text(widget.type.indicator.tooltip(context)),
|
||||
leading: IconButton(
|
||||
icon: selectedCategory == null
|
||||
? const Icon(Icons.close)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar_details.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LearningProgressBar extends StatelessWidget {
|
||||
final int totalXP;
|
||||
const LearningProgressBar({
|
||||
required this.totalXP,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ProgressBar(
|
||||
levelBars: [
|
||||
LevelBarDetails(
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
currentPoints: totalXP,
|
||||
widthMultiplier:
|
||||
MatrixState.pangeaController.getAnalytics.levelProgress,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,18 +2,15 @@ import 'dart:async';
|
|||
|
||||
import 'package:fluffychat/pages/chat_list/client_chooser_button.dart';
|
||||
import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
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/pages/settings_learning/settings_learning.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar_details.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/analytics_popup/analytics_popup.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/learning_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/level_badge.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/flag.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A summary of "My Analytics" shown at the top of the chat list
|
||||
|
|
@ -21,148 +18,58 @@ import 'package:flutter/material.dart';
|
|||
/// messages sent, words used, and error types, which can
|
||||
/// be clicked to access more fine-grained analytics data.
|
||||
class LearningProgressIndicators extends StatefulWidget {
|
||||
const LearningProgressIndicators({
|
||||
super.key,
|
||||
});
|
||||
const LearningProgressIndicators({super.key});
|
||||
|
||||
@override
|
||||
LearningProgressIndicatorsState createState() =>
|
||||
State<LearningProgressIndicators> createState() =>
|
||||
LearningProgressIndicatorsState();
|
||||
}
|
||||
|
||||
class LearningProgressIndicatorsState
|
||||
extends State<LearningProgressIndicators> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
ConstructListModel get _constructsModel =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel;
|
||||
bool _loading = true;
|
||||
|
||||
/// A stream subscription to listen for updates to
|
||||
/// the analytics data, either locally or from events
|
||||
StreamSubscription<AnalyticsStreamUpdate>? _analyticsUpdateSubscription;
|
||||
bool loading = true;
|
||||
|
||||
// Some buggy stuff is happening with this data not being updated at login, so switching
|
||||
// to stateful variables for now. Will switch this back later when I have more time to
|
||||
// figure out why it's now working.
|
||||
// int get serverXP => _pangeaController.analytics.serverXP;
|
||||
// int get totalXP => _pangeaController.analytics.currentXP;
|
||||
// int get level => _pangeaController.analytics.level;
|
||||
List<OneConstructUse> currentConstructs = [];
|
||||
int get currentXP => _pangeaController.getAnalytics.calcXP(currentConstructs);
|
||||
int get localXP => _pangeaController.getAnalytics.calcXP(
|
||||
_pangeaController.getAnalytics.locallyCachedConstructs,
|
||||
);
|
||||
int get serverXP => currentXP - localXP;
|
||||
int get level => _pangeaController.getAnalytics.level;
|
||||
StreamSubscription<AnalyticsStreamUpdate>? _analyticsSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
updateAnalyticsData(
|
||||
_pangeaController.getAnalytics.analyticsStream.value?.constructs ?? [],
|
||||
);
|
||||
_analyticsUpdateSubscription = _pangeaController
|
||||
.getAnalytics.analyticsStream.stream
|
||||
.listen((update) => updateAnalyticsData(update.constructs));
|
||||
_analyticsSubscription = MatrixState
|
||||
.pangeaController.getAnalytics.analyticsStream.stream
|
||||
.listen(updateData);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_analyticsUpdateSubscription?.cancel();
|
||||
_analyticsSubscription?.cancel();
|
||||
_analyticsSubscription = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Update the analytics data shown in the UI. This comes from a
|
||||
/// combination of stored events and locally cached data.
|
||||
Future<void> updateAnalyticsData(List<OneConstructUse> constructs) async {
|
||||
currentConstructs = constructs;
|
||||
if (loading) loading = false;
|
||||
void updateData(AnalyticsStreamUpdate _) {
|
||||
if (_loading) _loading = false;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
/// Get the number of points for a given progress indicator
|
||||
ConstructListModel? getConstructsModel(ProgressIndicatorEnum indicator) {
|
||||
int uniqueLemmas(ProgressIndicatorEnum indicator) {
|
||||
switch (indicator) {
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return _pangeaController.getAnalytics.vocabModel;
|
||||
case ProgressIndicatorEnum.morphsUsed:
|
||||
return _pangeaController.getAnalytics.grammarModel;
|
||||
return _constructsModel.grammarLemmas;
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return _constructsModel.vocabLemmas;
|
||||
default:
|
||||
return null;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of points for a given progress indicator
|
||||
int? getProgressPoints(ProgressIndicatorEnum indicator) {
|
||||
switch (indicator) {
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return _pangeaController
|
||||
.getAnalytics.vocabModel.lemmasWithPoints.length;
|
||||
case ProgressIndicatorEnum.morphsUsed:
|
||||
return _pangeaController
|
||||
.getAnalytics.grammarModel.lemmasWithPoints.length;
|
||||
case ProgressIndicatorEnum.level:
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
// double get levelBarWidth => FluffyThemes.columnWidth - (32 * 2) - 25;
|
||||
|
||||
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) {
|
||||
if (Matrix.of(context).client.userID == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final progressBar = ProgressBar(
|
||||
levelBars: [
|
||||
LevelBarDetails(
|
||||
fillColor: kDebugMode
|
||||
? const Color.fromARGB(255, 0, 190, 83)
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
currentPoints: currentXP,
|
||||
widthMultiplier: _pangeaController.getAnalytics.levelProgress,
|
||||
),
|
||||
LevelBarDetails(
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
currentPoints: serverXP,
|
||||
widthMultiplier: _pangeaController.getAnalytics.serverLevelProgress,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final levelBadge = Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: levelColor(level),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(5, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"$level",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
const ClientChooserButton(),
|
||||
|
|
@ -180,22 +87,19 @@ class LearningProgressIndicatorsState
|
|||
(indicator) => Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: ProgressIndicatorBadge(
|
||||
points: getProgressPoints(indicator),
|
||||
points: uniqueLemmas(indicator),
|
||||
loading: _loading,
|
||||
onTap: () {
|
||||
final model = getConstructsModel(indicator);
|
||||
if (model == null) return;
|
||||
showDialog<AnalyticsPopup>(
|
||||
context: context,
|
||||
builder: (c) => AnalyticsPopup(
|
||||
indicator: indicator,
|
||||
constructsModel: model,
|
||||
type: indicator.constructType,
|
||||
showGroups: indicator ==
|
||||
ProgressIndicatorEnum.morphsUsed,
|
||||
),
|
||||
);
|
||||
},
|
||||
progressIndicator: indicator,
|
||||
loading: loading,
|
||||
indicator: indicator,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -207,7 +111,8 @@ class LearningProgressIndicatorsState
|
|||
builder: (c) => const SettingsLearning(),
|
||||
),
|
||||
child: LanguageFlag(
|
||||
language: _pangeaController.languageController.userL2,
|
||||
language: MatrixState
|
||||
.pangeaController.languageController.userL2,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
|
|
@ -219,8 +124,17 @@ class LearningProgressIndicatorsState
|
|||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(left: 16, right: 0, child: progressBar),
|
||||
Positioned(left: 0, child: levelBadge),
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 0,
|
||||
child: LearningProgressBar(
|
||||
totalXP: _constructsModel.totalXP,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: LevelBadge(level: _constructsModel.level),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class LevelBadge extends StatelessWidget {
|
||||
final int level;
|
||||
const LevelBadge({
|
||||
required this.level,
|
||||
super.key,
|
||||
});
|
||||
|
||||
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) {
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: levelColor(level),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(5, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"$level",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,36 +3,36 @@ import 'package:flutter/material.dart';
|
|||
|
||||
/// A badge that represents one learning progress indicator (i.e., construct uses)
|
||||
class ProgressIndicatorBadge extends StatelessWidget {
|
||||
final int? points;
|
||||
final VoidCallback onTap;
|
||||
final ProgressIndicatorEnum progressIndicator;
|
||||
final bool loading;
|
||||
final int points;
|
||||
final VoidCallback onTap;
|
||||
final ProgressIndicatorEnum indicator;
|
||||
|
||||
const ProgressIndicatorBadge({
|
||||
super.key,
|
||||
required this.points,
|
||||
required this.onTap,
|
||||
required this.progressIndicator,
|
||||
required this.indicator,
|
||||
required this.loading,
|
||||
required this.points,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: progressIndicator.tooltip(context),
|
||||
message: indicator.tooltip(context),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
progressIndicator.icon,
|
||||
color: progressIndicator.color(context),
|
||||
indicator.icon,
|
||||
color: indicator.color(context),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
!loading
|
||||
? Text(
|
||||
points?.toString() ?? '0',
|
||||
points.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
|||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Seperated out the target tokens from the practice activity card
|
||||
|
|
@ -26,12 +25,12 @@ class TargetTokensController {
|
|||
|
||||
_targetTokens = await _initialize(pangeaMessageEvent);
|
||||
|
||||
final allConstructs = MatrixState
|
||||
.pangeaController.getAnalytics.analyticsStream.value?.constructs;
|
||||
await updateTokensWithConstructs(
|
||||
allConstructs ?? [],
|
||||
pangeaMessageEvent,
|
||||
);
|
||||
// final allConstructs = MatrixState
|
||||
// .pangeaController.getAnalytics.analyticsStream.value?.constructs;
|
||||
// await updateTokensWithConstructs(
|
||||
// allConstructs ?? [],
|
||||
// pangeaMessageEvent,
|
||||
// );
|
||||
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
|
@ -60,7 +59,7 @@ class TargetTokensController {
|
|||
) async {
|
||||
final ConstructListModel constructList = ConstructListModel(
|
||||
uses: constructUses,
|
||||
type: null,
|
||||
// type: null,
|
||||
);
|
||||
|
||||
_targetTokens ??= await _initialize(pangeaMessageEvent);
|
||||
|
|
@ -76,6 +75,7 @@ class TargetTokensController {
|
|||
ConstructIdentifier(
|
||||
lemma: construct.id.lemma,
|
||||
type: construct.id.type,
|
||||
category: construct.id.category,
|
||||
),
|
||||
);
|
||||
if (constructUseModel != null) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
|
|||
# Pangea#
|
||||
publish_to: none
|
||||
# On version bump also increase the build number for F-Droid
|
||||
version: 1.23.5+3564
|
||||
version: 1.23.6+3565
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue