chore(reading_assistance): persistent distractor selection and fixes to analytics saving

This commit is contained in:
wcjord 2025-03-31 11:44:18 -04:00
parent ac7d5f3938
commit b6e27d739a
20 changed files with 473 additions and 423 deletions

View file

@ -1,7 +1,4 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
@ -15,6 +12,7 @@ import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/user/client_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class MorphAnalyticsListView extends StatelessWidget {
final void Function(ConstructIdentifier) onConstructZoom;
@ -29,10 +27,10 @@ class MorphAnalyticsListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// spacing: 16.0,
spacing: 16.0,
children: [
// Add your text widget here
const InstructionsInlineTooltip(
@ -84,7 +82,6 @@ class MorphFeatureBox extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(16.0),
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
border: Border.all(

View file

@ -1,7 +1,4 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/analytics_details_popup/vocab_analytics_list_tile.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
@ -10,6 +7,7 @@ import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
/// Displays vocab analytics, sorted into categories
/// (flowers, greens, and seeds) by points
@ -113,14 +111,14 @@ class VocabAnalyticsListViewState extends State<VocabAnalyticsListView> {
),
);
return Column(
children: [
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.analyticsVocabList,
),
Padding(
padding: const EdgeInsets.all(32.0),
child: AnimatedContainer(
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.analyticsVocabList,
),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding:
@ -175,62 +173,29 @@ class VocabAnalyticsListViewState extends State<VocabAnalyticsListView> {
),
),
),
),
// Padding(
// padding: const EdgeInsets.all(32.0),
// child: Row(
// spacing: _isSearching ? 8.0 : 24.0,
// mainAxisAlignment: MainAxisAlignment.center,
// children: _isSearching
// ? [
// ConstrainedBox(
// constraints: const BoxConstraints(maxWidth: 200),
// child: TextField(
// autofocus: true,
// controller: _searchController,
// decoration: const InputDecoration(
// contentPadding: EdgeInsets.symmetric(
// vertical: 6.0,
// horizontal: 12.0,
// ),
// isDense: true,
// border: OutlineInputBorder(),
// ),
// onChanged: (value) {
// if (mounted) setState(() {});
// },
// ),
// ),
// IconButton(
// icon: const Icon(Icons.close),
// onPressed: _toggleSearching,
// ),
// ]
// : filters,
// ),
// ),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100.0,
mainAxisExtent: 100.0,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100.0,
mainAxisExtent: 100.0,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
),
itemCount: _filteredVocab.length,
itemBuilder: (context, index) {
final vocabItem = _filteredVocab[index];
return VocabAnalyticsListTile(
onTap: () => widget.onConstructZoom(vocabItem.id),
constructUse: vocabItem,
);
},
),
itemCount: _filteredVocab.length,
itemBuilder: (context, index) {
final vocabItem = _filteredVocab[index];
return VocabAnalyticsListTile(
onTap: () => widget.onConstructZoom(vocabItem.id),
constructUse: vocabItem,
);
},
),
),
),
],
],
),
);
}
}

View file

@ -1,14 +1,14 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_models.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'construct_type_enum.dart';
class ConstructAnalyticsModel {
@ -76,7 +76,7 @@ class OneConstructUse {
/// For vocab constructs, this is the POS. For morph
/// constructs, this is the morphological category.
String _category;
late String _category;
ConstructTypeEnum constructType;
ConstructUseTypeEnum useType;
@ -95,7 +95,13 @@ class OneConstructUse {
required category,
required this.form,
this.id,
}) : _category = category ?? "other";
}) {
if (category is MorphFeaturesEnum) {
_category = category.name;
} else {
_category = category ?? "other";
}
}
String? get chatId => metadata.roomId;
String get msgId => metadata.eventId!;

View file

@ -1,9 +1,6 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
@ -15,6 +12,9 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../common/utils/overlay.dart';
import '../controllers/it_feedback_controller.dart';
import '../models/it_response_model.dart';
@ -101,139 +101,145 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
axisAlignment: -1.0,
child: CompositedTransformTarget(
link: widget.choreographer.itBarLinkAndKey.link,
child: Container(
key: widget.choreographer.itBarLinkAndKey.key,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black,
),
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
child: SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
child: Column(
spacing: 8.0,
children: [
if (showITInstructionsTooltip)
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.clickBestOption,
animate: false,
),
if (showTranslationsChoicesTooltip)
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.translationChoices,
animate: false,
),
Container(
key: widget.choreographer.itBarLinkAndKey.key,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
),
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
child: SingleChildScrollView(
child: Column(
children: [
if (itController.isEditingSourceText)
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 20,
right: 10,
top: 10,
),
child: TextField(
controller: TextEditingController(
text: itController.sourceText,
),
autofocus: true,
enableSuggestions: false,
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: itController.onEditSourceTextSubmit,
obscureText: false,
decoration: const InputDecoration(
border: OutlineInputBorder(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (itController.isEditingSourceText)
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 20,
right: 10,
top: 10,
),
child: TextField(
controller: TextEditingController(
text: itController.sourceText,
),
autofocus: true,
enableSuggestions: false,
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted:
itController.onEditSourceTextSubmit,
obscureText: false,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
),
if (!itController.isEditingSourceText &&
itController.sourceText != null)
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
onPressed: () {
if (itController.nextITStep != null) {
itController.setIsEditingSourceText(true);
}
},
icon: const Icon(Icons.edit_outlined),
// iconSize: 20,
),
),
if (!itController.isEditingSourceText)
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.settings_outlined),
onPressed: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),
barrierDismissible: false,
),
),
),
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.close_outlined),
onPressed: () {
itController.isEditingSourceText
? itController.setIsEditingSourceText(false)
: itController.closeIT();
},
),
),
),
if (!itController.isEditingSourceText &&
itController.sourceText != null)
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
onPressed: () {
if (itController.nextITStep != null) {
itController.setIsEditingSourceText(true);
}
},
icon: const Icon(Icons.edit_outlined),
// iconSize: 20,
),
),
],
),
if (!itController.isEditingSourceText)
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.settings_outlined),
onPressed: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),
barrierDismissible: false,
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: itController.sourceText != null
? Text(
itController.sourceText!,
textAlign: TextAlign.center,
)
: const LinearProgressIndicator(),
),
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.close_outlined),
onPressed: () {
itController.isEditingSourceText
? itController.setIsEditingSourceText(false)
: itController.closeIT();
},
const SizedBox(height: 8.0),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
constraints: const BoxConstraints(minHeight: 80),
child: AnimatedSize(
duration: itController.animationSpeed,
child: Center(
child: itController.choreographer.errorService.isError
? ITError(
error: itController
.choreographer.errorService.error!,
controller: itController,
)
: itController.showChoiceFeedback
? ChoiceFeedbackText(
controller: itController,
)
: itController.isTranslationDone
? TranslationFeedback(
controller: itController,
)
: ITChoices(controller: itController),
),
),
),
],
),
if (!itController.isEditingSourceText)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: itController.sourceText != null
? Text(
itController.sourceText!,
textAlign: TextAlign.center,
)
: const LinearProgressIndicator(),
),
const SizedBox(height: 8.0),
if (showITInstructionsTooltip)
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.clickBestOption,
),
if (showTranslationsChoicesTooltip)
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.translationChoices,
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
constraints: const BoxConstraints(minHeight: 80),
child: AnimatedSize(
duration: itController.animationSpeed,
child: Center(
child: itController.choreographer.errorService.isError
? ITError(
error: itController
.choreographer.errorService.error!,
controller: itController,
)
: itController.showChoiceFeedback
? ChoiceFeedbackText(
controller: itController,
)
: itController.isTranslationDone
? TranslationFeedback(
controller: itController,
)
: ITChoices(controller: itController),
),
),
),
],
),
),
),
],
),
),
);

View file

@ -1,13 +1,16 @@
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
class ConstructForm {
String form;
ConstructIdentifier cId;
/// Form of the construct
final String form;
ConstructForm(
this.form,
this.cId,
);
/// The constructIdenfifier
final ConstructIdentifier cId;
ConstructForm({
required this.form,
required this.cId,
});
@override
bool operator ==(Object other) {
@ -18,4 +21,18 @@ class ConstructForm {
@override
int get hashCode => form.hashCode ^ cId.hashCode;
factory ConstructForm.fromJson(Map<String, dynamic> json) {
return ConstructForm(
form: json['form'],
cId: ConstructIdentifier.fromJson(json['cId']),
);
}
Map<String, dynamic> toJson() {
return {
'form': form,
'cId': cId.toJson(),
};
}
}

View file

@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/analytics_misc/construct_use_model.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/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
@ -493,6 +494,9 @@ class PangeaToken {
category: pos,
);
ConstructForm get vocabForm =>
ConstructForm(form: text.content, cId: vocabConstructID);
/// [setEmoji] sets the emoji for the lemma
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
Future<void> setEmoji(List<String> emojis) =>

View file

@ -9,11 +9,15 @@ import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
class InstructionsInlineTooltip extends StatefulWidget {
final InstructionsEnum instructionsEnum;
final bool bold;
final bool animate;
final double padding;
const InstructionsInlineTooltip({
super.key,
required this.instructionsEnum,
this.bold = false,
this.animate = true,
this.padding = 0.0,
});
@override
@ -24,8 +28,8 @@ class InstructionsInlineTooltip extends StatefulWidget {
class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
with TickerProviderStateMixin {
bool _isToggledOff = true;
late AnimationController _controller;
late Animation<double> _animation;
AnimationController? _controller;
Animation<double>? _animation;
@override
void didUpdateWidget(covariant InstructionsInlineTooltip oldWidget) {
@ -44,26 +48,28 @@ class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
void setToggled() {
_isToggledOff = widget.instructionsEnum.isToggledOff;
// Initialize AnimationController and Animation
_controller = AnimationController(
duration: FluffyThemes.animationDuration,
vsync: this,
);
if (widget.animate) {
// Initialize AnimationController and Animation only if animate is true
_controller = AnimationController(
duration: FluffyThemes.animationDuration,
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_animation = CurvedAnimation(
parent: _controller!,
curve: Curves.easeInOut,
);
// Start in correct state
if (!_isToggledOff) _controller.forward();
// Start in correct state
if (!_isToggledOff) _controller!.forward();
}
setState(() {});
}
@override
void dispose() {
_controller.dispose();
_controller?.dispose();
super.dispose();
}
@ -71,15 +77,28 @@ class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
widget.instructionsEnum.setToggledOff(true);
setState(() {
_isToggledOff = true;
_controller.reverse();
if (widget.animate) {
_controller?.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: _animation,
axisAlignment: -1.0,
return widget.animate
? SizeTransition(
sizeFactor: _animation!,
axisAlignment: -1.0,
child: _buildTooltipContent(context),
)
: (_isToggledOff
? const SizedBox.shrink()
: _buildTooltipContent(context));
}
Widget _buildTooltipContent(BuildContext context) {
return Padding(
padding: EdgeInsets.all(widget.padding),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),

View file

@ -9,10 +9,10 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/message_token_text/dotted_border_painter.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
@ -129,7 +129,7 @@ class MessageTokenButtonState extends State<MessageTokenButton>
PracticeTarget? get activity => widget.practiceTarget;
onMatch(ConstructForm form) {
onMatch(PracticeChoice form) {
if (widget.overlayController?.activity == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
@ -256,7 +256,7 @@ class MessageTokenButtonState extends State<MessageTokenButton>
);
}
return DragTarget<ConstructForm>(
return DragTarget<PracticeChoice>(
builder: (BuildContext context, accepted, rejected) {
final double colorAlpha = 0.3 +
(widget.overlayController?.selectedChoice != null ? 0.4 : 0.0) +

View file

@ -1,8 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
@ -10,6 +6,8 @@ import 'package:fluffychat/pangea/practice_activities/message_activity_request.d
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_match.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class EmojiActivityGenerator {
Future<MessageActivityResponse> get(
@ -57,8 +55,8 @@ class EmojiActivityGenerator {
final List<LemmaInfoResponse> lemmaInfos =
await Future.wait(lemmaInfoFutures);
final Map<ConstructIdentifier, List<String>> matchInfo = Map.fromIterables(
req.targetTokens.map((token) => token.vocabConstructID),
final Map<ConstructForm, List<String>> matchInfo = Map.fromIterables(
req.targetTokens.map((token) => token.vocabForm),
lemmaInfos.map((e) => e.emoji),
);
@ -67,7 +65,7 @@ class EmojiActivityGenerator {
activityType: ActivityTypeEnum.emoji,
targetTokens: req.targetTokens,
langCode: req.userL2,
matchContent: PracticeMatch(
matchContent: PracticeMatchActivity(
matchInfo: matchInfo,
),
),

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
@ -80,8 +81,8 @@ class LemmaMeaningActivityGenerator {
final List<LemmaInfoResponse> lemmaInfos =
await Future.wait(lemmaInfoFutures);
final Map<ConstructIdentifier, List<String>> matchInfo = Map.fromIterables(
req.targetTokens.map((token) => token.vocabConstructID),
final Map<ConstructForm, List<String>> matchInfo = Map.fromIterables(
req.targetTokens.map((token) => token.vocabForm),
lemmaInfos.map((lemmaInfo) => [lemmaInfo.meaning]),
);
@ -90,7 +91,7 @@ class LemmaMeaningActivityGenerator {
activityType: ActivityTypeEnum.wordMeaning,
targetTokens: req.targetTokens,
langCode: req.userL2,
matchContent: PracticeMatch(
matchContent: PracticeMatchActivity(
matchInfo: matchInfo,
),
),

View file

@ -11,13 +11,13 @@ 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_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_form.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/lemmas/user_set_lemma_info.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/practice_activities/practice_match.dart';
import 'package:fluffychat/pangea/practice_activities/practice_record.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
@ -32,7 +32,7 @@ class PracticeActivityModel {
final String langCode;
final MultipleChoiceActivity? multipleChoiceContent;
final PracticeMatch? matchContent;
final PracticeMatchActivity? matchContent;
PracticeActivityModel({
required this.targetTokens,
@ -60,7 +60,7 @@ class PracticeActivityModel {
void onMultipleChoiceSelect(
PangeaToken token,
ConstructForm choice,
PracticeChoice choice,
PangeaMessageEvent? event,
void Function() callback,
) {
@ -74,23 +74,24 @@ class PracticeActivityModel {
return;
}
if (practiceTarget.record.hasTextResponse(choice.form) || isComplete) {
if (practiceTarget.record.hasTextResponse(choice.choiceContent) ||
isComplete) {
// the user has already selected this choice
// so we don't want to record it again
return;
}
final bool isCorrect = multipleChoiceContent!.answers.any(
(answer) => answer.toLowerCase() == choice.form.toLowerCase(),
(answer) => answer.toLowerCase() == choice.choiceContent.toLowerCase(),
);
// NOTE: the response is associated with the contructId of the choice, not the selected token
// example: the user selects the word "cat" to match with the emoji 🐶
// the response is associated with correct word "dog", not the word "cat"
practiceTarget.record.addResponse(
cId: choice.cId,
cId: choice.form.cId,
target: practiceTarget,
text: choice.form,
text: choice.choiceContent,
score: isCorrect ? 1 : 0,
);
@ -105,15 +106,15 @@ class PracticeActivityModel {
constructs: [
OneConstructUse(
useType: responseUseType(choice)!,
lemma: choice.cId.lemma,
constructType: choice.cId.type,
lemma: choice.form.cId.lemma,
constructType: choice.form.cId.type,
metadata: ConstructUseMetaData(
roomId: event?.room.id,
timeStamp: DateTime.now(),
eventId: event?.eventId,
),
category: choice.cId.category,
form: choice.form,
category: choice.form.cId.category,
form: choice.form.form,
),
],
targetID: targetTokens.first.text.uniqueKey,
@ -126,24 +127,25 @@ class PracticeActivityModel {
/// only set up for vocab constructs atm
void onMatch(
PangeaToken token,
ConstructForm choice,
PracticeChoice choice,
PangeaMessageEvent? event,
void Function() callback,
) {
// the user has already selected this choice
// so we don't want to record it again
if (practiceTarget.record.hasTextResponse(choice.form) || isComplete) {
if (practiceTarget.record.hasTextResponse(choice.choiceContent) ||
isComplete) {
return;
}
bool isCorrect = false;
if (multipleChoiceContent != null) {
isCorrect = multipleChoiceContent!.answers.any(
(answer) => answer.toLowerCase() == choice.form.toLowerCase(),
(answer) => answer.toLowerCase() == choice.choiceContent.toLowerCase(),
);
} else if (matchContent != null) {
isCorrect = matchContent!.matchInfo[token.vocabConstructID]!
.contains(choice.form);
isCorrect = matchContent!.matchInfo[token.vocabForm]!
.contains(choice.choiceContent);
} else {
debugger(when: kDebugMode);
ErrorHandler.logError(
@ -158,8 +160,8 @@ class PracticeActivityModel {
// example: the user selects the word "cat" to match with the emoji 🐶
// the response is associated with correct word "dog", not the word "cat"
practiceTarget.record.addResponse(
cId: choice.cId,
text: choice.form,
cId: choice.form.cId,
text: choice.choiceContent,
target: practiceTarget,
score: isCorrect ? 1 : 0,
);
@ -173,14 +175,14 @@ class PracticeActivityModel {
constructs: [
OneConstructUse(
useType: responseUseType(choice)!,
lemma: choice.cId.lemma,
constructType: choice.cId.type,
lemma: choice.form.cId.lemma,
constructType: choice.form.cId.type,
metadata: ConstructUseMetaData(
roomId: event?.room.id,
timeStamp: DateTime.now(),
eventId: event?.eventId,
),
category: choice.cId.category,
category: choice.form.cId.category,
// in the case of a wrong answer, the cId doesn't match the token
form: token.text.content,
),
@ -193,16 +195,16 @@ class PracticeActivityModel {
if (activityType == ActivityTypeEnum.emoji) {
// final allEmojis = ;
choice.cId
.setUserLemmaInfo(UserSetLemmaInfo(emojis: [choice.form]))
choice.form.cId
.setUserLemmaInfo(UserSetLemmaInfo(emojis: [choice.choiceContent]))
.then((value) {
callback();
});
}
if (activityType == ActivityTypeEnum.wordMeaning) {
choice.cId
.setUserLemmaInfo(UserSetLemmaInfo(meaning: choice.form))
choice.form.cId
.setUserLemmaInfo(UserSetLemmaInfo(meaning: choice.choiceContent))
.then((value) {
callback();
});
@ -222,23 +224,23 @@ class PracticeActivityModel {
}
/// if null, it means the user has not yet responded with that choice
bool? wasCorrectMatch(ConstructForm choice) {
bool? wasCorrectMatch(PracticeChoice choice) {
for (final response in practiceTarget.record.responses) {
if (response.cId == choice.cId && response.isCorrect) {
if (response.cId == choice.form.cId && response.isCorrect) {
return true;
}
}
for (final response in practiceTarget.record.responses) {
if (response.cId == choice.cId) {
if (response.cId == choice.form.cId) {
return false;
}
}
return null;
}
ConstructUseTypeEnum? responseUseType(ConstructForm choice) {
ConstructUseTypeEnum? responseUseType(PracticeChoice choice) {
for (final response in practiceTarget.record.responses) {
if (response.cId == choice.cId) {
if (response.cId == choice.form.cId) {
return response.useType(activityType);
}
}
@ -324,7 +326,7 @@ class PracticeActivityModel {
.map((e) => PangeaToken.fromJson(e as Map<String, dynamic>))
.toList(),
matchContent: json['match_content'] != null
? PracticeMatch.fromJson(contentMap)
? PracticeMatchActivity.fromJson(contentMap)
: null,
morphFeature: json['morph_feature'] != null
? MorphFeaturesEnumExtension.fromString(

View file

@ -0,0 +1,26 @@
import 'package:fluffychat/pangea/constructs/construct_form.dart';
class PracticeChoice {
/// choiceContent
final String choiceContent;
/// Form of the associated token
final ConstructForm form;
PracticeChoice({
required this.choiceContent,
required this.form,
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PracticeChoice &&
other.form == form &&
other.choiceContent == choiceContent;
}
@override
int get hashCode => form.hashCode ^ choiceContent.hashCode;
}

View file

@ -1,16 +1,16 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
class PracticeMatch {
class PracticeMatchActivity {
/// The constructIdenfifiers involved in the activity
/// and the forms that are acceptable answers
final Map<ConstructIdentifier, List<String>> matchInfo;
final Map<ConstructForm, List<String>> matchInfo;
List<ConstructForm> displayForms = List.empty(growable: true);
List<PracticeChoice> choices = List.empty(growable: true);
PracticeMatch({
PracticeMatchActivity({
required this.matchInfo,
}) {
// if there are multiple forms for a construct, pick one to display
@ -21,38 +21,40 @@ class PracticeMatch {
// either from that construct's options, or returning to the previous construct
// and picking a different form from there
for (final ith in matchInfo.entries) {
for (final form in ith.value) {
if (!displayForms.any((element) => element.form == form)) {
displayForms.add(ConstructForm(form, ith.key));
for (final acceptableAnswer in ith.value) {
if (!choices
.any((element) => element.choiceContent == acceptableAnswer)) {
choices.add(
PracticeChoice(choiceContent: acceptableAnswer, form: ith.key),
);
break;
}
// TODO: if none found, we can probably pick a different form for the other one
}
}
// remove any items from matchInfo that don't have an item in displayForms
// remove any items from matchInfo that don't have an item in choices
for (final ith in matchInfo.keys) {
if (!displayForms.any((element) => element.cId == ith)) {
if (!choices.any((choice) => choice.form == ith)) {
matchInfo.remove(ith);
}
}
}
bool isCorrect(ConstructIdentifier cId, String value) {
return matchInfo[cId]!.contains(value);
bool isCorrect(ConstructForm form, String value) {
return matchInfo[form]!.contains(value);
}
factory PracticeMatch.fromJson(Map<String, dynamic> json) {
final Map<ConstructIdentifier, List<String>> matchInfo = {};
factory PracticeMatchActivity.fromJson(Map<String, dynamic> json) {
final Map<ConstructForm, List<String>> matchInfo = {};
for (final constructJson in json['match_info']) {
final ConstructIdentifier cId =
ConstructIdentifier.fromJson(constructJson['cId']);
final ConstructForm cId = ConstructForm.fromJson(constructJson['cId']);
final List<String> surfaceForms =
List<String>.from(constructJson['forms']);
matchInfo[cId] = surfaceForms;
}
return PracticeMatch(
return PracticeMatchActivity(
matchInfo: matchInfo,
);
}
@ -75,7 +77,7 @@ class PracticeMatch {
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PracticeMatch &&
return other is PracticeMatchActivity &&
const MapEquality().equals(other.matchInfo, matchInfo);
}

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
@ -55,10 +56,16 @@ class WordFocusListeningGenerator {
activityType: ActivityTypeEnum.wordFocusListening,
targetTokens: req.targetTokens,
langCode: req.userL2,
matchContent: PracticeMatch(
matchContent: PracticeMatchActivity(
matchInfo: Map.fromEntries(
req.targetTokens.map(
(token) => MapEntry(token.vocabConstructID, [token.text.content]),
(token) => MapEntry(
ConstructForm(
cId: token.vocabConstructID,
form: token.text.content,
),
[token.text.content],
),
),
),
),

View file

@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_morph_choice_item.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -126,11 +127,15 @@ class MessageMorphInputBarContentState
widget.activity.onMultipleChoiceSelect(
token,
ConstructForm(
choice,
widget.activity.targetTokens.first.morphIdByFeature(
widget.activity.morphFeature!,
)!,
PracticeChoice(
choiceContent: choice,
form: ConstructForm(
cId: widget.activity.targetTokens.first
.morphIdByFeature(
widget.activity.morphFeature!,
)!,
form: token.text.content,
),
),
widget.pangeaMessageEvent,
() => overlay.setState(() {}),

View file

@ -5,9 +5,9 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
@ -81,8 +81,8 @@ class MatchActivityCard extends StatelessWidget {
alignment: WrapAlignment.center,
spacing: 4.0,
runSpacing: 4.0,
children: activity.matchContent!.displayForms.map(
(ConstructForm cf) {
children: activity.matchContent!.choices.map(
(PracticeChoice cf) {
return ChoiceAnimationWidget(
isSelected: overlayController.selectedChoice == cf,
isCorrect: currentActivity.wasCorrectMatch(cf) ?? false,
@ -90,10 +90,10 @@ class MatchActivityCard extends StatelessWidget {
isSelected: overlayController.selectedChoice == cf,
isCorrect: currentActivity.wasCorrectMatch(cf),
constructForm: cf,
content: choiceDisplayContent(cf.form, fontSize),
content: choiceDisplayContent(cf.choiceContent, fontSize),
audioContent:
activityType == ActivityTypeEnum.wordFocusListening
? cf.form
? cf.choiceContent
: null,
overlayController: overlayController,
fixedSize: activityType == ActivityTypeEnum.wordMeaning

View file

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -22,7 +22,7 @@ class PracticeMatchItem extends StatefulWidget {
});
final Widget content;
final ConstructForm constructForm;
final PracticeChoice constructForm;
final String? audioContent;
final MessageOverlayController overlayController;
final double? fixedSize;
@ -129,9 +129,14 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
);
}
void onTap() {
play();
widget.overlayController.onChoiceSelect(widget.constructForm);
}
@override
Widget build(BuildContext context) {
return LongPressDraggable<ConstructForm>(
return LongPressDraggable<PracticeChoice>(
data: widget.constructForm,
feedback: Material(
type: MaterialType.transparency,
@ -144,10 +149,7 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
child: InkWell(
onHover: (isHovered) => setState(() => _isHovered = isHovered),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
onTap: () {
play();
widget.overlayController.onChoiceSelect(widget.constructForm);
},
onTap: onTap,
child: content(context),
),
);

View file

@ -7,6 +7,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_speech_to_text_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_translation_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morph_focus_widget.dart';
@ -35,63 +36,65 @@ class ReadingAssistanceInputBar extends StatelessWidget {
)
: null;
switch (overlayController.toolbarMode) {
case MessageMode.messageSpeechToText:
case MessageMode.practiceActivity:
case MessageMode.wordZoom:
case MessageMode.noneSelected:
case MessageMode.messageMeaning:
//TODO: show all emojis for the lemmas and allow sending normal reactions
break;
if (overlayController.pangeaMessageEvent?.isAudioMessage == true) {
return MessageSpeechToTextCard(
messageEvent: overlayController.pangeaMessageEvent!,
);
} else {
switch (overlayController.toolbarMode) {
case MessageMode.messageSpeechToText:
case MessageMode.practiceActivity:
case MessageMode.wordZoom:
case MessageMode.noneSelected:
case MessageMode.messageMeaning:
//TODO: show all emojis for the lemmas and allow sending normal reactions
break;
case MessageMode.messageTranslation:
if (overlayController.isTranslationUnlocked) {
content = MessageTranslationCard(
messageEvent: overlayController.pangeaMessageEvent!,
);
} else {
content = MessageModeLockedCard(controller: overlayController);
}
case MessageMode.messageTranslation:
if (overlayController.isTranslationUnlocked) {
content = MessageTranslationCard(
messageEvent: overlayController.pangeaMessageEvent!,
);
} else {
content = MessageModeLockedCard(controller: overlayController);
}
case MessageMode.wordEmoji:
case MessageMode.wordMeaning:
case MessageMode.listening:
debugPrint(
"reading_assistance_input_bar: wordEmoji or wordMeaning with target: $target",
);
if (target != null) {
content = PracticeActivityCard(
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
targetTokensAndActivityType: target,
overlayController: overlayController,
);
} else {
content = const Text("All done!");
}
case MessageMode.wordMorph:
if (target != null) {
content = PracticeActivityCard(
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
targetTokensAndActivityType: target,
overlayController: overlayController,
);
} else if (overlayController.selectedMorph != null) {
content = MorphFocusWidget(
token: overlayController.selectedMorph!.token,
morphFeature: overlayController.selectedMorph!.morph,
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
overlayController: overlayController,
onEditDone: () => overlayController.setState(() {}),
);
} else {
content = Center(
child: Text(
L10n.of(context).selectForGrammar,
style: Theme.of(context).textTheme.bodyLarge,
),
);
}
case MessageMode.wordEmoji:
case MessageMode.wordMeaning:
case MessageMode.listening:
if (target != null) {
content = PracticeActivityCard(
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
targetTokensAndActivityType: target,
overlayController: overlayController,
);
} else {
content = const Text("All done!");
}
case MessageMode.wordMorph:
if (target != null) {
content = PracticeActivityCard(
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
targetTokensAndActivityType: target,
overlayController: overlayController,
);
} else if (overlayController.selectedMorph != null) {
content = MorphFocusWidget(
token: overlayController.selectedMorph!.token,
morphFeature: overlayController.selectedMorph!.morph,
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
overlayController: overlayController,
onEditDone: () => overlayController.setState(() {}),
);
} else {
content = Center(
child: Text(
L10n.of(context).selectForGrammar,
style: Theme.of(context).textTheme.bodyLarge,
),
);
}
}
}
if (content == null) {
@ -108,17 +111,6 @@ class ReadingAssistanceInputBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (controller.showEmojiPicker) return const SizedBox.shrink();
final display = controller.editEvent == null &&
controller.replyEvent == null &&
controller.room.canSendDefaultMessages &&
controller.selectedEvents.isNotEmpty;
if (!display) {
return const SizedBox.shrink();
}
return Expanded(
child: Container(
width: overlayController.maxWidth,

View file

@ -12,7 +12,6 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart';
@ -23,6 +22,7 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart';
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
@ -77,7 +77,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
MorphSelection? selectedMorph;
/// tracks selected choice
ConstructForm? selectedChoice;
PracticeChoice? selectedChoice;
PangeaTokenText? _selectedSpan;
@ -336,7 +336,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
}
}
void onChoiceSelect(ConstructForm choice, [bool force = false]) {
void onChoiceSelect(PracticeChoice choice, [bool force = false]) {
if (selectedChoice == choice && !force) {
selectedChoice = null;
} else {
@ -399,6 +399,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/// you have to complete one of the mode mini-games to unlock translation
bool get isTranslationUnlocked =>
pangeaMessageEvent?.ownMessage == true ||
!messageInUserL2 ||
(messageLemmaInfos?.isEmpty ?? false) ||
isEmojiDone ||

View file

@ -114,10 +114,10 @@ packages:
dependency: "direct main"
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.12.0"
audio_session:
dependency: transitive
description:
@ -218,10 +218,10 @@ packages:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
cached_network_image:
dependency: "direct main"
description:
@ -266,10 +266,10 @@ packages:
dependency: "direct main"
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
charcode:
dependency: transitive
description:
@ -306,18 +306,18 @@ packages:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
collection:
dependency: "direct main"
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.19.1"
colorize:
dependency: transitive
description:
@ -506,10 +506,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.2"
fcm_shared_isolate:
dependency: "direct main"
description:
@ -529,10 +529,10 @@ packages:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
@ -1389,18 +1389,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.7"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.8"
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
@ -1477,10 +1477,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
@ -1510,10 +1510,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.16.0"
mgrs_dart:
dependency: transitive
description:
@ -1694,10 +1694,10 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
path_parsing:
dependency: transitive
description:
@ -1822,10 +1822,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
version: "3.1.6"
platform_detect:
dependency: transitive
description:
@ -1902,10 +1902,10 @@ packages:
dependency: transitive
description:
name: process
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
version: "5.0.3"
proj4dart:
dependency: transitive
description:
@ -2283,10 +2283,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.10.1"
sprintf:
dependency: transitive
description:
@ -2363,26 +2363,26 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.1"
string_validator:
dependency: transitive
description:
@ -2435,34 +2435,34 @@ packages:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test:
dependency: transitive
description:
name: test
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
url: "https://pub.dev"
source: hosted
version: "1.25.8"
version: "1.25.15"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.8"
text_to_speech:
dependency: "direct main"
description:
@ -2755,10 +2755,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "14.3.0"
version: "14.3.1"
wakelock_plus:
dependency: "direct main"
description:
@ -2888,5 +2888,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.27.0"