Merge branch 'main' into fluffychat-merge-2
This commit is contained in:
commit
dbeae58a6b
22 changed files with 571 additions and 202 deletions
|
|
@ -35,8 +35,6 @@ abstract class SettingKeys {
|
|||
static const String displayNavigationRail =
|
||||
'chat.fluffy.display_navigation_rail';
|
||||
// #Pangea
|
||||
static const String useActivityImageAsChatBackground =
|
||||
'pangea.use_activity_image_as_chat_background';
|
||||
static const String volume = 'pangea.volume';
|
||||
static const String showedActivityMenu =
|
||||
'pangea.showed_activity_menu_tutorial';
|
||||
|
|
|
|||
|
|
@ -76,10 +76,25 @@ class ChatSearchController extends State<ChatSearchPage>
|
|||
(result) => (
|
||||
<String, Event>{
|
||||
for (final event in result.$1) event.eventId: event,
|
||||
}.values.toList(),
|
||||
// #Pangea
|
||||
// }.values.toList(),
|
||||
}
|
||||
.values
|
||||
.toList()
|
||||
.where(
|
||||
(e) => !e.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
// Pangea#
|
||||
result.$2,
|
||||
),
|
||||
)
|
||||
// #Pangea
|
||||
.where((result) => result.$1.isNotEmpty)
|
||||
// Pangea#
|
||||
.asBroadcastStream();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ class Settings3PidController extends State<Settings3Pid> {
|
|||
auth: auth,
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
showError: (e) => !e.toString().contains("Request has been canceled"),
|
||||
// Pangea#
|
||||
);
|
||||
if (success.error != null) return;
|
||||
setState(() => request = null);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/user/style_settings_repo.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'settings_chat_view.dart';
|
||||
|
||||
class SettingsChat extends StatefulWidget {
|
||||
|
|
@ -10,6 +13,15 @@ class SettingsChat extends StatefulWidget {
|
|||
}
|
||||
|
||||
class SettingsChatController extends State<SettingsChat> {
|
||||
// #Pangea
|
||||
Future<void> setUseActivityImageBackground(bool value) async {
|
||||
final userId = Matrix.of(context).client.userID!;
|
||||
AppConfig.useActivityImageAsChatBackground = value;
|
||||
setState(() {});
|
||||
await StyleSettingsRepo.setUseActivityImageBackground(userId, value);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SettingsChatView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,12 +73,10 @@ class SettingsChatView extends StatelessWidget {
|
|||
),
|
||||
|
||||
// #Pangea
|
||||
SettingsSwitchListTile.adaptive(
|
||||
title: L10n.of(context).useActivityImageAsChatBackground,
|
||||
onChanged: (b) =>
|
||||
AppConfig.useActivityImageAsChatBackground = b,
|
||||
storeKey: SettingKeys.useActivityImageAsChatBackground,
|
||||
defaultValue: AppConfig.useActivityImageAsChatBackground,
|
||||
SwitchListTile.adaptive(
|
||||
value: AppConfig.useActivityImageAsChatBackground,
|
||||
title: Text(L10n.of(context).useActivityImageAsChatBackground),
|
||||
onChanged: controller.setUseActivityImageBackground,
|
||||
),
|
||||
// Divider(color: theme.dividerColor),
|
||||
// ListTile(
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ class AnalyticsDataService {
|
|||
String? roomId,
|
||||
DateTime? since,
|
||||
ConstructUseTypeEnum? type,
|
||||
bool filterCapped = true,
|
||||
}) async {
|
||||
await _ensureInitialized();
|
||||
final uses = await _analyticsClientGetter.database.getUses(
|
||||
|
|
@ -264,7 +265,8 @@ class AnalyticsDataService {
|
|||
cappedLastUseCache[use.identifier] = constructs.cappedLastUse;
|
||||
}
|
||||
final cappedLastUse = cappedLastUseCache[use.identifier];
|
||||
if (cappedLastUse != null && use.timeStamp.isAfter(cappedLastUse)) {
|
||||
if (filterCapped &&
|
||||
(cappedLastUse != null && use.timeStamp.isAfter(cappedLastUse))) {
|
||||
continue;
|
||||
}
|
||||
filtered.add(use);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
|||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_generation_repo.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
|
@ -101,6 +100,8 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
final ValueNotifier<SelectedMorphChoice?> selectedMorphChoice =
|
||||
ValueNotifier<SelectedMorphChoice?>(null);
|
||||
|
||||
final ValueNotifier<bool> hintPressedNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
final Map<String, Map<String, String>> _choiceTexts = {};
|
||||
final Map<String, Map<String, String?>> _choiceEmojis = {};
|
||||
|
||||
|
|
@ -125,6 +126,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
progressNotifier.dispose();
|
||||
enableChoicesNotifier.dispose();
|
||||
selectedMorphChoice.dispose();
|
||||
hintPressedNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -210,6 +212,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
activityState.value = const AsyncState.loading();
|
||||
activityTarget.value = null;
|
||||
selectedMorphChoice.value = null;
|
||||
hintPressedNotifier.value = false;
|
||||
enableChoicesNotifier.value = true;
|
||||
progressNotifier.value = 0.0;
|
||||
_queue.clear();
|
||||
|
|
@ -282,6 +285,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
try {
|
||||
activityState.value = const AsyncState.loading();
|
||||
selectedMorphChoice.value = null;
|
||||
hintPressedNotifier.value = false;
|
||||
|
||||
final req = activityTarget.value!;
|
||||
final res = await _fetchActivity(req);
|
||||
|
|
@ -324,6 +328,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
while (_queue.isNotEmpty) {
|
||||
activityState.value = const AsyncState.loading();
|
||||
selectedMorphChoice.value = null;
|
||||
hintPressedNotifier.value = false;
|
||||
final nextActivityCompleter = _queue.removeFirst();
|
||||
|
||||
try {
|
||||
|
|
@ -477,6 +482,10 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
await _analyticsService.updateService.addAnalytics(null, [use]);
|
||||
}
|
||||
|
||||
void onHintPressed() {
|
||||
hintPressedNotifier.value = !hintPressedNotifier.value;
|
||||
}
|
||||
|
||||
Future<void> onSelectChoice(
|
||||
String choiceContent,
|
||||
) async {
|
||||
|
|
@ -528,21 +537,19 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
}
|
||||
|
||||
Future<List<InlineSpan>?> getExampleMessage(
|
||||
PracticeTarget target,
|
||||
MessageActivityRequest activityRequest,
|
||||
) async {
|
||||
final target = activityRequest.target;
|
||||
final token = target.tokens.first;
|
||||
final construct = target.targetTokenConstructID(token);
|
||||
|
||||
String? form;
|
||||
if (widget.type == ConstructTypeEnum.morph) {
|
||||
if (target.morphFeature == null) return null;
|
||||
form = token.lemma.form;
|
||||
return activityRequest.morphExampleInfo?.exampleMessage;
|
||||
}
|
||||
|
||||
return ExampleMessageUtil.getExampleMessage(
|
||||
await _analyticsService.getConstructUse(construct),
|
||||
Matrix.of(context).client,
|
||||
form: form,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,70 @@
|
|||
import 'package:flutter/painting.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
|
||||
class MorphExampleInfo {
|
||||
final List<InlineSpan> exampleMessage;
|
||||
|
||||
const MorphExampleInfo({
|
||||
required this.exampleMessage,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final segments = <Map<String, dynamic>>[];
|
||||
|
||||
for (final span in exampleMessage) {
|
||||
if (span is TextSpan) {
|
||||
segments.add({
|
||||
'text': span.text ?? '',
|
||||
'isBold': span.style?.fontWeight == FontWeight.bold,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'segments': segments,
|
||||
};
|
||||
}
|
||||
|
||||
factory MorphExampleInfo.fromJson(Map<String, dynamic> json) {
|
||||
final segments = json['segments'] as List<dynamic>? ?? [];
|
||||
|
||||
final spans = <InlineSpan>[];
|
||||
for (final segment in segments) {
|
||||
final text = segment['text'] as String? ?? '';
|
||||
final isBold = segment['isBold'] as bool? ?? false;
|
||||
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: isBold ? const TextStyle(fontWeight: FontWeight.bold) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MorphExampleInfo(exampleMessage: spans);
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsActivityTarget {
|
||||
final PracticeTarget target;
|
||||
final GrammarErrorRequestInfo? grammarErrorInfo;
|
||||
final MorphExampleInfo? morphExampleInfo;
|
||||
|
||||
AnalyticsActivityTarget({
|
||||
required this.target,
|
||||
this.grammarErrorInfo,
|
||||
this.morphExampleInfo,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'target': target.toJson(),
|
||||
'grammarErrorInfo': grammarErrorInfo?.toJson(),
|
||||
'morphExampleInfo': morphExampleInfo?.toJson(),
|
||||
};
|
||||
|
||||
factory AnalyticsActivityTarget.fromJson(Map<String, dynamic> json) =>
|
||||
|
|
@ -24,6 +73,9 @@ class AnalyticsActivityTarget {
|
|||
grammarErrorInfo: json['grammarErrorInfo'] != null
|
||||
? GrammarErrorRequestInfo.fromJson(json['grammarErrorInfo'])
|
||||
: null,
|
||||
morphExampleInfo: json['morphExampleInfo'] != null
|
||||
? MorphExampleInfo.fromJson(json['morphExampleInfo'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -79,6 +131,7 @@ class AnalyticsPracticeSessionModel {
|
|||
activityQualityFeedback: null,
|
||||
target: target.target,
|
||||
grammarErrorInfo: target.grammarErrorInfo,
|
||||
morphExampleInfo: target.morphExampleInfo,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/example_message_util.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
|
|
@ -12,8 +13,11 @@ import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
|||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
|
|
@ -63,15 +67,18 @@ class AnalyticsPracticeSessionRepo {
|
|||
final remainingCount = (AnalyticsPracticeConstants.practiceGroupSize +
|
||||
AnalyticsPracticeConstants.errorBufferSize) -
|
||||
targets.length;
|
||||
final morphEntries = morphs.entries.take(remainingCount);
|
||||
final morphEntries = morphs.take(remainingCount);
|
||||
|
||||
for (final entry in morphEntries) {
|
||||
targets.add(
|
||||
AnalyticsActivityTarget(
|
||||
target: PracticeTarget(
|
||||
tokens: [entry.key],
|
||||
tokens: [entry.token],
|
||||
activityType: ActivityTypeEnum.grammarCategory,
|
||||
morphFeature: entry.value,
|
||||
morphFeature: entry.feature,
|
||||
),
|
||||
morphExampleInfo: MorphExampleInfo(
|
||||
exampleMessage: entry.exampleMessage,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -125,12 +132,37 @@ class AnalyticsPracticeSessionRepo {
|
|||
return targets;
|
||||
}
|
||||
|
||||
static Future<Map<PangeaToken, MorphFeaturesEnum>> _fetchMorphs() async {
|
||||
static Future<List<MorphPracticeTarget>> _fetchMorphs() async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.morph)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
final morphInfoRequest = MorphInfoRequest(
|
||||
userL1: MatrixState.pangeaController.userController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
userL2: MatrixState.pangeaController.userController.userL2?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
final morphInfoResult = await MorphInfoRepo.get(
|
||||
MatrixState.pangeaController.userController.accessToken,
|
||||
morphInfoRequest,
|
||||
);
|
||||
|
||||
// Build list of features with multiple tags (valid for practice)
|
||||
final List<String> validFeatures = [];
|
||||
if (!morphInfoResult.isError) {
|
||||
final response = morphInfoResult.asValue?.value;
|
||||
if (response != null) {
|
||||
for (final feature in response.features) {
|
||||
if (feature.tags.length > 1) {
|
||||
validFeatures.add(feature.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
constructs.sort((a, b) {
|
||||
final dateA = a.lastUsed;
|
||||
|
|
@ -141,7 +173,7 @@ class AnalyticsPracticeSessionRepo {
|
|||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
final targets = <PangeaToken, MorphFeaturesEnum>{};
|
||||
final targets = <MorphPracticeTarget>[];
|
||||
final Set<String> seenForms = {};
|
||||
|
||||
for (final entry in constructs) {
|
||||
|
|
@ -152,10 +184,14 @@ class AnalyticsPracticeSessionRepo {
|
|||
}
|
||||
|
||||
final feature = MorphFeaturesEnumExtension.fromString(entry.id.category);
|
||||
if (feature == MorphFeaturesEnum.Unknown) {
|
||||
|
||||
// Only include features that are in the valid list (have multiple tags)
|
||||
if (feature == MorphFeaturesEnum.Unknown ||
|
||||
(validFeatures.isNotEmpty && !validFeatures.contains(feature.name))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<InlineSpan>? exampleMessage;
|
||||
for (final use in entry.cappedUses) {
|
||||
if (targets.length >=
|
||||
(AnalyticsPracticeConstants.practiceGroupSize +
|
||||
|
|
@ -169,6 +205,17 @@ class AnalyticsPracticeSessionRepo {
|
|||
continue;
|
||||
}
|
||||
|
||||
exampleMessage = await ExampleMessageUtil.getExampleMessage(
|
||||
await MatrixState.pangeaController.matrixState.analyticsDataService
|
||||
.getConstructUse(entry.id),
|
||||
MatrixState.pangeaController.matrixState.client,
|
||||
form: form,
|
||||
);
|
||||
|
||||
if (exampleMessage == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenForms.add(form);
|
||||
final token = PangeaToken(
|
||||
lemma: Lemma(
|
||||
|
|
@ -180,7 +227,13 @@ class AnalyticsPracticeSessionRepo {
|
|||
pos: 'other',
|
||||
morph: {feature: use.lemma},
|
||||
);
|
||||
targets[token] = feature;
|
||||
targets.add(
|
||||
MorphPracticeTarget(
|
||||
feature: feature,
|
||||
token: token,
|
||||
exampleMessage: exampleMessage,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -189,14 +242,32 @@ class AnalyticsPracticeSessionRepo {
|
|||
}
|
||||
|
||||
static Future<List<AnalyticsActivityTarget>> _fetchErrors() async {
|
||||
final uses = await MatrixState
|
||||
// Fetch all recent uses in one call (not filtering blocked constructs)
|
||||
final allRecentUses = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getUses(count: 100, type: ConstructUseTypeEnum.ga);
|
||||
.getUses(count: 200, filterCapped: false);
|
||||
|
||||
// Filter for grammar error uses
|
||||
final grammarErrorUses = allRecentUses
|
||||
.where((use) => use.useType == ConstructUseTypeEnum.ga)
|
||||
.toList();
|
||||
|
||||
// Create list of recently used constructs
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(hours: 24));
|
||||
final recentlyPracticedConstructs = allRecentUses
|
||||
.where(
|
||||
(use) =>
|
||||
use.metadata.timeStamp.isAfter(cutoffTime) &&
|
||||
(use.useType == ConstructUseTypeEnum.corGE ||
|
||||
use.useType == ConstructUseTypeEnum.incGE),
|
||||
)
|
||||
.map((use) => use.identifier)
|
||||
.toSet();
|
||||
|
||||
final client = MatrixState.pangeaController.matrixState.client;
|
||||
final Map<String, PangeaMessageEvent?> idsToEvents = {};
|
||||
|
||||
for (final use in uses) {
|
||||
for (final use in grammarErrorUses) {
|
||||
final eventID = use.metadata.eventId;
|
||||
if (eventID == null || idsToEvents.containsKey(eventID)) continue;
|
||||
|
||||
|
|
@ -257,7 +328,11 @@ class AnalyticsPracticeSessionRepo {
|
|||
|
||||
if (igcMatch!.match.offset == 0 &&
|
||||
igcMatch.match.length >= stepText.trim().characters.length) {
|
||||
// Skip if the grammar error spans the entire step
|
||||
continue;
|
||||
}
|
||||
|
||||
if (igcMatch.match.isNormalizationError()) {
|
||||
// Skip normalization errors
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -272,8 +347,22 @@ class AnalyticsPracticeSessionRepo {
|
|||
)
|
||||
.toList();
|
||||
|
||||
// Skip if no valid tokens found for this grammar error
|
||||
if (choiceTokens.isEmpty) continue;
|
||||
// Skip if no valid tokens found for this grammar error, or only one answer
|
||||
if (choiceTokens.length <= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final firstToken = choiceTokens.first;
|
||||
final tokenIdentifier = ConstructIdentifier(
|
||||
lemma: firstToken.lemma.text,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
category: firstToken.pos,
|
||||
);
|
||||
|
||||
final hasRecentPractice =
|
||||
recentlyPracticedConstructs.contains(tokenIdentifier);
|
||||
|
||||
if (hasRecentPractice) continue;
|
||||
|
||||
String? translation;
|
||||
try {
|
||||
|
|
@ -291,6 +380,7 @@ class AnalyticsPracticeSessionRepo {
|
|||
}
|
||||
|
||||
if (translation == null) continue;
|
||||
|
||||
targets.add(
|
||||
AnalyticsActivityTarget(
|
||||
target: PracticeTarget(
|
||||
|
|
@ -312,3 +402,15 @@ class AnalyticsPracticeSessionRepo {
|
|||
return targets;
|
||||
}
|
||||
}
|
||||
|
||||
class MorphPracticeTarget {
|
||||
final PangeaToken token;
|
||||
final MorphFeaturesEnum feature;
|
||||
final List<InlineSpan> exampleMessage;
|
||||
|
||||
MorphPracticeTarget({
|
||||
required this.token,
|
||||
required this.feature,
|
||||
required this.exampleMessage,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import 'package:fluffychat/pangea/analytics_practice/practice_timer_widget.dart'
|
|||
import 'package:fluffychat/pangea/analytics_summary/animated_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/async_state.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
|
||||
|
|
@ -161,28 +162,7 @@ class _AnalyticsActivityView extends StatelessWidget {
|
|||
const SizedBox(height: 16.0),
|
||||
_ActivityChoicesWidget(controller),
|
||||
const SizedBox(height: 16.0),
|
||||
ListenableBuilder(
|
||||
listenable: Listenable.merge([
|
||||
controller.activityState,
|
||||
controller.selectedMorphChoice,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
final activityState = controller.activityState.value;
|
||||
final selectedChoice = controller.selectedMorphChoice.value;
|
||||
|
||||
if (activityState
|
||||
is! AsyncLoaded<MultipleChoicePracticeActivityModel> ||
|
||||
selectedChoice == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return MorphMeaningWidget(
|
||||
feature: selectedChoice.feature,
|
||||
tag: selectedChoice.tag,
|
||||
blankErrorFeedback: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
_WrongAnswerFeedback(controller: controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -214,15 +194,12 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ErrorBlankWidget(
|
||||
activity: activity,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_GrammarErrorTranslationButton(
|
||||
key: ValueKey(
|
||||
'${activity.eventID}_${activity.errorOffset}_${activity.errorLength}',
|
||||
),
|
||||
translation: activity.translation,
|
||||
activity: activity,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
_ => const SizedBox(),
|
||||
|
|
@ -230,11 +207,31 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
ActivityTypeEnum.grammarCategory => Center(
|
||||
child: Column(
|
||||
children: [
|
||||
_CorrectAnswerHint(controller: controller),
|
||||
_ExampleMessageWidget(
|
||||
controller.getExampleMessage(target!),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: controller.hintPressedNotifier,
|
||||
builder: (context, hintPressed, __) {
|
||||
return HintButton(
|
||||
depressed: hintPressed,
|
||||
onPressed: controller.onHintPressed,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ => SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: _ExampleMessageWidget(
|
||||
controller.getExampleMessage(target!.target),
|
||||
controller.getExampleMessage(target!),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -284,18 +281,123 @@ class _ExampleMessageWidget extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _ErrorBlankWidget extends StatelessWidget {
|
||||
final GrammarErrorPracticeActivityModel activity;
|
||||
class _CorrectAnswerHint extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const _ErrorBlankWidget({
|
||||
required this.activity,
|
||||
const _CorrectAnswerHint({
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = activity.text;
|
||||
final errorOffset = activity.errorOffset;
|
||||
final errorLength = activity.errorLength;
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller.hintPressedNotifier,
|
||||
builder: (context, hintPressed, __) {
|
||||
if (!hintPressed) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller.activityState,
|
||||
builder: (context, state, __) {
|
||||
if (state is! AsyncLoaded<MultipleChoicePracticeActivityModel>) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final activity = state.value;
|
||||
if (activity is! MorphPracticeActivityModel) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final correctAnswerTag =
|
||||
activity.multipleChoiceContent.answers.first;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: MorphMeaningWidget(
|
||||
feature: activity.morphFeature,
|
||||
tag: correctAnswerTag,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WrongAnswerFeedback extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const _WrongAnswerFeedback({
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: Listenable.merge([
|
||||
controller.activityState,
|
||||
controller.selectedMorphChoice,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
final activityState = controller.activityState.value;
|
||||
final selectedChoice = controller.selectedMorphChoice.value;
|
||||
|
||||
if (activityState
|
||||
is! AsyncLoaded<MultipleChoicePracticeActivityModel> ||
|
||||
selectedChoice == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final activity = activityState.value;
|
||||
final isWrongAnswer =
|
||||
!activity.multipleChoiceContent.isCorrect(selectedChoice.tag);
|
||||
|
||||
if (!isWrongAnswer) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: MorphMeaningWidget(
|
||||
feature: selectedChoice.feature,
|
||||
tag: selectedChoice.tag,
|
||||
blankErrorFeedback: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorBlankWidget extends StatefulWidget {
|
||||
final GrammarErrorPracticeActivityModel activity;
|
||||
|
||||
const _ErrorBlankWidget({
|
||||
super.key,
|
||||
required this.activity,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ErrorBlankWidget> createState() => _ErrorBlankWidgetState();
|
||||
}
|
||||
|
||||
class _ErrorBlankWidgetState extends State<_ErrorBlankWidget> {
|
||||
late final String translation = widget.activity.translation;
|
||||
bool _showTranslation = false;
|
||||
|
||||
void _toggleTranslation() {
|
||||
setState(() {
|
||||
_showTranslation = !_showTranslation;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = widget.activity.text;
|
||||
final errorOffset = widget.activity.errorOffset;
|
||||
final errorLength = widget.activity.errorLength;
|
||||
|
||||
const maxContextChars = 50;
|
||||
|
||||
|
|
@ -342,122 +444,107 @@ class _ErrorBlankWidget extends StatelessWidget {
|
|||
|
||||
final after = chars.skip(errorEnd).take(afterEnd - errorEnd).toString();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.alphaBlend(
|
||||
Colors.white.withAlpha(180),
|
||||
ThemeData.dark().colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryFixed,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
children: [
|
||||
if (trimmedBefore) const TextSpan(text: '…'),
|
||||
if (before.isNotEmpty) TextSpan(text: before),
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
height: 4.0,
|
||||
width: (errorLength * 8).toDouble(),
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: BoxDecoration(
|
||||
color: Color.alphaBlend(
|
||||
Colors.white.withAlpha(180),
|
||||
ThemeData.dark().colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryFixed,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
children: [
|
||||
if (trimmedBefore) const TextSpan(text: '…'),
|
||||
if (before.isNotEmpty) TextSpan(text: before),
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
height: 4.0,
|
||||
width: (errorLength * 8).toDouble(),
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (after.isNotEmpty) TextSpan(text: after),
|
||||
if (trimmedAfter) const TextSpan(text: '…'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (after.isNotEmpty) TextSpan(text: after),
|
||||
if (trimmedAfter) const TextSpan(text: '…'),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
_showTranslation
|
||||
? Text(
|
||||
translation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryFixed,
|
||||
fontSize: AppConfig.fontSizeFactor *
|
||||
AppConfig.messageFontSize,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
HintButton(depressed: _showTranslation, onPressed: _toggleTranslation),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GrammarErrorTranslationButton extends StatefulWidget {
|
||||
final String translation;
|
||||
class HintButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
final bool depressed;
|
||||
|
||||
const _GrammarErrorTranslationButton({
|
||||
const HintButton({
|
||||
required this.onPressed,
|
||||
required this.depressed,
|
||||
super.key,
|
||||
required this.translation,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_GrammarErrorTranslationButton> createState() =>
|
||||
_GrammarErrorTranslationButtonState();
|
||||
}
|
||||
|
||||
class _GrammarErrorTranslationButtonState
|
||||
extends State<_GrammarErrorTranslationButton> {
|
||||
bool _showTranslation = false;
|
||||
|
||||
void _toggleTranslation() {
|
||||
if (_showTranslation) {
|
||||
setState(() {
|
||||
_showTranslation = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_showTranslation = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: _toggleTranslation,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
if (_showTranslation)
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.alphaBlend(
|
||||
Colors.white.withAlpha(180),
|
||||
ThemeData.dark().colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
widget.translation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryFixed,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ElevatedButton(
|
||||
onPressed: _toggleTranslation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
padding: const EdgeInsets.all(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return PressableButton(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
onPressed: onPressed,
|
||||
depressed: depressed,
|
||||
playSound: true,
|
||||
colorFactor: 0.3,
|
||||
builder: (context, depressed, shadowColor) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color: depressed
|
||||
? shadowColor
|
||||
: Theme.of(context).colorScheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class GrammarChoiceCard extends StatelessWidget {
|
|||
final baseTextSize =
|
||||
(Theme.of(context).textTheme.titleMedium?.fontSize ?? 16) *
|
||||
(height / 72.0).clamp(1.0, 1.4);
|
||||
final emojiSize = baseTextSize * 1.2;
|
||||
final emojiSize = baseTextSize * 1.5;
|
||||
final copy = getGrammarCopy(
|
||||
category: feature.name,
|
||||
lemma: tag,
|
||||
|
|
@ -54,7 +54,7 @@ class GrammarChoiceCard extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: height * .7,
|
||||
width: height,
|
||||
height: height,
|
||||
child: Center(
|
||||
child: MorphIcon(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_models.dart';
|
||||
|
|
@ -58,6 +59,8 @@ class MorphCategoryActivityGenerator {
|
|||
choices: choices.toSet(),
|
||||
answers: {morphTag},
|
||||
),
|
||||
morphExampleInfo:
|
||||
req.morphExampleInfo ?? const MorphExampleInfo(exampleMessage: []),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class BotOptionsModel {
|
|||
final String? textAdventureGameMasterInstructions;
|
||||
final String? targetLanguage;
|
||||
final String? targetVoice;
|
||||
final GenderEnum targetGender;
|
||||
final Map<String, GenderEnum> userGenders;
|
||||
|
||||
const BotOptionsModel({
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -37,7 +37,7 @@ class BotOptionsModel {
|
|||
this.mode = BotMode.discussion,
|
||||
this.targetLanguage,
|
||||
this.targetVoice,
|
||||
this.targetGender = GenderEnum.unselected,
|
||||
this.userGenders = const {},
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
|
|
@ -61,6 +61,22 @@ class BotOptionsModel {
|
|||
});
|
||||
|
||||
factory BotOptionsModel.fromJson(json) {
|
||||
final genderEntry = json[ModelKey.targetGender];
|
||||
Map<String, GenderEnum> targetGenders = {};
|
||||
if (genderEntry is Map<String, dynamic>) {
|
||||
targetGenders = Map<String, GenderEnum>.fromEntries(
|
||||
genderEntry.entries.map(
|
||||
(e) => MapEntry(
|
||||
e.key,
|
||||
GenderEnum.values.firstWhere(
|
||||
(g) => g.name == e.value,
|
||||
orElse: () => GenderEnum.unselected,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return BotOptionsModel(
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// General Bot Options
|
||||
|
|
@ -76,12 +92,7 @@ class BotOptionsModel {
|
|||
mode: json[ModelKey.mode] ?? BotMode.discussion,
|
||||
targetLanguage: json[ModelKey.targetLanguage],
|
||||
targetVoice: json[ModelKey.targetVoice],
|
||||
targetGender: json[ModelKey.targetGender] != null
|
||||
? GenderEnum.values.firstWhere(
|
||||
(g) => g.name == json[ModelKey.targetGender],
|
||||
orElse: () => GenderEnum.unselected,
|
||||
)
|
||||
: GenderEnum.unselected,
|
||||
userGenders: targetGenders,
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
|
|
@ -112,6 +123,11 @@ class BotOptionsModel {
|
|||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
try {
|
||||
final Map<String, String> gendersEntry = {};
|
||||
for (final entry in userGenders.entries) {
|
||||
gendersEntry[entry.key] = entry.value.name;
|
||||
}
|
||||
|
||||
// data[ModelKey.isConversationBotChat] = isConversationBotChat;
|
||||
data[ModelKey.languageLevel] = languageLevel.storageInt;
|
||||
data[ModelKey.safetyModeration] = safetyModeration;
|
||||
|
|
@ -130,7 +146,7 @@ class BotOptionsModel {
|
|||
data[ModelKey.customTriggerReactionKey] = customTriggerReactionKey ?? "⏩";
|
||||
data[ModelKey.textAdventureGameMasterInstructions] =
|
||||
textAdventureGameMasterInstructions;
|
||||
data[ModelKey.targetGender] = targetGender.name;
|
||||
data[ModelKey.targetGender] = gendersEntry;
|
||||
return data;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -159,7 +175,7 @@ class BotOptionsModel {
|
|||
String? textAdventureGameMasterInstructions,
|
||||
String? targetLanguage,
|
||||
String? targetVoice,
|
||||
GenderEnum? targetGender,
|
||||
Map<String, GenderEnum>? userGenders,
|
||||
}) {
|
||||
return BotOptionsModel(
|
||||
languageLevel: languageLevel ?? this.languageLevel,
|
||||
|
|
@ -183,7 +199,7 @@ class BotOptionsModel {
|
|||
this.textAdventureGameMasterInstructions,
|
||||
targetLanguage: targetLanguage ?? this.targetLanguage,
|
||||
targetVoice: targetVoice ?? this.targetVoice,
|
||||
targetGender: targetGender ?? this.targetGender,
|
||||
userGenders: userGenders ?? this.userGenders,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
|
|||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/gender_enum.dart';
|
||||
import 'package:fluffychat/pangea/user/user_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
|
@ -64,15 +65,22 @@ extension BotClientExtension on Client {
|
|||
if (botOptions.targetLanguage == targetLanguage &&
|
||||
botOptions.languageLevel == languageLevel &&
|
||||
botOptions.targetVoice == voice &&
|
||||
botOptions.targetGender == gender) {
|
||||
botOptions.userGenders[userID] == gender) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final updatedGenders =
|
||||
Map<String, GenderEnum>.from(botOptions.userGenders);
|
||||
|
||||
if (updatedGenders[userID] != gender) {
|
||||
updatedGenders[userID!] = gender;
|
||||
}
|
||||
|
||||
final updated = botOptions.copyWith(
|
||||
targetLanguage: targetLanguage,
|
||||
languageLevel: languageLevel,
|
||||
targetVoice: voice,
|
||||
targetGender: gender,
|
||||
userGenders: updatedGenders,
|
||||
);
|
||||
futures.add(targetBotRoom.setBotOptions(updated));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,8 +66,10 @@ class PangeaController {
|
|||
});
|
||||
subscriptionController.reinitialize();
|
||||
|
||||
StyleSettingsRepo.fontSizeFactor(userID!).then((factor) {
|
||||
AppConfig.fontSizeFactor = factor;
|
||||
StyleSettingsRepo.settings(userID!).then((settings) {
|
||||
AppConfig.fontSizeFactor = settings.fontSizeFactor;
|
||||
AppConfig.useActivityImageAsChatBackground =
|
||||
settings.useActivityImageBackground;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -312,6 +312,7 @@ class OverlayUtil {
|
|||
closePrevOverlay: false,
|
||||
backDropToDismiss: false,
|
||||
ignorePointer: true,
|
||||
canPop: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,7 +133,6 @@ class SettingsLearningController extends State<SettingsLearning> {
|
|||
waitForDataInSync: true,
|
||||
),
|
||||
onError: (e, s) {
|
||||
debugPrint("Error resetting instruction tooltips: $e");
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
|
|
@ -79,6 +80,7 @@ class MessageActivityRequest {
|
|||
final PracticeTarget target;
|
||||
final ActivityQualityFeedback? activityQualityFeedback;
|
||||
final GrammarErrorRequestInfo? grammarErrorInfo;
|
||||
final MorphExampleInfo? morphExampleInfo;
|
||||
|
||||
MessageActivityRequest({
|
||||
required this.userL1,
|
||||
|
|
@ -86,6 +88,7 @@ class MessageActivityRequest {
|
|||
required this.activityQualityFeedback,
|
||||
required this.target,
|
||||
this.grammarErrorInfo,
|
||||
this.morphExampleInfo,
|
||||
}) {
|
||||
if (target.tokens.isEmpty) {
|
||||
throw Exception('Target tokens must not be empty');
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
|
|
@ -111,6 +112,9 @@ sealed class PracticeActivityModel {
|
|||
tokens: tokens,
|
||||
morphFeature: morph!,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
morphExampleInfo: json['morph_example_info'] != null
|
||||
? MorphExampleInfo.fromJson(json['morph_example_info'])
|
||||
: const MorphExampleInfo(exampleMessage: []),
|
||||
);
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
assert(
|
||||
|
|
@ -302,11 +306,13 @@ sealed class MorphPracticeActivityModel
|
|||
}
|
||||
|
||||
class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel {
|
||||
final MorphExampleInfo morphExampleInfo;
|
||||
MorphCategoryPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.morphFeature,
|
||||
required super.multipleChoiceContent,
|
||||
required this.morphExampleInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -330,6 +336,13 @@ class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel {
|
|||
xp: useType.pointValue,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = super.toJson();
|
||||
json['morph_example_info'] = morphExampleInfo.toJson();
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
class MorphMatchPracticeActivityModel extends MorphPracticeActivityModel {
|
||||
|
|
|
|||
|
|
@ -83,10 +83,12 @@ class _NewWordOverlayState extends State<NewWordOverlay>
|
|||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
MatrixState.pAnyState.closeOverlay(widget.transformTargetId);
|
||||
MatrixState.pAnyState.closeOverlay(_overlayKey);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get _overlayKey => "new-word-overlay-${widget.transformTargetId}";
|
||||
|
||||
void _showFlyingWidget() {
|
||||
if (_controller == null || _opacityAnim == null || _moveAnim == null) {
|
||||
return;
|
||||
|
|
@ -96,9 +98,10 @@ class _NewWordOverlayState extends State<NewWordOverlay>
|
|||
context: context,
|
||||
closePrevOverlay: false,
|
||||
ignorePointer: true,
|
||||
canPop: false,
|
||||
offset: const Offset(0, 45),
|
||||
targetAnchor: Alignment.center,
|
||||
overlayKey: widget.transformTargetId,
|
||||
overlayKey: _overlayKey,
|
||||
transformTargetId: widget.transformTargetId,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller!,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,39 @@
|
|||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
class _StyleSettings {
|
||||
final double fontSizeFactor;
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
||||
const _StyleSettings({
|
||||
class StyleSettings {
|
||||
final double fontSizeFactor;
|
||||
final bool useActivityImageBackground;
|
||||
|
||||
const StyleSettings({
|
||||
this.fontSizeFactor = 1.0,
|
||||
this.useActivityImageBackground = true,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'fontSizeFactor': fontSizeFactor,
|
||||
'useActivityImageBackground': useActivityImageBackground,
|
||||
};
|
||||
}
|
||||
|
||||
factory _StyleSettings.fromJson(Map<String, dynamic> json) {
|
||||
return _StyleSettings(
|
||||
factory StyleSettings.fromJson(Map<String, dynamic> json) {
|
||||
return StyleSettings(
|
||||
fontSizeFactor: (json['fontSizeFactor'] as num?)?.toDouble() ?? 1.0,
|
||||
useActivityImageBackground:
|
||||
json['useActivityImageBackground'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
StyleSettings copyWith({
|
||||
double? fontSizeFactor,
|
||||
bool? useActivityImageBackground,
|
||||
}) {
|
||||
return StyleSettings(
|
||||
fontSizeFactor: fontSizeFactor ?? this.fontSizeFactor,
|
||||
useActivityImageBackground:
|
||||
useActivityImageBackground ?? this.useActivityImageBackground,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,18 +41,42 @@ class _StyleSettings {
|
|||
class StyleSettingsRepo {
|
||||
static final GetStorage _storage = GetStorage("style_settings");
|
||||
|
||||
static Future<double> fontSizeFactor(String userId) async {
|
||||
static String _storageKey(String userId) => '${userId}_style_settings';
|
||||
|
||||
static Future<StyleSettings> settings(String userId) async {
|
||||
await GetStorage.init("style_settings");
|
||||
final json =
|
||||
_storage.read<Map<String, dynamic>>('${userId}_style_settings');
|
||||
final settings =
|
||||
json != null ? _StyleSettings.fromJson(json) : const _StyleSettings();
|
||||
return settings.fontSizeFactor;
|
||||
final key = _storageKey(userId);
|
||||
final json = _storage.read<Map<String, dynamic>>(key);
|
||||
if (json == null) return const StyleSettings();
|
||||
try {
|
||||
return StyleSettings.fromJson(json);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"settings_entry": json,
|
||||
},
|
||||
);
|
||||
_storage.remove(key);
|
||||
return const StyleSettings();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setFontSizeFactor(String userId, double factor) async {
|
||||
await GetStorage.init("style_settings");
|
||||
final settings = _StyleSettings(fontSizeFactor: factor);
|
||||
await _storage.write('${userId}_style_settings', settings.toJson());
|
||||
final currentSettings = await settings(userId);
|
||||
final updatedSettings = currentSettings.copyWith(fontSizeFactor: factor);
|
||||
await _storage.write(_storageKey(userId), updatedSettings.toJson());
|
||||
}
|
||||
|
||||
static Future<void> setUseActivityImageBackground(
|
||||
String userId,
|
||||
bool useBackground,
|
||||
) async {
|
||||
final currentSettings = await settings(userId);
|
||||
final updatedSettings = currentSettings.copyWith(
|
||||
useActivityImageBackground: useBackground,
|
||||
);
|
||||
await _storage.write(_storageKey(userId), updatedSettings.toJson());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -566,8 +566,10 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||
// double.tryParse(store.getString(SettingKeys.fontSizeFactor) ?? '') ??
|
||||
// AppConfig.fontSizeFactor;
|
||||
if (client.isLogged()) {
|
||||
StyleSettingsRepo.fontSizeFactor(client.userID!).then((factor) {
|
||||
AppConfig.fontSizeFactor = factor;
|
||||
StyleSettingsRepo.settings(client.userID!).then((settings) {
|
||||
AppConfig.fontSizeFactor = settings.fontSizeFactor;
|
||||
AppConfig.useActivityImageAsChatBackground =
|
||||
settings.useActivityImageBackground;
|
||||
});
|
||||
}
|
||||
// Pangea#
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue