merge conflicts
This commit is contained in:
commit
0f3e1d2cad
26 changed files with 485 additions and 163 deletions
|
|
@ -114,13 +114,12 @@ class ChatEventList extends StatelessWidget {
|
|||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
i--;
|
||||
|
||||
// The message at this index:
|
||||
// #Pangea
|
||||
// final event = events[i];
|
||||
final event = events[i - 1];
|
||||
// i--;
|
||||
i = i - 2;
|
||||
// Pangea#
|
||||
|
||||
final event = events[i];
|
||||
final animateIn = animateInEventIndex != null &&
|
||||
controller.timeline!.events.length > animateInEventIndex &&
|
||||
event == controller.timeline!.events[animateInEventIndex];
|
||||
|
|
|
|||
|
|
@ -42,11 +42,15 @@ class ChatView extends StatelessWidget {
|
|||
tooltip: L10n.of(context)!.edit,
|
||||
onPressed: controller.editSelectedEventAction,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_outlined),
|
||||
tooltip: L10n.of(context)!.copy,
|
||||
onPressed: controller.copyEventsAction,
|
||||
),
|
||||
// #Pangea
|
||||
if (controller.selectedEvents.length == 1 &&
|
||||
controller.selectedEvents.single.messageType == MessageTypes.Text)
|
||||
// Pangea#
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_outlined),
|
||||
tooltip: L10n.of(context)!.copy,
|
||||
onPressed: controller.copyEventsAction,
|
||||
),
|
||||
if (controller.canSaveSelectedEvent)
|
||||
// Use builder context to correctly position the share dialog on iPad
|
||||
Builder(
|
||||
|
|
|
|||
|
|
@ -344,9 +344,6 @@ class MessageContent extends StatelessWidget {
|
|||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
);
|
||||
toolbarController?.toolbar?.textSelection.setMessageText(
|
||||
messageText,
|
||||
);
|
||||
return SelectableLinkify(
|
||||
onSelectionChanged: (selection, cause) {
|
||||
if (cause == SelectionChangedCause.longPress &&
|
||||
|
|
@ -363,8 +360,7 @@ class MessageContent extends StatelessWidget {
|
|||
.onTextSelection(selection);
|
||||
},
|
||||
onTap: () => toolbarController?.showToolbar(context),
|
||||
text: toolbarController?.toolbar?.textSelection.messageText ??
|
||||
messageText,
|
||||
text: messageText,
|
||||
contextMenuBuilder: (context, state) =>
|
||||
(toolbarController?.highlighted ?? false)
|
||||
? const SizedBox.shrink()
|
||||
|
|
|
|||
|
|
@ -191,7 +191,10 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = isLoggingIn = false;
|
||||
// #Pangea
|
||||
// isLoading = isLoggingIn = false;
|
||||
isLoggingIn = false;
|
||||
// Pangea#
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,14 @@ class HomeserverPickerView extends StatelessWidget {
|
|||
// Pangea#
|
||||
Expanded(
|
||||
child: controller.isLoading
|
||||
? const Center(child: CircularProgressIndicator.adaptive())
|
||||
// #Pangea
|
||||
// ? const Center(child: CircularProgressIndicator.adaptive())
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.black),
|
||||
),
|
||||
)
|
||||
// Pangea#
|
||||
: ListView(
|
||||
children: [
|
||||
if (errorText != null) ...[
|
||||
|
|
|
|||
|
|
@ -120,7 +120,9 @@ class LoginController extends State<Login> {
|
|||
return setState(() => loading = false);
|
||||
}
|
||||
|
||||
if (mounted) setState(() => loading = false);
|
||||
// #Pangea
|
||||
// if (mounted) setState(() => loading = false);
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
Timer? _coolDown;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class NewGroupView extends StatelessWidget {
|
|||
Expanded(
|
||||
child: TextField(
|
||||
// #Pangea
|
||||
maxLength: 32,
|
||||
maxLength: 64,
|
||||
// Pangea#
|
||||
controller: controller.nameController,
|
||||
autocorrect: false,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ class NewSpaceView extends StatelessWidget {
|
|||
Expanded(
|
||||
child: TextField(
|
||||
// #Pangea
|
||||
maxLength: 32,
|
||||
maxLength: 64,
|
||||
// Pangea#
|
||||
controller: controller.nameController,
|
||||
autocorrect: false,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../models/language_detection_model.dart';
|
||||
import '../../models/span_card_model.dart';
|
||||
import '../../repo/span_data_repo.dart';
|
||||
|
|
@ -237,7 +236,8 @@ class IgcController {
|
|||
|
||||
clear() {
|
||||
igcTextData = null;
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
// Not sure why this is here
|
||||
// MatrixState.pAnyState.closeOverlay();
|
||||
}
|
||||
|
||||
bool get canSendMessage {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class ITController {
|
|||
String? sourceText;
|
||||
List<ITStep> completedITSteps = [];
|
||||
CurrentITStep? currentITStep;
|
||||
CurrentITStep? nextITStep;
|
||||
GoldRouteTracker goldRouteTracker = GoldRouteTracker.defaultTracker;
|
||||
List<int> payLoadIds = [];
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ class ITController {
|
|||
sourceText = null;
|
||||
completedITSteps = [];
|
||||
currentITStep = null;
|
||||
nextITStep = null;
|
||||
goldRouteTracker = GoldRouteTracker.defaultTracker;
|
||||
payLoadIds = [];
|
||||
|
||||
|
|
@ -130,36 +132,75 @@ class ITController {
|
|||
);
|
||||
}
|
||||
|
||||
currentITStep = null;
|
||||
if (nextITStep == null) {
|
||||
currentITStep = null;
|
||||
|
||||
final ITResponseModel res = await _customInputTranslation(currentText);
|
||||
// final ITResponseModel res = await (useCustomInput ||
|
||||
// currentText.isEmpty ||
|
||||
// translationId == null ||
|
||||
// completedITSteps.last.chosenContinuance?.indexSavedByServer ==
|
||||
// null
|
||||
// ? _customInputTranslation(currentText)
|
||||
// : _systemChoiceTranslation(translationId));
|
||||
final ITResponseModel res = await _customInputTranslation(currentText);
|
||||
// final ITResponseModel res = await (useCustomInput ||
|
||||
// currentText.isEmpty ||
|
||||
// translationId == null ||
|
||||
// completedITSteps.last.chosenContinuance?.indexSavedByServer ==
|
||||
// null
|
||||
// ? _customInputTranslation(currentText)
|
||||
// : _systemChoiceTranslation(translationId));
|
||||
|
||||
if (res.goldContinuances != null && res.goldContinuances!.isNotEmpty) {
|
||||
goldRouteTracker = GoldRouteTracker(
|
||||
res.goldContinuances!,
|
||||
sourceText!,
|
||||
if (res.goldContinuances != null && res.goldContinuances!.isNotEmpty) {
|
||||
goldRouteTracker = GoldRouteTracker(
|
||||
res.goldContinuances!,
|
||||
sourceText!,
|
||||
);
|
||||
}
|
||||
|
||||
currentITStep = CurrentITStep(
|
||||
sourceText: sourceText!,
|
||||
currentText: currentText,
|
||||
responseModel: res,
|
||||
storedGoldContinuances: goldRouteTracker.continuances,
|
||||
);
|
||||
|
||||
_addPayloadId(res);
|
||||
} else {
|
||||
currentITStep = nextITStep;
|
||||
nextITStep = null;
|
||||
}
|
||||
|
||||
currentITStep = CurrentITStep(
|
||||
sourceText: sourceText!,
|
||||
currentText: currentText,
|
||||
responseModel: res,
|
||||
storedGoldContinuances: goldRouteTracker.continuances,
|
||||
);
|
||||
|
||||
_addPayloadId(res);
|
||||
|
||||
if (isTranslationDone) {
|
||||
choreographer.altTranslator.setTranslationFeedback();
|
||||
choreographer.getLanguageHelp(true);
|
||||
} else {
|
||||
getNextTranslationData();
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
if (e is! http.Response) {
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
choreographer.errorService.setErrorAndLock(
|
||||
ChoreoError(type: ChoreoErrorType.unknown, raw: e),
|
||||
);
|
||||
} finally {
|
||||
choreographer.stopLoading();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void>getNextTranslationData() async {
|
||||
try {
|
||||
if (completedITSteps.length < goldRouteTracker.continuances.length) {
|
||||
final String currentText = choreographer.currentText;
|
||||
final String nextText =
|
||||
goldRouteTracker.continuances[completedITSteps.length].text;
|
||||
|
||||
final ITResponseModel res =
|
||||
await _customInputTranslation(currentText + nextText);
|
||||
|
||||
nextITStep = CurrentITStep(
|
||||
sourceText: sourceText!,
|
||||
currentText: nextText,
|
||||
responseModel: res,
|
||||
storedGoldContinuances: goldRouteTracker.continuances,
|
||||
);
|
||||
} else {
|
||||
nextITStep = null;
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../utils/bot_style.dart';
|
||||
import 'it_shimmer.dart';
|
||||
|
|
@ -56,10 +58,12 @@ class Choice {
|
|||
Choice({
|
||||
this.color,
|
||||
required this.text,
|
||||
this.isGold = false,
|
||||
});
|
||||
|
||||
final Color? color;
|
||||
final String text;
|
||||
final bool isGold;
|
||||
}
|
||||
|
||||
class ChoiceItem extends StatelessWidget {
|
||||
|
|
@ -86,45 +90,50 @@ class ChoiceItem extends StatelessWidget {
|
|||
waitDuration: onLongPress != null
|
||||
? const Duration(milliseconds: 500)
|
||||
: const Duration(days: 1),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: isSelected
|
||||
? BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
border: Border.all(
|
||||
color: entry.value.color ?? theme.colorScheme.primary,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
child: ChoiceAnimationWidget(
|
||||
key: ValueKey(entry.value.text),
|
||||
selected: entry.value.color != null,
|
||||
isGold: entry.value.isGold,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: isSelected
|
||||
? BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
border: Border.all(
|
||||
color: entry.value.color ?? theme.colorScheme.primary,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 7),
|
||||
),
|
||||
//if index is selected, then give the background a slight primary color
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
entry.value.color != null
|
||||
? entry.value.color!.withOpacity(0.2)
|
||||
: theme.colorScheme.primary.withOpacity(0.1),
|
||||
),
|
||||
textStyle: MaterialStateProperty.all(
|
||||
BotStyle.text(context),
|
||||
),
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 7),
|
||||
),
|
||||
//if index is selected, then give the background a slight primary color
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
entry.value.color != null
|
||||
? entry.value.color!.withOpacity(0.2)
|
||||
: theme.colorScheme.primary.withOpacity(0.1),
|
||||
),
|
||||
textStyle: MaterialStateProperty.all(
|
||||
BotStyle.text(context),
|
||||
),
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onLongPress:
|
||||
onLongPress != null ? () => onLongPress!(entry.key) : null,
|
||||
onPressed: () => onPressed(entry.key),
|
||||
child: Text(
|
||||
entry.value.text,
|
||||
style: BotStyle.text(context),
|
||||
onLongPress:
|
||||
onLongPress != null ? () => onLongPress!(entry.key) : null,
|
||||
onPressed: () => onPressed(entry.key),
|
||||
child: Text(
|
||||
entry.value.text,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -135,3 +144,110 @@ class ChoiceItem extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChoiceAnimationWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
final bool selected;
|
||||
final bool isGold;
|
||||
|
||||
const ChoiceAnimationWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.selected,
|
||||
this.isGold = false,
|
||||
});
|
||||
|
||||
@override
|
||||
ChoiceAnimationWidgetState createState() => ChoiceAnimationWidgetState();
|
||||
}
|
||||
|
||||
class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _animation;
|
||||
bool animationPlayed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = widget.isGold
|
||||
? Tween<double>(begin: 1.0, end: 1.2).animate(_controller)
|
||||
: TweenSequence<double>([
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0, end: -8 * pi / 180),
|
||||
weight: 1.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: -8 * pi / 180, end: 16 * pi / 180),
|
||||
weight: 2.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 16 * pi / 180, end: 0),
|
||||
weight: 1.0,
|
||||
),
|
||||
]).animate(_controller);
|
||||
|
||||
if (widget.selected && !animationPlayed) {
|
||||
_controller.forward();
|
||||
animationPlayed = true;
|
||||
setState(() {});
|
||||
}
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reverse();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
_controller.stop();
|
||||
_controller.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ChoiceAnimationWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selected && !animationPlayed) {
|
||||
_controller.forward();
|
||||
animationPlayed = true;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.isGold
|
||||
? AnimatedBuilder(
|
||||
key: UniqueKey(),
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
)
|
||||
: AnimatedBuilder(
|
||||
key: UniqueKey(),
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: _animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -280,7 +280,11 @@ class ITChoices extends StatelessWidget {
|
|||
originalSpan: "dummy",
|
||||
choices: controller.currentITStep!.continuances.map((e) {
|
||||
try {
|
||||
return Choice(text: e.text.trim(), color: e.color);
|
||||
return Choice(
|
||||
text: e.text.trim(),
|
||||
color: e.color,
|
||||
isGold: e.description == "best",
|
||||
);
|
||||
} catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
return Choice(text: "error", color: Colors.red);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/construct_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -111,4 +112,48 @@ class MyAnalyticsController {
|
|||
ErrorHandler.logError(e: err, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
// used to aggregate ConstructEvents, from multiple senders (students) with the same lemma
|
||||
List<AggregateConstructUses> aggregateConstructData(
|
||||
List<ConstructEvent> constructs,
|
||||
) {
|
||||
final Map<String, List<ConstructEvent>> lemmasToConstructs = {};
|
||||
for (final construct in constructs) {
|
||||
lemmasToConstructs[construct.content.lemma] ??= [];
|
||||
lemmasToConstructs[construct.content.lemma]!.add(construct);
|
||||
}
|
||||
|
||||
final List<AggregateConstructUses> aggregatedConstructs = [];
|
||||
for (final lemmaToConstructs in lemmasToConstructs.entries) {
|
||||
final List<ConstructEvent> lemmaConstructs = lemmaToConstructs.value;
|
||||
final AggregateConstructUses aggregatedData = AggregateConstructUses(
|
||||
constructs: lemmaConstructs,
|
||||
);
|
||||
aggregatedConstructs.add(aggregatedData);
|
||||
}
|
||||
return aggregatedConstructs;
|
||||
}
|
||||
}
|
||||
|
||||
class AggregateConstructUses {
|
||||
final List<ConstructEvent> _constructs;
|
||||
|
||||
AggregateConstructUses({required List<ConstructEvent> constructs})
|
||||
: _constructs = constructs;
|
||||
|
||||
String get lemma {
|
||||
assert(
|
||||
_constructs.isNotEmpty &&
|
||||
_constructs.every(
|
||||
(construct) =>
|
||||
construct.content.lemma == _constructs.first.content.lemma,
|
||||
),
|
||||
);
|
||||
return _constructs.first.content.lemma;
|
||||
}
|
||||
|
||||
List<OneConstructUse> get uses => _constructs
|
||||
.map((construct) => construct.content.uses)
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,26 +68,33 @@ class ConstructUses {
|
|||
}
|
||||
|
||||
enum ConstructUseType {
|
||||
/// encountered match and accepted it
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a correct use
|
||||
wa,
|
||||
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
|
||||
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
|
||||
ga,
|
||||
|
||||
/// used without assistance
|
||||
wa,
|
||||
/// produced in chat by user and igc was not run
|
||||
unk,
|
||||
|
||||
/// selected correctly in IT flow
|
||||
corIt,
|
||||
|
||||
/// encountered as it distractor and selected it
|
||||
incIt,
|
||||
|
||||
/// encountered as IT distractor and correctly ignored it
|
||||
ignIt,
|
||||
|
||||
/// encountered as it distractor and selected it
|
||||
incIt,
|
||||
|
||||
/// encountered in igc match and ignored match
|
||||
ignIGC,
|
||||
|
||||
/// encountered in igc match and ignored match
|
||||
/// selected correctly in IGC flow
|
||||
corIGC,
|
||||
|
||||
/// encountered as distractor in IGC flow and selected it
|
||||
incIGC,
|
||||
}
|
||||
|
||||
extension on ConstructUseType {
|
||||
|
|
@ -107,6 +114,10 @@ extension on ConstructUseType {
|
|||
return 'ignIGC';
|
||||
case ConstructUseType.corIGC:
|
||||
return 'corIGC';
|
||||
case ConstructUseType.incIGC:
|
||||
return 'incIGC';
|
||||
case ConstructUseType.unk:
|
||||
return 'unk';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +137,10 @@ extension on ConstructUseType {
|
|||
return Icons.close;
|
||||
case ConstructUseType.corIGC:
|
||||
return Icons.check;
|
||||
case ConstructUseType.incIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseType.unk:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
import '../enum/vocab_proficiency_enum.dart';
|
||||
|
||||
class VocabHeadwords {
|
||||
|
|
@ -176,6 +176,11 @@ class VocabTotals {
|
|||
case ConstructUseType.corIGC:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseType.incIGC:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseType.unk:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,6 @@ class AnalyticsListTileState extends State<AnalyticsListTile> {
|
|||
)
|
||||
: null,
|
||||
selected: widget.selected,
|
||||
enabled: widget.enabled,
|
||||
onTap: () {
|
||||
(room?.isSpace ?? false) && widget.allowNavigateOnSelect
|
||||
? context.go(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:fluffychat/pangea/extensions/client_extension/client_extension.d
|
|||
import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
|
|
@ -101,18 +102,40 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
}
|
||||
}
|
||||
|
||||
void toggleSelection(AnalyticsSelected selectedParam) {
|
||||
Future<void> toggleSelection(AnalyticsSelected selectedParam) async {
|
||||
final bool joinSelectedRoom =
|
||||
selectedParam.type == AnalyticsEntryType.room &&
|
||||
!enableSelection(
|
||||
selectedParam,
|
||||
);
|
||||
|
||||
if (joinSelectedRoom) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = Matrix.of(context).client.waitForRoomInSync(
|
||||
selectedParam.id,
|
||||
join: true,
|
||||
);
|
||||
await Matrix.of(context).client.joinRoom(selectedParam.id);
|
||||
await waitForRoom;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
debugPrint("selectedParam.id is ${selectedParam.id}");
|
||||
currentLemma = null;
|
||||
selected = isSelected(selectedParam.id) ? null : selectedParam;
|
||||
});
|
||||
|
||||
pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: selected,
|
||||
removeIT: true,
|
||||
);
|
||||
|
||||
Future.delayed(Duration.zero, () => setState(() {}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
) *
|
||||
72,
|
||||
child: TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/class_list/class_list_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../constants/pangea_event_types.dart';
|
||||
import '../../../controllers/pangea_controller.dart';
|
||||
|
|
@ -42,7 +41,11 @@ class AnalyticsClassListController extends State<AnalyticsClassList> {
|
|||
if (!(refreshTimer[newState.room.id]?.isActive ?? false)) {
|
||||
refreshTimer[newState.room.id] = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() => updateClassAnalytics(context, newState.room),
|
||||
() {
|
||||
if (newState.room.isSpace) {
|
||||
updateClassAnalytics(context, newState.room);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import 'dart:async';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/construct_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
|
|
@ -169,7 +169,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
|
||||
int get lemmaIndex =>
|
||||
constructs?.indexWhere(
|
||||
(element) => element.content.lemma == widget.controller.currentLemma,
|
||||
(element) => element.lemma == widget.controller.currentLemma,
|
||||
) ??
|
||||
-1;
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
|
||||
setState(() => fetchingUses = true);
|
||||
try {
|
||||
final List<OneConstructUse> uses = currentConstruct!.content.uses;
|
||||
final List<OneConstructUse> uses = currentConstruct!.uses;
|
||||
_msgEvents.clear();
|
||||
|
||||
for (final OneConstructUse use in uses) {
|
||||
|
|
@ -236,16 +236,24 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
m: "Failed to fetch uses for current construct ${currentConstruct?.content.lemma}",
|
||||
m: "Failed to fetch uses for current construct ${currentConstruct?.lemma}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<ConstructEvent>? get constructs =>
|
||||
widget.pangeaController.analytics.constructs;
|
||||
List<AggregateConstructUses>? get constructs =>
|
||||
widget.pangeaController.analytics.constructs != null
|
||||
? widget.pangeaController.myAnalytics
|
||||
.aggregateConstructData(
|
||||
widget.pangeaController.analytics.constructs!,
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => b.uses.length.compareTo(a.uses.length),
|
||||
)
|
||||
: null;
|
||||
|
||||
ConstructEvent? get currentConstruct => constructs?.firstWhereOrNull(
|
||||
(element) => element.content.lemma == widget.controller.currentLemma,
|
||||
AggregateConstructUses? get currentConstruct => constructs?.firstWhereOrNull(
|
||||
(element) => element.lemma == widget.controller.currentLemma,
|
||||
);
|
||||
|
||||
// given the current lemma and list of message events, return a list of
|
||||
|
|
@ -280,6 +288,13 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
return allMsgErrorSteps;
|
||||
}
|
||||
|
||||
Future<void> showConstructMessagesDialog() async {
|
||||
await showDialog<ConstructMessagesDialog>(
|
||||
context: context,
|
||||
builder: (c) => ConstructMessagesDialog(controller: this),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.init || fetchingUses) {
|
||||
|
|
@ -294,58 +309,92 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
);
|
||||
}
|
||||
|
||||
final msgEventMatches = getMessageEventMatches();
|
||||
|
||||
return widget.controller.currentLemma == null
|
||||
? Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: constructs!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
constructs![index].content.lemma,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${L10n.of(context)!.total} ${constructs![index].content.uses.length}',
|
||||
),
|
||||
onTap: () {
|
||||
final String lemma = constructs![index].content.lemma;
|
||||
widget.controller.setCurrentLemma(lemma);
|
||||
fetchUses();
|
||||
},
|
||||
);
|
||||
},
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: constructs!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
constructs![index].lemma,
|
||||
),
|
||||
)
|
||||
: Expanded(
|
||||
subtitle: Text(
|
||||
'${L10n.of(context)!.total} ${constructs![index].uses.length}',
|
||||
),
|
||||
onTap: () async {
|
||||
final String lemma = constructs![index].lemma;
|
||||
widget.controller.setCurrentLemma(lemma);
|
||||
fetchUses().then((_) => showConstructMessagesDialog());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructMessagesDialog extends StatelessWidget {
|
||||
final ConstructListViewState controller;
|
||||
const ConstructMessagesDialog({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.widget.controller.currentLemma == null) {
|
||||
return const AlertDialog(content: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final msgEventMatches = controller.getMessageEventMatches();
|
||||
|
||||
return AlertDialog(
|
||||
title: Center(child: Text(controller.widget.controller.currentLemma!)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (controller.constructs![controller.lemmaIndex].uses.length >
|
||||
controller._msgEvents.length)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context)!.roomDataMissing),
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (constructs![lemmaIndex].content.uses.length >
|
||||
_msgEvents.length)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context)!.roomDataMissing),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, index) =>
|
||||
...msgEventMatches.mapIndexed(
|
||||
(index, event) => Column(
|
||||
children: [
|
||||
ConstructMessage(
|
||||
msgEvent: event.msgEvent,
|
||||
lemma: controller.widget.controller.currentLemma!,
|
||||
errorMessage: event.lemmaMatch,
|
||||
),
|
||||
if (index < msgEventMatches.length - 1)
|
||||
const Divider(height: 1),
|
||||
itemCount: msgEventMatches.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ConstructMessage(
|
||||
msgEvent: msgEventMatches[index].msgEvent,
|
||||
lemma: widget.controller.currentLemma!,
|
||||
errorMessage: msgEventMatches[index].lemmaMatch,
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
child: Text(
|
||||
L10n.of(context)!.close.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(150),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ class ClassNameHeader extends StatelessWidget {
|
|||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
),
|
||||
label: visible.Visibility(
|
||||
icon: visible.Visibility(
|
||||
visible: controller.showEditNameIcon,
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
icon: room.nameAndRoomTypeIcon(
|
||||
label: room.nameAndRoomTypeIcon(
|
||||
TextStyle(
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ void setClassDisplayname(BuildContext context, String? roomId) async {
|
|||
: L10n.of(context)!.changeTheNameOfTheChat,
|
||||
),
|
||||
content: TextField(
|
||||
maxLength: 32,
|
||||
maxLength: 64,
|
||||
controller: textFieldController,
|
||||
),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -188,10 +188,13 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
currentMode = newMode;
|
||||
updatingMode = true;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
currentMode = newMode;
|
||||
updatingMode = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!subscribed) {
|
||||
toolbarContent = MessageUnsubscribedCard(
|
||||
languageTool: newMode.title(context),
|
||||
|
|
@ -221,9 +224,11 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
break;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
updatingMode = false;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
updatingMode = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void showTranslation() {
|
||||
|
|
@ -289,7 +294,7 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
final bool autoplay = MatrixState.pangeaController.pStoreService.read(
|
||||
PLocalKey.autoPlayMessages,
|
||||
) ??
|
||||
true;
|
||||
false;
|
||||
|
||||
if (widget.pangeaMessageEvent.isAudioMessage) {
|
||||
updateMode(MessageMode.speechToText);
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
l2Code = MatrixState.pangeaController.languageController.activeL2Code(
|
||||
roomID: widget.messageEvent.room.id,
|
||||
);
|
||||
setState(() {});
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
loadTranslation(() async {
|
||||
if (widget.selection.selectedText != null) {
|
||||
|
|
|
|||
|
|
@ -61,9 +61,11 @@ class PangeaRichTextState extends State<PangeaRichText> {
|
|||
widget.toolbarController?.toolbar?.textSelection.setMessageText(
|
||||
newTextSpan,
|
||||
);
|
||||
setState(() {
|
||||
textSpan = newTextSpan;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
textSpan = newTextSpan;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void setTextSpan() {
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ class WordMatchContent extends StatelessWidget {
|
|||
(e) => Choice(
|
||||
text: e.value,
|
||||
color: e.selected ? e.type.color : null,
|
||||
isGold: e.type.name == 'bestCorrection',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue