inital work for having a 2-stage progress bar and saving draft analytics locally
This commit is contained in:
parent
474198c7a1
commit
75234e6a6f
22 changed files with 859 additions and 557 deletions
|
|
@ -12,6 +12,7 @@ import 'package:fluffychat/pages/chat/reply_display.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
|
|
@ -466,8 +467,15 @@ class ChatView extends StatelessWidget {
|
|||
StartIGCButton(
|
||||
controller: controller,
|
||||
),
|
||||
ChatFloatingActionButton(
|
||||
controller: controller,
|
||||
Row(
|
||||
children: [
|
||||
const PointsGainedAnimation(
|
||||
color: Colors.blue,
|
||||
),
|
||||
ChatFloatingActionButton(
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -931,6 +931,8 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
|
||||
// #Pangea
|
||||
MatrixState.pangeaController.myAnalytics.initialize();
|
||||
MatrixState.pangeaController.analytics.initialize();
|
||||
await _initPangeaControllers(client);
|
||||
// Pangea#
|
||||
if (!mounted) return;
|
||||
|
|
|
|||
|
|
@ -411,6 +411,7 @@ class Choreographer {
|
|||
choreoRecord = ChoreoRecord.newRecord;
|
||||
itController.clear();
|
||||
igc.clear();
|
||||
pangeaController.myAnalytics.clearDraftConstructUses(roomId);
|
||||
// errorService.clear();
|
||||
_resetDebounceTimer();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,6 +318,26 @@ class ITChoices extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
void selectContinuance(int index, BuildContext context) {
|
||||
final Continuance continuance =
|
||||
controller.currentITStep!.continuances[index];
|
||||
if (continuance.level == 1 || continuance.wasClicked) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() => controller.selectTranslation(index),
|
||||
);
|
||||
} else {
|
||||
showCard(
|
||||
context,
|
||||
index,
|
||||
continuance.level == 2 ? ChoreoConstants.yellow : ChoreoConstants.red,
|
||||
continuance.feedbackText(context),
|
||||
);
|
||||
}
|
||||
controller.currentITStep!.continuances[index].wasClicked = true;
|
||||
controller.choreographer.setState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
|
|
@ -342,31 +362,8 @@ class ITChoices extends StatelessWidget {
|
|||
return Choice(text: "error", color: Colors.red);
|
||||
}
|
||||
}).toList(),
|
||||
onPressed: (int index) {
|
||||
final Continuance continuance =
|
||||
controller.currentITStep!.continuances[index];
|
||||
debugPrint("is gold? ${continuance.gold}");
|
||||
if (continuance.level == 1 || continuance.wasClicked) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() => controller.selectTranslation(index),
|
||||
);
|
||||
} else {
|
||||
showCard(
|
||||
context,
|
||||
index,
|
||||
continuance.level == 2
|
||||
? ChoreoConstants.yellow
|
||||
: ChoreoConstants.red,
|
||||
continuance.feedbackText(context),
|
||||
);
|
||||
}
|
||||
controller.currentITStep!.continuances[index].wasClicked = true;
|
||||
controller.choreographer.setState();
|
||||
},
|
||||
onLongPress: (int index) {
|
||||
showCard(context, index);
|
||||
},
|
||||
onPressed: (int index) => selectContinuance(index, context),
|
||||
onLongPress: (int index) => showCard(context, index),
|
||||
uniqueKeyForLayerLink: (int index) => "itChoices$index",
|
||||
selectedChoiceIndex: null,
|
||||
);
|
||||
|
|
|
|||
3
lib/pangea/constants/analytics_constants.dart
Normal file
3
lib/pangea/constants/analytics_constants.dart
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class AnalyticsConstants {
|
||||
static const int xpPerLevel = 2000;
|
||||
}
|
||||
|
|
@ -1,33 +1,122 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package: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
|
||||
class GetAnalyticsController {
|
||||
late PangeaController _pangeaController;
|
||||
final List<AnalyticsCacheEntry> _cache = [];
|
||||
StreamSubscription<AnalyticsUpdateType>? _analyticsUpdateSubscription;
|
||||
CachedStreamController<List<OneConstructUse>> analyticsStream =
|
||||
CachedStreamController<List<OneConstructUse>>();
|
||||
|
||||
/// The previous XP points of the user, before the last update.
|
||||
/// Used for animating analytics updates.
|
||||
int? prevXP;
|
||||
|
||||
GetAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
int get level => currentXP ~/ AnalyticsConstants.xpPerLevel;
|
||||
|
||||
void initialize() {
|
||||
_analyticsUpdateSubscription ??= _pangeaController
|
||||
.myAnalytics.analyticsUpdateStream.stream
|
||||
.listen(onAnalyticsUpdate);
|
||||
|
||||
_pangeaController.myAnalytics.lastUpdatedCompleter.future.then((_) {
|
||||
getConstructs().then((_) => updateAnalyticsStream());
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear all cached analytics data.
|
||||
void dispose() {
|
||||
_analyticsUpdateSubscription?.cancel();
|
||||
_analyticsUpdateSubscription = null;
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
Future<void> onAnalyticsUpdate(AnalyticsUpdateType type) async {
|
||||
if (type == AnalyticsUpdateType.server) {
|
||||
await getConstructs(forceUpdate: true);
|
||||
}
|
||||
updateAnalyticsStream();
|
||||
}
|
||||
|
||||
void updateAnalyticsStream() {
|
||||
// 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?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set the previous XP to the currentXP
|
||||
if (analyticsStream.value != null) {
|
||||
prevXP = calcXP(analyticsStream.value!);
|
||||
}
|
||||
|
||||
// finally, add to the stream
|
||||
analyticsStream.add(allConstructUses);
|
||||
}
|
||||
|
||||
/// 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 errors = ConstructListModel(
|
||||
uses: constructs,
|
||||
type: ConstructTypeEnum.grammar,
|
||||
);
|
||||
return words.points + errors.points;
|
||||
}
|
||||
|
||||
List<OneConstructUse> get allConstructUses {
|
||||
final List<OneConstructUse> storedUses = getConstructsLocal() ?? [];
|
||||
final List<OneConstructUse> localUses = locallyCachedConstructs;
|
||||
|
||||
final List<OneConstructUse> allConstructs = [
|
||||
...storedUses,
|
||||
...localUses,
|
||||
];
|
||||
|
||||
return allConstructs;
|
||||
}
|
||||
|
||||
/// A local cache of eventIds and construct uses for messages sent since the last update.
|
||||
/// It's a map of eventIDs to a list of OneConstructUses. Not just a list of OneConstructUses
|
||||
/// because, with practice activity constructs, we might need to add to the list for a given
|
||||
/// eventID.
|
||||
Map<String, List<OneConstructUse>> get messagesSinceUpdate {
|
||||
try {
|
||||
final dynamic locallySaved = _pangeaController.pStoreService.read(
|
||||
|
|
@ -61,29 +150,34 @@ class GetAnalyticsController {
|
|||
}
|
||||
}
|
||||
|
||||
/// A flat list of all locally cached construct uses
|
||||
List<OneConstructUse> get locallyCachedConstructs =>
|
||||
messagesSinceUpdate.values.expand((e) => e).toList();
|
||||
|
||||
/// A flat list of all locally cached construct uses that are not drafts
|
||||
List<OneConstructUse> get locallyCachedSentConstructs =>
|
||||
messagesSinceUpdate.entries
|
||||
.where((entry) => !entry.key.startsWith('draft'))
|
||||
.expand((e) => e.value)
|
||||
.toList();
|
||||
|
||||
/// 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");
|
||||
// if the user isn't logged in, return an empty list
|
||||
if (client.userID == null) return [];
|
||||
await client.roomsLoading;
|
||||
|
||||
// don't try to get constructs until last updated time has been loaded
|
||||
await _pangeaController.myAnalytics.lastUpdatedCompleter.future;
|
||||
|
||||
// if forcing a refreshing, clear the cache
|
||||
if (forceUpdate) clearCache();
|
||||
|
||||
// get the last time the user updated their analytics for their current l2
|
||||
// then try to get local cache of construct uses. lastUpdate time is used to
|
||||
// determine if cached data is still valid.
|
||||
final DateTime? lastUpdated = _pangeaController.myAnalytics.lastUpdated ??
|
||||
await myAnalyticsLastUpdated();
|
||||
if (forceUpdate) _cache.clear();
|
||||
|
||||
final List<OneConstructUse>? local = getConstructsLocal(
|
||||
constructType: constructType,
|
||||
lastUpdated: lastUpdated,
|
||||
);
|
||||
|
||||
if (local != null) {
|
||||
|
|
@ -160,7 +254,6 @@ class GetAnalyticsController {
|
|||
|
||||
/// Get the cached construct uses for the current user, if it exists
|
||||
List<OneConstructUse>? getConstructsLocal({
|
||||
DateTime? lastUpdated,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
final index = _cache.indexWhere(
|
||||
|
|
@ -168,6 +261,7 @@ class GetAnalyticsController {
|
|||
);
|
||||
|
||||
if (index > -1) {
|
||||
final DateTime? lastUpdated = _pangeaController.myAnalytics.lastUpdated;
|
||||
if (_cache[index].needsUpdate(lastUpdated)) {
|
||||
_cache.removeAt(index);
|
||||
return null;
|
||||
|
|
@ -191,11 +285,6 @@ class GetAnalyticsController {
|
|||
);
|
||||
_cache.add(entry);
|
||||
}
|
||||
|
||||
/// Clear all cached analytics data.
|
||||
void clearCache() {
|
||||
_cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsCacheEntry {
|
||||
|
|
|
|||
|
|
@ -4,24 +4,32 @@ import 'package:fluffychat/pangea/constants/local.key.dart';
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/cached_stream_controller.dart';
|
||||
|
||||
enum AnalyticsUpdateType { server, local }
|
||||
|
||||
/// handles the processing of analytics for
|
||||
/// 1) messages sent by the user and
|
||||
/// 2) constructs used by the user, both in sending messages and doing practice activities
|
||||
class MyAnalyticsController extends BaseController {
|
||||
late PangeaController _pangeaController;
|
||||
final StreamController analyticsUpdateStream = StreamController.broadcast();
|
||||
CachedStreamController<AnalyticsUpdateType> analyticsUpdateStream =
|
||||
CachedStreamController<AnalyticsUpdateType>();
|
||||
StreamSubscription? _messageSendSubscription;
|
||||
Timer? _updateTimer;
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
|
@ -37,7 +45,7 @@ class MyAnalyticsController extends BaseController {
|
|||
|
||||
/// the max number of messages that will be cached before
|
||||
/// an automatic update is triggered
|
||||
final int _maxMessagesCached = 1;
|
||||
final int _maxMessagesCached = 10;
|
||||
|
||||
/// the number of minutes before an automatic update is triggered
|
||||
final int _minutesBeforeUpdate = 5;
|
||||
|
|
@ -47,22 +55,28 @@ class MyAnalyticsController extends BaseController {
|
|||
|
||||
MyAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (_pangeaController.matrixState.client.prevBatch == null) {
|
||||
_pangeaController.matrixState.client.onSync.stream.first.then(
|
||||
(_) => _refreshAnalyticsIfOutdated(),
|
||||
);
|
||||
} else {
|
||||
_refreshAnalyticsIfOutdated();
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
// Listen to a stream that provides the eventIDs
|
||||
// of new messages sent by the logged in user
|
||||
stateStream.where((data) => data is Map).listen((data) {
|
||||
onMessageSent(data as Map<String, dynamic>);
|
||||
});
|
||||
_messageSendSubscription ??= stateStream
|
||||
.where((data) => data is Map)
|
||||
.listen((data) => onMessageSent(data as Map<String, dynamic>));
|
||||
|
||||
_refreshAnalyticsIfOutdated();
|
||||
}
|
||||
|
||||
/// Reset analytics last updated time to null.
|
||||
@override
|
||||
void dispose() {
|
||||
_updateTimer?.cancel();
|
||||
lastUpdated = null;
|
||||
lastUpdatedCompleter = Completer<DateTime?>();
|
||||
_messageSendSubscription?.cancel();
|
||||
_messageSendSubscription = null;
|
||||
_refreshAnalyticsIfOutdated();
|
||||
clearMessagesSinceUpdate();
|
||||
}
|
||||
|
||||
/// If analytics haven't been updated in the last day, update them
|
||||
|
|
@ -98,6 +112,7 @@ class MyAnalyticsController extends BaseController {
|
|||
void onMessageSent(Map<String, dynamic> data) {
|
||||
// cancel the last timer that was set on message event and
|
||||
// reset it to fire after _minutesBeforeUpdate minutes
|
||||
debugPrint("ONE MESSAGE SENT");
|
||||
_updateTimer?.cancel();
|
||||
_updateTimer = Timer(Duration(minutes: _minutesBeforeUpdate), () {
|
||||
debugPrint("timer fired, updating analytics");
|
||||
|
|
@ -154,27 +169,82 @@ class MyAnalyticsController extends BaseController {
|
|||
|
||||
_pangeaController.analytics
|
||||
.filterConstructs(unfilteredConstructs: constructs)
|
||||
.then((filtered) => addMessageSinceUpdate(eventID, filtered));
|
||||
.then((filtered) {
|
||||
if (filtered.isEmpty) return;
|
||||
final level = _pangeaController.analytics.level;
|
||||
addLocalMessage(eventID, filtered).then(
|
||||
(_) => afterAddLocalMessages(level),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Called when the user selects a replacement during IGC
|
||||
void onReplacementSelected(
|
||||
List<PangeaToken> tokens,
|
||||
String roomID,
|
||||
bool isBestCorrection,
|
||||
) {
|
||||
final useType = isBestCorrection
|
||||
? ConstructUseTypeEnum.corIGC
|
||||
: ConstructUseTypeEnum.incIGC;
|
||||
setDraftConstructUses(tokens, roomID, useType);
|
||||
}
|
||||
|
||||
/// Called when the user ignores a match during IGC
|
||||
void onIgnoreMatch(
|
||||
List<PangeaToken> tokens,
|
||||
String roomID,
|
||||
) {
|
||||
const useType = ConstructUseTypeEnum.ignIGC;
|
||||
setDraftConstructUses(tokens, roomID, useType);
|
||||
}
|
||||
|
||||
void setDraftConstructUses(
|
||||
List<PangeaToken> tokens,
|
||||
String roomID,
|
||||
ConstructUseTypeEnum useType,
|
||||
) {
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomID,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
final uses = tokens
|
||||
.map(
|
||||
(token) => OneConstructUse(
|
||||
useType: useType,
|
||||
lemma: token.lemma.text,
|
||||
form: token.lemma.form,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: metadata,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
addLocalMessage('draft$roomID', uses);
|
||||
}
|
||||
|
||||
void clearDraftConstructUses(String roomID) {
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
currentCache.remove('draft$roomID');
|
||||
setMessagesSinceUpdate(currentCache);
|
||||
}
|
||||
|
||||
/// Called when the user selects a continuance during IT
|
||||
/// TODO implement
|
||||
void onSelectContinuance() {}
|
||||
|
||||
/// Add a list of construct uses for a new message to the local
|
||||
/// cache of recently sent messages
|
||||
void addMessageSinceUpdate(
|
||||
Future<void> addLocalMessage(
|
||||
String eventID,
|
||||
List<OneConstructUse> constructs,
|
||||
) {
|
||||
) async {
|
||||
try {
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
constructs.addAll(currentCache[eventID] ?? []);
|
||||
currentCache[eventID] = constructs;
|
||||
setMessagesSinceUpdate(currentCache);
|
||||
|
||||
// if the cached has reached if max-length, update analytics
|
||||
if (_pangeaController.analytics.messagesSinceUpdate.length >
|
||||
_maxMessagesCached) {
|
||||
debugPrint("reached max messages, updating");
|
||||
updateAnalytics();
|
||||
}
|
||||
await setMessagesSinceUpdate(currentCache);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError("Failed to add message since update: $e"),
|
||||
|
|
@ -184,23 +254,42 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handles cleanup after adding a new message to the local cache.
|
||||
/// If the addition brought the total number of messages in the cache
|
||||
/// to the max, or if the addition triggered a level-up, update the analytics.
|
||||
/// Otherwise, add a local update to the alert stream.
|
||||
void afterAddLocalMessages(int prevLevel) {
|
||||
if (_pangeaController.analytics.messagesSinceUpdate.length >
|
||||
_maxMessagesCached) {
|
||||
debugPrint("reached max messages, updating");
|
||||
updateAnalytics();
|
||||
return;
|
||||
}
|
||||
|
||||
final int newLevel = _pangeaController.analytics.level;
|
||||
newLevel > prevLevel
|
||||
? updateAnalytics()
|
||||
: analyticsUpdateStream.add(AnalyticsUpdateType.local);
|
||||
}
|
||||
|
||||
/// Clears the local cache of recently sent constructs. Called before updating analytics
|
||||
void clearMessagesSinceUpdate() {
|
||||
_pangeaController.pStoreService.delete(PLocalKey.messagesSinceUpdate);
|
||||
}
|
||||
|
||||
/// Save the local cache of recently sent constructs to the local storage
|
||||
void setMessagesSinceUpdate(Map<String, List<OneConstructUse>> cache) {
|
||||
Future<void> setMessagesSinceUpdate(
|
||||
Map<String, List<OneConstructUse>> cache,
|
||||
) async {
|
||||
final formattedCache = {};
|
||||
for (final entry in cache.entries) {
|
||||
final constructJsons = entry.value.map((e) => e.toJson()).toList();
|
||||
formattedCache[entry.key] = constructJsons;
|
||||
}
|
||||
_pangeaController.pStoreService.save(
|
||||
await _pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
formattedCache,
|
||||
);
|
||||
analyticsUpdateStream.add(null);
|
||||
}
|
||||
|
||||
/// Prevent concurrent updates to analytics
|
||||
|
|
@ -223,8 +312,9 @@ class MyAnalyticsController extends BaseController {
|
|||
try {
|
||||
await _updateAnalytics();
|
||||
clearMessagesSinceUpdate();
|
||||
|
||||
lastUpdated = DateTime.now();
|
||||
analyticsUpdateStream.add(null);
|
||||
analyticsUpdateStream.add(AnalyticsUpdateType.server);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
|
|
@ -241,7 +331,10 @@ class MyAnalyticsController extends BaseController {
|
|||
/// The analytics room is determined based on the user's current target language.
|
||||
Future<void> _updateAnalytics() async {
|
||||
// if there's no cached construct data, there's nothing to send
|
||||
if (_pangeaController.analytics.messagesSinceUpdate.isEmpty) return;
|
||||
final cachedConstructs = _pangeaController.analytics.messagesSinceUpdate;
|
||||
final bool onlyDraft = cachedConstructs.length == 1 &&
|
||||
cachedConstructs.keys.single.startsWith('draft');
|
||||
if (cachedConstructs.isEmpty || onlyDraft) return;
|
||||
|
||||
// if missing important info, don't send analytics. Could happen if user just signed up.
|
||||
if (userL2 == null || _client.userID == null) return;
|
||||
|
|
@ -251,17 +344,7 @@ class MyAnalyticsController extends BaseController {
|
|||
|
||||
// and send cached analytics data to the room
|
||||
await analyticsRoom?.sendConstructsEvent(
|
||||
_pangeaController.analytics.messagesSinceUpdate.values
|
||||
.expand((e) => e)
|
||||
.toList(),
|
||||
_pangeaController.analytics.locallyCachedSentConstructs,
|
||||
);
|
||||
}
|
||||
|
||||
/// Reset analytics last updated time to null.
|
||||
void clearCache() {
|
||||
_updateTimer?.cancel();
|
||||
lastUpdated = null;
|
||||
lastUpdatedCompleter = Completer<DateTime?>();
|
||||
_refreshAnalyticsIfOutdated();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,19 @@ class PangeaController {
|
|||
|
||||
/// check user information if not found then redirect to Date of birth page
|
||||
_handleLoginStateChange(LoginState state) {
|
||||
switch (state) {
|
||||
case LoginState.loggedOut:
|
||||
case LoginState.softLoggedOut:
|
||||
// Reset cached analytics data
|
||||
MatrixState.pangeaController.myAnalytics.dispose();
|
||||
MatrixState.pangeaController.analytics.dispose();
|
||||
break;
|
||||
case LoginState.loggedIn:
|
||||
// Initialize analytics data
|
||||
MatrixState.pangeaController.myAnalytics.initialize();
|
||||
MatrixState.pangeaController.analytics.initialize();
|
||||
break;
|
||||
}
|
||||
if (state != LoginState.loggedIn) {
|
||||
_logOutfromPangea();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,20 +82,27 @@ class ConstructListModel {
|
|||
|
||||
/// The total number of points for all uses of this construct type
|
||||
int get points {
|
||||
double totalPoints = 0;
|
||||
// double totalPoints = 0;
|
||||
return typedConstructs.fold<int>(
|
||||
0,
|
||||
(total, typedConstruct) =>
|
||||
total +
|
||||
typedConstruct.useType.pointValue * typedConstruct.uses.length,
|
||||
);
|
||||
// Commenting this out for now
|
||||
// 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();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -227,37 +227,6 @@ class IGCTextData {
|
|||
decorationThickness: 5,
|
||||
);
|
||||
|
||||
List<MatchToken> getMatchTokens() {
|
||||
final List<MatchToken> matchTokens = [];
|
||||
int? endTokenIndex;
|
||||
PangeaMatch? topMatch;
|
||||
for (final (i, token) in tokens.indexed) {
|
||||
if (endTokenIndex != null) {
|
||||
if (i <= endTokenIndex) {
|
||||
matchTokens.add(
|
||||
MatchToken(
|
||||
token: token,
|
||||
match: topMatch,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
endTokenIndex = null;
|
||||
}
|
||||
topMatch = getTopMatchForToken(token);
|
||||
if (topMatch != null) {
|
||||
endTokenIndex = tokens.indexWhere((e) => e.end >= topMatch!.end, i);
|
||||
}
|
||||
matchTokens.add(
|
||||
MatchToken(
|
||||
token: token,
|
||||
match: topMatch,
|
||||
),
|
||||
);
|
||||
}
|
||||
return matchTokens;
|
||||
}
|
||||
|
||||
TextSpan getSpanItem({
|
||||
required int start,
|
||||
required int end,
|
||||
|
|
@ -347,11 +316,19 @@ class IGCTextData {
|
|||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
class MatchToken {
|
||||
final PangeaToken token;
|
||||
final PangeaMatch? match;
|
||||
List<PangeaToken> matchTokens(int matchIndex) {
|
||||
if (matchIndex >= matches.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
MatchToken({required this.token, this.match});
|
||||
final PangeaMatch match = matches[matchIndex];
|
||||
final List<PangeaToken> tokensForMatch = [];
|
||||
for (final token in tokens) {
|
||||
if (match.isOffsetInMatchSpan(token.text.offset)) {
|
||||
tokensForMatch.add(token);
|
||||
}
|
||||
}
|
||||
return tokensForMatch;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
// Call to server for additional/followup info
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../enum/span_choice_type.dart';
|
||||
|
|
@ -99,14 +101,31 @@ class Context {
|
|||
}
|
||||
|
||||
class SpanChoice {
|
||||
String value;
|
||||
SpanChoiceType type;
|
||||
bool selected;
|
||||
String? feedback;
|
||||
DateTime? timestamp;
|
||||
List<PangeaToken> tokens;
|
||||
|
||||
SpanChoice({
|
||||
required this.value,
|
||||
required this.type,
|
||||
this.feedback,
|
||||
this.selected = false,
|
||||
this.timestamp,
|
||||
this.tokens = const [],
|
||||
});
|
||||
|
||||
factory SpanChoice.fromJson(Map<String, dynamic> json) {
|
||||
final List<PangeaToken> tokensInternal = (json[ModelKey.tokens] != null)
|
||||
? (json[ModelKey.tokens] as Iterable)
|
||||
.map<PangeaToken>(
|
||||
(e) => PangeaToken.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
.cast<PangeaToken>()
|
||||
: [];
|
||||
return SpanChoice(
|
||||
value: json['value'] as String,
|
||||
type: json['type'] != null
|
||||
|
|
@ -119,21 +138,17 @@ class SpanChoice {
|
|||
selected: json['selected'] ?? false,
|
||||
timestamp:
|
||||
json['timestamp'] != null ? DateTime.parse(json['timestamp']) : null,
|
||||
tokens: tokensInternal,
|
||||
);
|
||||
}
|
||||
|
||||
String value;
|
||||
SpanChoiceType type;
|
||||
bool selected;
|
||||
String? feedback;
|
||||
DateTime? timestamp;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'value': value,
|
||||
'type': type.name,
|
||||
'selected': selected,
|
||||
'feedback': feedback,
|
||||
'timestamp': timestamp?.toIso8601String(),
|
||||
'tokens': tokens.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
String feedbackToDisplay(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async {
|
|||
// before wiping out locally cached construct data, save it to the server
|
||||
await MatrixState.pangeaController.myAnalytics.updateAnalytics();
|
||||
|
||||
// Reset cached analytics data
|
||||
MatrixState.pangeaController.myAnalytics.clearCache();
|
||||
MatrixState.pangeaController.analytics.clearCache();
|
||||
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => matrix.client.logout(),
|
||||
|
|
|
|||
103
lib/pangea/widgets/animations/gain_points.dart
Normal file
103
lib/pangea/widgets/animations/gain_points.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PointsGainedAnimation extends StatefulWidget {
|
||||
final Color? color;
|
||||
const PointsGainedAnimation({super.key, this.color});
|
||||
|
||||
@override
|
||||
PointsGainedAnimationState createState() => PointsGainedAnimationState();
|
||||
}
|
||||
|
||||
class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _offsetAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
StreamSubscription? _pointsSubscription;
|
||||
int? get _prevXP => MatrixState.pangeaController.analytics.prevXP;
|
||||
int? get _currentXP => MatrixState.pangeaController.analytics.currentXP;
|
||||
int? _addedPoints;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_offsetAnimation = Tween<Offset>(
|
||||
begin: const Offset(0.0, 0.0),
|
||||
end: const Offset(0.0, -1.0),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
);
|
||||
|
||||
_pointsSubscription = MatrixState
|
||||
.pangeaController.analytics.analyticsStream.stream
|
||||
.listen(_showPointsGained);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_pointsSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showPointsGained(List<OneConstructUse> constructs) {
|
||||
setState(() => _addedPoints = (_currentXP ?? 0) - (_prevXP ?? 0));
|
||||
if (_prevXP != _currentXP && !_controller.isAnimating) {
|
||||
_controller.reset();
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
bool get animate =>
|
||||
_currentXP != null &&
|
||||
_prevXP != null &&
|
||||
_addedPoints != null &&
|
||||
_prevXP! != _currentXP!;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!animate) return const SizedBox();
|
||||
|
||||
return SlideTransition(
|
||||
position: _offsetAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Text(
|
||||
'+$_addedPoints',
|
||||
style: BotStyle.text(
|
||||
context,
|
||||
big: true,
|
||||
setColor: widget.color == null,
|
||||
existingStyle: TextStyle(
|
||||
color: widget.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedLevelBar extends StatefulWidget {
|
||||
final double height;
|
||||
final double beginWidth;
|
||||
final double endWidth;
|
||||
final BoxDecoration? decoration;
|
||||
|
||||
const AnimatedLevelBar({
|
||||
super.key,
|
||||
required this.height,
|
||||
required this.beginWidth,
|
||||
required this.endWidth,
|
||||
this.decoration,
|
||||
});
|
||||
|
||||
@override
|
||||
AnimatedLevelBarState createState() => AnimatedLevelBarState();
|
||||
}
|
||||
|
||||
class AnimatedLevelBarState extends State<AnimatedLevelBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
/// Whether the animation has run for the first time during initState. Don't
|
||||
/// want the animation to run when the widget mounts, only when points are gained.
|
||||
bool _init = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_controller.forward().then((_) => _init = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AnimatedLevelBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.endWidth != widget.endWidth) {
|
||||
_controller.reset();
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Animation<double> get _animation {
|
||||
// If this is the first run of the animation, don't animate. This is just the widget mounting,
|
||||
// not a points gain. This could instead be 'if going from 0 to a non-zero value', but that
|
||||
// would remove the animation for first points gained. It would remove the need for a flag though.
|
||||
if (_init) {
|
||||
return Tween<double>(
|
||||
begin: widget.endWidth,
|
||||
end: widget.endWidth,
|
||||
).animate(_controller);
|
||||
}
|
||||
|
||||
// animate the width of the bar
|
||||
return Tween<double>(
|
||||
begin: widget.beginWidth,
|
||||
end: widget.endWidth,
|
||||
).animate(_controller);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
height: widget.height,
|
||||
width: _animation.value,
|
||||
decoration: widget.decoration,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/pangea/widgets/animations/progress_bar/level_bar.dart
Normal file
59
lib/pangea/widgets/animations/progress_bar/level_bar.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/animated_level_dart.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar_details.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LevelBar extends StatefulWidget {
|
||||
final LevelBarDetails details;
|
||||
final ProgressBarDetails progressBarDetails;
|
||||
|
||||
const LevelBar({
|
||||
super.key,
|
||||
required this.details,
|
||||
required this.progressBarDetails,
|
||||
});
|
||||
|
||||
@override
|
||||
LevelBarState createState() => LevelBarState();
|
||||
}
|
||||
|
||||
class LevelBarState extends State<LevelBar> {
|
||||
double prevWidth = 0;
|
||||
|
||||
double get width {
|
||||
const perLevel = AnalyticsConstants.xpPerLevel;
|
||||
final percent = (widget.details.currentPoints % perLevel) / perLevel;
|
||||
return widget.progressBarDetails.totalWidth * percent;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LevelBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.details.currentPoints != widget.details.currentPoints) {
|
||||
setState(() => prevWidth = width);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedLevelBar(
|
||||
height: widget.progressBarDetails.height,
|
||||
beginWidth: prevWidth,
|
||||
endWidth: width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
color: widget.details.fillColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(5, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/pangea/widgets/animations/progress_bar/progress_bar.dart
Normal file
36
lib/pangea/widgets/animations/progress_bar/progress_bar.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/level_bar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar_background.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar_details.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Provide an order list of level indicators, each with it's color
|
||||
// and stream. Also provide an overall width and pointsPerLevel.
|
||||
|
||||
class ProgressBar extends StatelessWidget {
|
||||
final List<LevelBarDetails> levelBars;
|
||||
final ProgressBarDetails progressBarDetails;
|
||||
|
||||
const ProgressBar({
|
||||
super.key,
|
||||
required this.levelBars,
|
||||
required this.progressBarDetails,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
ProgressBarBackground(details: progressBarDetails),
|
||||
for (final levelBar in levelBars)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: LevelBar(
|
||||
details: levelBar,
|
||||
progressBarDetails: progressBarDetails,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar_details.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProgressBarBackground extends StatelessWidget {
|
||||
final ProgressBarDetails details;
|
||||
|
||||
const ProgressBarBackground({
|
||||
super.key,
|
||||
required this.details,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: details.height + 4,
|
||||
width: details.totalWidth + 4,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: details.borderColor.withOpacity(0.5),
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
color: details.borderColor.withOpacity(0.2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import 'dart:ui';
|
||||
|
||||
class LevelBarDetails {
|
||||
final Color fillColor;
|
||||
final int currentPoints;
|
||||
|
||||
const LevelBarDetails({
|
||||
required this.fillColor,
|
||||
required this.currentPoints,
|
||||
});
|
||||
}
|
||||
|
||||
class ProgressBarDetails {
|
||||
final double totalWidth;
|
||||
final Color borderColor;
|
||||
final double height;
|
||||
|
||||
const ProgressBarDetails({
|
||||
required this.totalWidth,
|
||||
required this.borderColor,
|
||||
this.height = 16,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.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/progress_indicator.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -34,7 +37,7 @@ class LearningProgressIndicatorsState
|
|||
|
||||
/// A stream subscription to listen for updates to
|
||||
/// the analytics data, either locally or from events
|
||||
StreamSubscription? _onAnalyticsUpdate;
|
||||
StreamSubscription<List<OneConstructUse>>? _analyticsUpdateSubscription;
|
||||
|
||||
/// Vocabulary constructs model
|
||||
ConstructListModel? words;
|
||||
|
|
@ -44,66 +47,39 @@ class LearningProgressIndicatorsState
|
|||
|
||||
bool loading = true;
|
||||
|
||||
/// The previous number of XP points, used to determine when to animate the level bar
|
||||
int? previousXP;
|
||||
int get serverXP => _pangeaController.analytics.serverXP;
|
||||
int get totalXP => _pangeaController.analytics.currentXP;
|
||||
int get level => _pangeaController.analytics.level;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
updateAnalyticsData().then((_) {
|
||||
setState(() => loading = false);
|
||||
});
|
||||
// listen for changes to analytics data and update the UI
|
||||
_onAnalyticsUpdate = _pangeaController
|
||||
.myAnalytics.analyticsUpdateStream.stream
|
||||
.listen((_) => updateAnalyticsData());
|
||||
updateAnalyticsData(
|
||||
_pangeaController.analytics.analyticsStream.value ?? [],
|
||||
);
|
||||
_pangeaController.analytics.analyticsStream.stream
|
||||
.listen(updateAnalyticsData);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_onAnalyticsUpdate?.cancel();
|
||||
_analyticsUpdateSubscription?.cancel();
|
||||
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() async {
|
||||
previousXP = xpPoints;
|
||||
|
||||
final List<OneConstructUse> storedUses =
|
||||
await _pangeaController.analytics.getConstructs();
|
||||
final List<OneConstructUse> localUses = [];
|
||||
for (final uses in _pangeaController.analytics.messagesSinceUpdate.values) {
|
||||
localUses.addAll(uses);
|
||||
}
|
||||
|
||||
if (storedUses.isEmpty) {
|
||||
words = ConstructListModel(
|
||||
type: ConstructTypeEnum.vocab,
|
||||
uses: localUses,
|
||||
);
|
||||
errors = ConstructListModel(
|
||||
type: ConstructTypeEnum.grammar,
|
||||
uses: localUses,
|
||||
);
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
|
||||
final List<OneConstructUse> allConstructs = [
|
||||
...storedUses,
|
||||
...localUses,
|
||||
];
|
||||
|
||||
Future<void> updateAnalyticsData(List<OneConstructUse> constructs) async {
|
||||
words = ConstructListModel(
|
||||
type: ConstructTypeEnum.vocab,
|
||||
uses: allConstructs,
|
||||
uses: constructs,
|
||||
);
|
||||
errors = ConstructListModel(
|
||||
type: ConstructTypeEnum.grammar,
|
||||
uses: allConstructs,
|
||||
uses: constructs,
|
||||
);
|
||||
|
||||
if (loading) loading = false;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
|
|
@ -119,21 +95,10 @@ class LearningProgressIndicatorsState
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the total number of xp points, based on the point values of use types.
|
||||
/// Null if niether words nor error constructs are available.
|
||||
int? get xpPoints {
|
||||
if (words == null && errors == null) return null;
|
||||
if (words == null) return errors!.points;
|
||||
if (errors == null) return words!.points;
|
||||
return words!.points + errors!.points;
|
||||
}
|
||||
|
||||
/// Get the current level based on the number of xp points
|
||||
int get level => (xpPoints ?? 0) ~/ 500;
|
||||
|
||||
double get levelBarWidth => FluffyThemes.columnWidth - (32 * 2) - 25;
|
||||
double get pointsBarWidth {
|
||||
final percent = ((xpPoints ?? 0) % 500) / 500;
|
||||
final percent = (totalXP % AnalyticsConstants.xpPerLevel) /
|
||||
AnalyticsConstants.xpPerLevel;
|
||||
return levelBarWidth * percent;
|
||||
}
|
||||
|
||||
|
|
@ -149,61 +114,42 @@ class LearningProgressIndicatorsState
|
|||
return colors[level % colors.length];
|
||||
}
|
||||
|
||||
/// Whether to animate the level bar increase. Prevents this bar from seeming to
|
||||
/// reload each time the user navigates to a different space or back to the chat list.
|
||||
/// PreviousXP would be null if this widget just mounted. Also handles case of rebuilds
|
||||
/// without any change in XP points.
|
||||
bool get animate => previousXP != null && previousXP != xpPoints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Matrix.of(context).client.userID == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final levelBar = Container(
|
||||
height: 20,
|
||||
width: levelBarWidth,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
width: 2,
|
||||
final progressBar = ProgressBar(
|
||||
levelBars: [
|
||||
LevelBarDetails(
|
||||
fillColor: const Color.fromARGB(255, 0, 190, 83),
|
||||
currentPoints: totalXP,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||
LevelBarDetails(
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
currentPoints: serverXP,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
|
||||
],
|
||||
progressBarDetails: ProgressBarDetails(
|
||||
totalWidth: levelBarWidth,
|
||||
borderColor: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
);
|
||||
|
||||
final xpBarDecoration = BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
|
||||
final xpBar = animate
|
||||
? AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
height: 16,
|
||||
width: pointsBarWidth,
|
||||
decoration: xpBarDecoration,
|
||||
)
|
||||
: Container(
|
||||
height: 16,
|
||||
width: pointsBarWidth,
|
||||
decoration: xpBarDecoration,
|
||||
);
|
||||
|
||||
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(
|
||||
|
|
@ -213,61 +159,71 @@ class LearningProgressIndicatorsState
|
|||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(46, 0, 32, 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future:
|
||||
_pangeaController.matrixState.client.getProfileFromUserId(
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final mxid = Matrix.of(context).client.userID ??
|
||||
L10n.of(context)!.user;
|
||||
return Avatar(
|
||||
name: snapshot.data?.displayName ?? mxid.localpart ?? mxid,
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
size: 40,
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: ProgressIndicatorEnum.values
|
||||
.where(
|
||||
(indicator) => indicator != ProgressIndicatorEnum.level,
|
||||
)
|
||||
.map(
|
||||
(indicator) => ProgressIndicatorBadge(
|
||||
points: getProgressPoints(indicator),
|
||||
onTap: () {},
|
||||
progressIndicator: indicator,
|
||||
loading: loading,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Positioned(
|
||||
child: PointsGainedAnimation(),
|
||||
),
|
||||
Container(
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(left: 16, right: 0, child: levelBar),
|
||||
Positioned(left: 16, child: xpBar),
|
||||
Positioned(left: 0, child: levelBadge),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(46, 0, 32, 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: _pangeaController.matrixState.client
|
||||
.getProfileFromUserId(
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final mxid = Matrix.of(context).client.userID ??
|
||||
L10n.of(context)!.user;
|
||||
return Avatar(
|
||||
name: snapshot.data?.displayName ??
|
||||
mxid.localpart ??
|
||||
mxid,
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
size: 40,
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: ProgressIndicatorEnum.values
|
||||
.where(
|
||||
(indicator) =>
|
||||
indicator != ProgressIndicatorEnum.level,
|
||||
)
|
||||
.map(
|
||||
(indicator) => ProgressIndicatorBadge(
|
||||
points: getProgressPoints(indicator),
|
||||
onTap: () {},
|
||||
progressIndicator: indicator,
|
||||
loading: loading,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(left: 16, right: 0, child: progressBar),
|
||||
Positioned(left: 0, child: levelBadge),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ import '../common/bot_face_svg.dart';
|
|||
import 'card_header.dart';
|
||||
import 'why_button.dart';
|
||||
|
||||
const wordMatchResultsCount = 5;
|
||||
|
||||
//switch for definition vs correction vs practice
|
||||
|
||||
//always show a title
|
||||
|
|
@ -32,12 +30,12 @@ const wordMatchResultsCount = 5;
|
|||
class SpanCard extends StatefulWidget {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
final SpanCardModel scm;
|
||||
final String? roomId;
|
||||
final String roomId;
|
||||
|
||||
SpanCard({
|
||||
super.key,
|
||||
required this.scm,
|
||||
this.roomId,
|
||||
required this.roomId,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -62,12 +60,16 @@ class SpanCardState extends State<SpanCard> {
|
|||
|
||||
//get selected choice
|
||||
SpanChoice? get selectedChoice {
|
||||
if (selectedChoiceIndex == null ||
|
||||
widget.scm.pangeaMatch?.match.choices == null ||
|
||||
widget.scm.pangeaMatch!.match.choices!.length <= selectedChoiceIndex!) {
|
||||
if (selectedChoiceIndex == null) return null;
|
||||
return choiceByIndex(selectedChoiceIndex!);
|
||||
}
|
||||
|
||||
SpanChoice? choiceByIndex(int index) {
|
||||
if (widget.scm.pangeaMatch?.match.choices == null ||
|
||||
widget.scm.pangeaMatch!.match.choices!.length <= index) {
|
||||
return null;
|
||||
}
|
||||
return widget.scm.pangeaMatch?.match.choices?[selectedChoiceIndex!];
|
||||
return widget.scm.pangeaMatch?.match.choices?[index];
|
||||
}
|
||||
|
||||
void fetchSelected() {
|
||||
|
|
@ -76,8 +78,9 @@ class SpanCardState extends State<SpanCard> {
|
|||
}
|
||||
if (selectedChoiceIndex == null) {
|
||||
DateTime? mostRecent;
|
||||
for (int i = 0; i < widget.scm.pangeaMatch!.match.choices!.length; i++) {
|
||||
final choice = widget.scm.pangeaMatch?.match.choices![i];
|
||||
final numChoices = widget.scm.pangeaMatch!.match.choices!.length;
|
||||
for (int i = 0; i < numChoices; i++) {
|
||||
final choice = choiceByIndex(i);
|
||||
if (choice!.timestamp != null &&
|
||||
(mostRecent == null || choice.timestamp!.isAfter(mostRecent))) {
|
||||
mostRecent = choice.timestamp;
|
||||
|
|
@ -114,6 +117,58 @@ class SpanCardState extends State<SpanCard> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> onChoiceSelect(int index) async {
|
||||
selectedChoiceIndex = index;
|
||||
if (selectedChoice != null) {
|
||||
selectedChoice!.timestamp = DateTime.now();
|
||||
selectedChoice!.selected = true;
|
||||
setState(
|
||||
() => (selectedChoice!.isBestCorrection
|
||||
? BotExpression.gold
|
||||
: BotExpression.surprised),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onReplaceSelected() {
|
||||
if (selectedChoice != null) {
|
||||
final tokens = widget.scm.choreographer.igc.igcTextData
|
||||
?.matchTokens(widget.scm.matchIndex) ??
|
||||
[];
|
||||
MatrixState.pangeaController.myAnalytics.onReplacementSelected(
|
||||
tokens,
|
||||
widget.roomId,
|
||||
selectedChoice!.isBestCorrection,
|
||||
);
|
||||
}
|
||||
|
||||
widget.scm
|
||||
.onReplacementSelect(
|
||||
matchIndex: widget.scm.matchIndex,
|
||||
choiceIndex: selectedChoiceIndex!,
|
||||
)
|
||||
.then((value) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
void onIgnoreMatch() {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() {
|
||||
widget.scm.onIgnore();
|
||||
final tokens = widget.scm.choreographer.igc.igcTextData
|
||||
?.matchTokens(widget.scm.matchIndex) ??
|
||||
[];
|
||||
MatrixState.pangeaController.myAnalytics.onIgnoreMatch(
|
||||
tokens,
|
||||
widget.roomId,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WordMatchContent(controller: this);
|
||||
|
|
@ -129,61 +184,6 @@ class WordMatchContent extends StatelessWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
Future<void> onChoiceSelect(int index) async {
|
||||
controller.selectedChoiceIndex = index;
|
||||
controller
|
||||
.widget
|
||||
.scm
|
||||
.choreographer
|
||||
.igc
|
||||
.igcTextData
|
||||
?.matches[controller.widget.scm.matchIndex]
|
||||
.match
|
||||
.choices?[index]
|
||||
.timestamp = DateTime.now();
|
||||
controller
|
||||
.widget
|
||||
.scm
|
||||
.choreographer
|
||||
.igc
|
||||
.igcTextData
|
||||
?.matches[controller.widget.scm.matchIndex]
|
||||
.match
|
||||
.choices?[index]
|
||||
.selected = true;
|
||||
|
||||
controller.setState(
|
||||
() => (controller.currentExpression = controller
|
||||
.widget
|
||||
.scm
|
||||
.choreographer
|
||||
.igc
|
||||
.igcTextData!
|
||||
.matches[controller.widget.scm.matchIndex]
|
||||
.match
|
||||
.choices![index]
|
||||
.isBestCorrection
|
||||
? BotExpression.gold
|
||||
: BotExpression.surprised),
|
||||
);
|
||||
// if (controller.widget.scm.pangeaMatch.match.choices![index].type ==
|
||||
// SpanChoiceType.distractor) {
|
||||
// await controller.getSpanDetails();
|
||||
// }
|
||||
// controller.setState(() {});
|
||||
}
|
||||
|
||||
void onReplaceSelected() {
|
||||
controller.widget.scm
|
||||
.onReplacementSelect(
|
||||
matchIndex: controller.widget.scm.matchIndex,
|
||||
choiceIndex: controller.selectedChoiceIndex!,
|
||||
)
|
||||
.then((value) {
|
||||
controller.setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.widget.scm.pangeaMatch == null) {
|
||||
|
|
@ -248,7 +248,7 @@ class WordMatchContent extends StatelessWidget {
|
|||
),
|
||||
)
|
||||
.toList(),
|
||||
onPressed: onChoiceSelect,
|
||||
onPressed: controller.onChoiceSelect,
|
||||
uniqueKeyForLayerLink: (int index) => "wordMatch$index",
|
||||
selectedChoiceIndex: controller.selectedChoiceIndex,
|
||||
),
|
||||
|
|
@ -272,13 +272,7 @@ class WordMatchContent extends StatelessWidget {
|
|||
AppConfig.primaryColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => controller.widget.scm.onIgnore(),
|
||||
);
|
||||
},
|
||||
onPressed: controller.onIgnoreMatch,
|
||||
child: Center(
|
||||
child: Text(L10n.of(context)!.ignoreInThisText),
|
||||
),
|
||||
|
|
@ -292,7 +286,7 @@ class WordMatchContent extends StatelessWidget {
|
|||
opacity: controller.selectedChoiceIndex != null ? 1.0 : 0.5,
|
||||
child: TextButton(
|
||||
onPressed: controller.selectedChoiceIndex != null
|
||||
? onReplaceSelected
|
||||
? controller.onReplaceSelected
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
|
|
@ -352,7 +346,6 @@ class WordMatchContent extends StatelessWidget {
|
|||
} on Exception catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: StackTrace.current);
|
||||
print(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,184 +0,0 @@
|
|||
//Possible actions/effects from cards
|
||||
// Nothing
|
||||
// useType of viewed definitions
|
||||
// SpanChoice of text in message from options
|
||||
// Call to server for additional/followup info
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../../enum/span_data_type.dart';
|
||||
|
||||
class SpanData {
|
||||
String? message;
|
||||
String? shortMessage;
|
||||
List<SpanChoice>? choices;
|
||||
List<Replacement>? replacements;
|
||||
int offset;
|
||||
int length;
|
||||
String fullText;
|
||||
Context? context;
|
||||
SpanDataTypeEnum type;
|
||||
Rule? rule;
|
||||
|
||||
SpanData({
|
||||
this.message,
|
||||
this.shortMessage,
|
||||
this.choices,
|
||||
this.replacements,
|
||||
required this.offset,
|
||||
required this.length,
|
||||
required this.fullText,
|
||||
this.context,
|
||||
required this.type,
|
||||
this.rule,
|
||||
});
|
||||
|
||||
factory SpanData.fromJson(Map<String, dynamic> json) {
|
||||
return SpanData(
|
||||
message: json['message'],
|
||||
shortMessage: json['shortMessage'],
|
||||
choices: json['choices'] != null
|
||||
? List<SpanChoice>.from(
|
||||
json['choices'].map((x) => SpanChoice.fromJson(x)),
|
||||
)
|
||||
: null,
|
||||
replacements: json['replacements'] != null
|
||||
? List<Replacement>.from(
|
||||
json['replacements'].map((x) => Replacement.fromJson(x)),
|
||||
)
|
||||
: null,
|
||||
offset: json['offset'],
|
||||
length: json['length'],
|
||||
fullText: json['full_text'],
|
||||
context:
|
||||
json['context'] != null ? Context.fromJson(json['context']) : null,
|
||||
type: SpanDataTypeEnum.values.firstWhereOrNull(
|
||||
(e) => e.toString() == 'SpanDataTypeEnum.${json['type']}',
|
||||
) ??
|
||||
SpanDataTypeEnum.correction,
|
||||
rule: json['rule'] != null ? Rule.fromJson(json['rule']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['message'] = message;
|
||||
data['shortMessage'] = shortMessage;
|
||||
if (choices != null) {
|
||||
data['choices'] = choices!.map((x) => x.toJson()).toList();
|
||||
}
|
||||
if (replacements != null) {
|
||||
data['replacements'] = replacements!.map((x) => x.toJson()).toList();
|
||||
}
|
||||
data['offset'] = offset;
|
||||
data['length'] = length;
|
||||
data['full_text'] = fullText;
|
||||
if (context != null) {
|
||||
data['context'] = context!.toJson();
|
||||
}
|
||||
data['type'] = type.toString().split('.').last;
|
||||
if (rule != null) {
|
||||
data['rule'] = rule!.toJson();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Context {
|
||||
String sentence;
|
||||
int offset;
|
||||
int length;
|
||||
|
||||
Context({required this.sentence, required this.offset, required this.length});
|
||||
|
||||
factory Context.fromJson(Map<String, dynamic> json) {
|
||||
return Context(
|
||||
sentence: json['sentence'],
|
||||
offset: json['offset'],
|
||||
length: json['length'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['sentence'] = sentence;
|
||||
data['offset'] = offset;
|
||||
data['length'] = length;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class SpanChoice {
|
||||
String value;
|
||||
bool selected;
|
||||
|
||||
SpanChoice({required this.value, required this.selected});
|
||||
|
||||
factory SpanChoice.fromJson(Map<String, dynamic> json) {
|
||||
return SpanChoice(
|
||||
value: json['value'],
|
||||
selected: json['selected'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['value'] = value;
|
||||
data['selected'] = selected;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Replacement {
|
||||
String value;
|
||||
|
||||
Replacement({required this.value});
|
||||
|
||||
factory Replacement.fromJson(Map<String, dynamic> json) {
|
||||
return Replacement(
|
||||
value: json['value'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['value'] = value;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Rule {
|
||||
String id;
|
||||
|
||||
Rule({required this.id});
|
||||
|
||||
factory Rule.fromJson(Map<String, dynamic> json) {
|
||||
return Rule(
|
||||
id: json['id'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class SpanDataType {
|
||||
String type;
|
||||
|
||||
SpanDataType({required this.type});
|
||||
|
||||
factory SpanDataType.fromJson(Map<String, dynamic> json) {
|
||||
return SpanDataType(
|
||||
type: json['type'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['type'] = type;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -91,20 +91,28 @@ Future<void> pLanguageDialog(
|
|||
context: context,
|
||||
future: () async {
|
||||
try {
|
||||
pangeaController.userController.updateProfile(
|
||||
(profile) {
|
||||
profile.userSettings.sourceLanguage =
|
||||
selectedSourceLanguage.langCode;
|
||||
profile.userSettings.targetLanguage =
|
||||
selectedTargetLanguage.langCode;
|
||||
return profile;
|
||||
},
|
||||
waitForDataInSync: true,
|
||||
).then((_) {
|
||||
pangeaController.myAnalytics
|
||||
.updateAnalytics()
|
||||
.then((_) {
|
||||
pangeaController.userController.updateProfile(
|
||||
(profile) {
|
||||
profile.userSettings.sourceLanguage =
|
||||
selectedSourceLanguage.langCode;
|
||||
profile.userSettings.targetLanguage =
|
||||
selectedTargetLanguage.langCode;
|
||||
return profile;
|
||||
},
|
||||
waitForDataInSync: true,
|
||||
);
|
||||
}).then((_) {
|
||||
// if the profile update is successful, reset cached analytics
|
||||
// data, since analytics data corresponds to the user's L2
|
||||
pangeaController.myAnalytics.clearCache();
|
||||
pangeaController.analytics.clearCache();
|
||||
pangeaController.myAnalytics.dispose();
|
||||
pangeaController.analytics.dispose();
|
||||
|
||||
pangeaController.myAnalytics.initialize();
|
||||
pangeaController.analytics.initialize();
|
||||
|
||||
Navigator.pop(context);
|
||||
});
|
||||
} catch (err, s) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue