Merge branch 'main' of https://github.com/pangeachat/client into show-disabled-buttons
This commit is contained in:
commit
175c11ea0b
31 changed files with 904 additions and 666 deletions
|
|
@ -102,30 +102,47 @@ class SettingsSecurityController extends State<SettingsSecurity> {
|
||||||
if (mxid == null || mxid.isEmpty || mxid != supposedMxid) {
|
if (mxid == null || mxid.isEmpty || mxid != supposedMxid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final input = await showTextInputDialog(
|
// #Pangea
|
||||||
useRootNavigator: false,
|
// final input = await showTextInputDialog(
|
||||||
|
// useRootNavigator: false,
|
||||||
|
// context: context,
|
||||||
|
// title: L10n.of(context).pleaseEnterYourPassword,
|
||||||
|
// okLabel: L10n.of(context).ok,
|
||||||
|
// cancelLabel: L10n.of(context).cancel,
|
||||||
|
// isDestructive: true,
|
||||||
|
// obscureText: true,
|
||||||
|
// hintText: '******',
|
||||||
|
// minLines: 1,
|
||||||
|
// maxLines: 1,
|
||||||
|
// );
|
||||||
|
// if (input == null) return;
|
||||||
|
// await showFutureLoadingDialog(
|
||||||
|
// context: context,
|
||||||
|
// future: () => Matrix.of(context).client.deactivateAccount(
|
||||||
|
// auth: AuthenticationPassword(
|
||||||
|
// password: input,
|
||||||
|
// identifier: AuthenticationUserIdentifier(
|
||||||
|
// user: Matrix.of(context).client.userID!,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// Pangea#
|
||||||
|
|
||||||
|
final resp = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context).pleaseEnterYourPassword,
|
delay: false,
|
||||||
okLabel: L10n.of(context).ok,
|
future: () =>
|
||||||
cancelLabel: L10n.of(context).cancel,
|
Matrix.of(context).client.uiaRequestBackground<IdServerUnbindResult?>(
|
||||||
isDestructive: true,
|
(auth) => Matrix.of(context).client.deactivateAccount(
|
||||||
obscureText: true,
|
auth: auth,
|
||||||
hintText: '******',
|
),
|
||||||
minLines: 1,
|
|
||||||
maxLines: 1,
|
|
||||||
);
|
|
||||||
if (input == null) return;
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => Matrix.of(context).client.deactivateAccount(
|
|
||||||
auth: AuthenticationPassword(
|
|
||||||
password: input,
|
|
||||||
identifier: AuthenticationUserIdentifier(
|
|
||||||
user: Matrix.of(context).client.userID!,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!resp.isError) {
|
||||||
|
await Matrix.of(context).client.logout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showBootstrapDialog(BuildContext context) async {
|
void showBootstrapDialog(BuildContext context) async {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
|
import 'package:fluffychat/pangea/analytics_summary/progress_bar/animated_progress_bar.dart';
|
||||||
|
|
||||||
|
class LevelPopupProgressBar extends StatefulWidget {
|
||||||
|
final double height;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
const LevelPopupProgressBar({
|
||||||
|
required this.height,
|
||||||
|
required this.duration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
LevelPopupProgressBarState createState() => LevelPopupProgressBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class LevelPopupProgressBarState extends State<LevelPopupProgressBar> {
|
||||||
|
double width = 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {
|
||||||
|
width = 1.0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedProgressBar(
|
||||||
|
height: widget.height,
|
||||||
|
widthPercent: width,
|
||||||
|
barColor: AppConfig.goldLight,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
duration: widget.duration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,11 +12,10 @@ import 'package:matrix/matrix_api_lite/generated/model.dart';
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/l10n/l10n.dart';
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
|
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
|
||||||
|
import 'package:fluffychat/pangea/analytics_misc/level_up/level_popup_progess_bar.dart';
|
||||||
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart';
|
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart';
|
||||||
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart';
|
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart';
|
||||||
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
|
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart';
|
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart';
|
|
||||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||||
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
|
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
|
||||||
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
|
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
|
||||||
|
|
@ -193,11 +192,6 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
|
||||||
@override
|
@override
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Animation<double> progressAnimation =
|
|
||||||
Tween<double>(begin: 0, end: 1).animate(
|
|
||||||
CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.5)),
|
|
||||||
);
|
|
||||||
|
|
||||||
final Animation<int> vocabAnimation =
|
final Animation<int> vocabAnimation =
|
||||||
IntTween(begin: _startVocab, end: _endVocab).animate(
|
IntTween(begin: _startVocab, end: _endVocab).animate(
|
||||||
CurvedAnimation(
|
CurvedAnimation(
|
||||||
|
|
@ -282,23 +276,10 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (_, __) => Row(
|
builder: (_, __) => Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
const Expanded(
|
||||||
child: LayoutBuilder(
|
child: LevelPopupProgressBar(
|
||||||
builder: (context, constraints) {
|
height: 20,
|
||||||
return LevelBar(
|
duration: Duration(milliseconds: 1000),
|
||||||
details: const LevelBarDetails(
|
|
||||||
fillColor: AppConfig.goldLight,
|
|
||||||
currentPoints: 0,
|
|
||||||
widthMultiplier: 1,
|
|
||||||
),
|
|
||||||
progressBarDetails: ProgressBarDetails(
|
|
||||||
totalWidth: constraints.maxWidth *
|
|
||||||
progressAnimation.value,
|
|
||||||
height: 20,
|
|
||||||
borderColor: colorScheme.primary,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar.dart';
|
import 'package:fluffychat/pangea/analytics_summary/progress_bar/animated_progress_bar.dart';
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart';
|
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
class LearningProgressBar extends StatelessWidget {
|
class LearningProgressBar extends StatelessWidget {
|
||||||
final int level;
|
final int level;
|
||||||
final int totalXP;
|
final int totalXP;
|
||||||
final double? height;
|
final double height;
|
||||||
final bool loading;
|
final bool loading;
|
||||||
|
|
||||||
const LearningProgressBar({
|
const LearningProgressBar({
|
||||||
required this.level,
|
required this.level,
|
||||||
required this.totalXP,
|
required this.totalXP,
|
||||||
required this.loading,
|
required this.loading,
|
||||||
this.height,
|
required this.height,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -30,16 +29,12 @@ class LearningProgressBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ProgressBar(
|
|
||||||
|
return AnimatedProgressBar(
|
||||||
height: height,
|
height: height,
|
||||||
levelBars: [
|
widthPercent: MatrixState.pangeaController.getAnalytics.levelProgress,
|
||||||
LevelBarDetails(
|
barColor: AppConfig.goldLight,
|
||||||
fillColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
currentPoints: totalXP,
|
|
||||||
widthMultiplier:
|
|
||||||
MatrixState.pangeaController.getAnalytics.levelProgress,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
|
||||||
|
|
||||||
class AnimatedLevelBar extends StatefulWidget {
|
|
||||||
final double height;
|
|
||||||
final double beginWidth;
|
|
||||||
final double endWidth;
|
|
||||||
final Color primaryColor;
|
|
||||||
|
|
||||||
const AnimatedLevelBar({
|
|
||||||
super.key,
|
|
||||||
required this.height,
|
|
||||||
required this.beginWidth,
|
|
||||||
required this.endWidth,
|
|
||||||
required this.primaryColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
AnimatedLevelBarState createState() => AnimatedLevelBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnimatedLevelBarState extends State<AnimatedLevelBar>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _controller;
|
|
||||||
|
|
||||||
double get _beginWidth =>
|
|
||||||
widget.beginWidth == 0 ? 0 : max(20, widget.beginWidth);
|
|
||||||
double get _endWidth => widget.endWidth == 0 ? 0 : max(20, widget.endWidth);
|
|
||||||
|
|
||||||
/// Whether the animation has run for the first time during initState. Don't
|
|
||||||
/// want the animation to run when the widget mounts, only when points are gained.
|
|
||||||
bool _init = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: FluffyThemes.animationDuration,
|
|
||||||
);
|
|
||||||
|
|
||||||
_controller.forward().then((_) => _init = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant AnimatedLevelBar oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if ((oldWidget.endWidth == 0 ? 0 : max(20, oldWidget.endWidth)) !=
|
|
||||||
(widget.endWidth == 0 ? 0 : max(20, widget.endWidth))) {
|
|
||||||
_controller.reset();
|
|
||||||
_controller.forward();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation<double> get _animation {
|
|
||||||
// If this is the first run of the animation, don't animate. This is just the widget mounting,
|
|
||||||
// not a points gain. This could instead be 'if going from 0 to a non-zero value', but that
|
|
||||||
// would remove the animation for first points gained. It would remove the need for a flag though.
|
|
||||||
if (_init) {
|
|
||||||
return Tween<double>(
|
|
||||||
begin: _endWidth,
|
|
||||||
end: _endWidth,
|
|
||||||
).animate(_controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
// animate the width of the bar
|
|
||||||
return Tween<double>(
|
|
||||||
begin: _beginWidth,
|
|
||||||
end: _endWidth,
|
|
||||||
).animate(_controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _animation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
height: widget.height,
|
|
||||||
width: _animation.value,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: widget.primaryColor,
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(AppConfig.borderRadius),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
|
import 'package:fluffychat/config/themes.dart';
|
||||||
|
|
||||||
|
class AnimatedProgressBar extends StatelessWidget {
|
||||||
|
final double height;
|
||||||
|
final double widthPercent;
|
||||||
|
|
||||||
|
final Color barColor;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Duration? duration;
|
||||||
|
|
||||||
|
const AnimatedProgressBar({
|
||||||
|
required this.height,
|
||||||
|
required this.widthPercent,
|
||||||
|
required this.barColor,
|
||||||
|
required this.backgroundColor,
|
||||||
|
this.duration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
child: Container(
|
||||||
|
height: height,
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(AppConfig.borderRadius),
|
||||||
|
),
|
||||||
|
color: backgroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: duration ?? FluffyThemes.animationDuration,
|
||||||
|
height: height,
|
||||||
|
width: constraints.maxWidth * widthPercent,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: barColor,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(AppConfig.borderRadius),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/animated_level_dart.dart';
|
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart';
|
|
||||||
|
|
||||||
class LevelBar extends StatefulWidget {
|
|
||||||
final LevelBarDetails details;
|
|
||||||
final ProgressBarDetails progressBarDetails;
|
|
||||||
|
|
||||||
const LevelBar({
|
|
||||||
super.key,
|
|
||||||
required this.details,
|
|
||||||
required this.progressBarDetails,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
LevelBarState createState() => LevelBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class LevelBarState extends State<LevelBar> {
|
|
||||||
double prevWidth = 0;
|
|
||||||
double get width =>
|
|
||||||
widget.progressBarDetails.totalWidth * widget.details.widthMultiplier;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant LevelBar oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.details.currentPoints != widget.details.currentPoints) {
|
|
||||||
setState(() => prevWidth = width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedLevelBar(
|
|
||||||
height: widget.progressBarDetails.height,
|
|
||||||
beginWidth: prevWidth,
|
|
||||||
endWidth: width,
|
|
||||||
primaryColor: AppConfig.goldLight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart';
|
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_background.dart';
|
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart';
|
|
||||||
|
|
||||||
// Provide an order list of level indicators, each with it's color
|
|
||||||
// and stream. Also provide an overall width and pointsPerLevel.
|
|
||||||
|
|
||||||
class ProgressBar extends StatefulWidget {
|
|
||||||
final List<LevelBarDetails> levelBars;
|
|
||||||
final double? height;
|
|
||||||
|
|
||||||
const ProgressBar({
|
|
||||||
super.key,
|
|
||||||
required this.levelBars,
|
|
||||||
this.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ProgressBarState createState() => ProgressBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProgressBarState extends State<ProgressBar> {
|
|
||||||
double width = 0;
|
|
||||||
void setWidth(double newWidth) {
|
|
||||||
if (width != newWidth) {
|
|
||||||
setState(() => width = newWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get progressBarDetails => ProgressBarDetails(
|
|
||||||
totalWidth: width,
|
|
||||||
borderColor: Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
height: widget.height ?? 14,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
if (width != constraints.maxWidth) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) => setWidth(constraints.maxWidth),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
children: [
|
|
||||||
ProgressBarBackground(details: progressBarDetails),
|
|
||||||
for (final levelBar in widget.levelBars)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
||||||
child: LevelBar(
|
|
||||||
details: levelBar,
|
|
||||||
progressBarDetails: progressBarDetails,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
|
||||||
import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart';
|
|
||||||
|
|
||||||
class ProgressBarBackground extends StatelessWidget {
|
|
||||||
final ProgressBarDetails details;
|
|
||||||
|
|
||||||
const ProgressBarBackground({
|
|
||||||
super.key,
|
|
||||||
required this.details,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
||||||
child: Container(
|
|
||||||
height: details.height,
|
|
||||||
width: details.totalWidth,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(AppConfig.borderRadius),
|
|
||||||
),
|
|
||||||
color: details.borderColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
class LevelBarDetails {
|
|
||||||
final Color fillColor;
|
|
||||||
final int currentPoints;
|
|
||||||
final double widthMultiplier;
|
|
||||||
|
|
||||||
const LevelBarDetails({
|
|
||||||
required this.fillColor,
|
|
||||||
required this.currentPoints,
|
|
||||||
required this.widthMultiplier,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProgressBarDetails {
|
|
||||||
final double totalWidth;
|
|
||||||
final Color borderColor;
|
|
||||||
final double height;
|
|
||||||
|
|
||||||
const ProgressBarDetails({
|
|
||||||
required this.totalWidth,
|
|
||||||
required this.borderColor,
|
|
||||||
this.height = 14,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||||
|
|
@ -88,6 +90,10 @@ abstract class AsyncLoader<T> {
|
||||||
final result = await fetch();
|
final result = await fetch();
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
state.value = AsyncState.loaded(result);
|
state.value = AsyncState.loaded(result);
|
||||||
|
} on HttpException catch (e) {
|
||||||
|
if (!_disposed) {
|
||||||
|
state.value = AsyncState.error(e);
|
||||||
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
ErrorHandler.logError(
|
ErrorHandler.logError(
|
||||||
e: e,
|
e: e,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
|
|
@ -150,7 +151,7 @@ class ConstructIdentifier {
|
||||||
uses: [],
|
uses: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
LemmaInfoRequest get _lemmaInfoRequest => LemmaInfoRequest(
|
LemmaInfoRequest get lemmaInfoRequest => LemmaInfoRequest(
|
||||||
partOfSpeech: category,
|
partOfSpeech: category,
|
||||||
lemmaLang:
|
lemmaLang:
|
||||||
MatrixState.pangeaController.userController.userL2?.langCodeShort ??
|
MatrixState.pangeaController.userController.userL2?.langCodeShort ??
|
||||||
|
|
@ -162,8 +163,9 @@ class ConstructIdentifier {
|
||||||
);
|
);
|
||||||
|
|
||||||
/// [lemmmaLang] if not set, assumed to be userL2
|
/// [lemmmaLang] if not set, assumed to be userL2
|
||||||
Future<LemmaInfoResponse> getLemmaInfo() => LemmaInfoRepo.get(
|
Future<Result<LemmaInfoResponse>> getLemmaInfo() => LemmaInfoRepo.get(
|
||||||
_lemmaInfoRequest,
|
MatrixState.pangeaController.userController.accessToken,
|
||||||
|
lemmaInfoRequest,
|
||||||
);
|
);
|
||||||
|
|
||||||
List<String> get userSetEmoji => userLemmaInfo.emojis ?? [];
|
List<String> get userSetEmoji => userLemmaInfo.emojis ?? [];
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,12 @@ class LanguageArc {
|
||||||
'l2': l2.toJson(),
|
'l2': l2.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => l1.hashCode ^ l2.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is LanguageArc && other.l1 == l1 && other.l2 == l2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,105 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||||
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
|
||||||
|
class _LemmaInfoCacheItem {
|
||||||
|
final Future<Result<LemmaInfoResponse>> resultFuture;
|
||||||
|
final DateTime timestamp;
|
||||||
|
|
||||||
|
const _LemmaInfoCacheItem({
|
||||||
|
required this.resultFuture,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class LemmaInfoRepo {
|
class LemmaInfoRepo {
|
||||||
static final GetStorage _lemmaStorage = GetStorage('lemma_storage');
|
// In-memory cache
|
||||||
|
static final Map<String, _LemmaInfoCacheItem> _cache = {};
|
||||||
|
static const Duration _cacheDuration = Duration(minutes: 10);
|
||||||
|
|
||||||
static void set(LemmaInfoRequest request, LemmaInfoResponse response) {
|
// Persistent storage
|
||||||
// set expireAt if not set
|
static final GetStorage _storage = GetStorage('lemma_storage');
|
||||||
response.expireAt ??= DateTime.now().add(const Duration(days: 100));
|
|
||||||
_lemmaStorage.write(request.storageKey, response.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
static LemmaInfoResponse? getCached(LemmaInfoRequest request) {
|
|
||||||
final cachedJson = _lemmaStorage.read(request.storageKey);
|
|
||||||
|
|
||||||
final cached =
|
|
||||||
cachedJson == null ? null : LemmaInfoResponse.fromJson(cachedJson);
|
|
||||||
|
|
||||||
|
/// Public entry point
|
||||||
|
static Future<Result<LemmaInfoResponse>> get(
|
||||||
|
String accessToken,
|
||||||
|
LemmaInfoRequest request,
|
||||||
|
) {
|
||||||
|
// 1. Try memory cache
|
||||||
|
final cached = _getCached(request);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
if (DateTime.now().isBefore(cached.expireAt!)) {
|
return cached;
|
||||||
return cached;
|
|
||||||
} else {
|
|
||||||
_lemmaStorage.remove(request.storageKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
// 2. Try disk cache
|
||||||
|
final stored = _getStored(request);
|
||||||
|
if (stored != null) {
|
||||||
|
return Future.value(Result.value(stored));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch from network (safe future)
|
||||||
|
final future = _safeFetch(accessToken, request);
|
||||||
|
|
||||||
|
// 4. Save to in-memory cache
|
||||||
|
_cache[request.hashCode.toString()] = _LemmaInfoCacheItem(
|
||||||
|
resultFuture: future,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Write to disk *after* the fetch finishes, without rethrowing
|
||||||
|
writeToDisk(request, future);
|
||||||
|
|
||||||
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get lemma info, prefering user set data over fetched data
|
static Future<void> set(
|
||||||
static Future<LemmaInfoResponse> get(LemmaInfoRequest request) async {
|
LemmaInfoRequest request,
|
||||||
final cached = getCached(request);
|
LemmaInfoResponse resultFuture,
|
||||||
if (cached != null) return cached;
|
) async {
|
||||||
|
final key = request.hashCode.toString();
|
||||||
|
try {
|
||||||
|
await _storage.write(key, resultFuture.toJson());
|
||||||
|
_cache.remove(key); // Invalidate in-memory cache
|
||||||
|
} catch (e, s) {
|
||||||
|
ErrorHandler.logError(
|
||||||
|
e: e,
|
||||||
|
s: s,
|
||||||
|
data: {'lemma': request.lemma},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final Requests req = Requests(
|
static Future<Result<LemmaInfoResponse>> _safeFetch(
|
||||||
|
String token,
|
||||||
|
LemmaInfoRequest request,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final resp = await _fetch(token, request);
|
||||||
|
return Result.value(resp);
|
||||||
|
} catch (e, s) {
|
||||||
|
// Ensure error is logged and converted to a Result
|
||||||
|
ErrorHandler.logError(e: e, s: s, data: request.toJson());
|
||||||
|
return Result.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LemmaInfoResponse> _fetch(
|
||||||
|
String accessToken,
|
||||||
|
LemmaInfoRequest request,
|
||||||
|
) async {
|
||||||
|
final req = Requests(
|
||||||
choreoApiKey: Environment.choreoApiKey,
|
choreoApiKey: Environment.choreoApiKey,
|
||||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
accessToken: accessToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
final Response res = await req.post(
|
final Response res = await req.post(
|
||||||
|
|
@ -50,10 +107,59 @@ class LemmaInfoRepo {
|
||||||
body: request.toJson(),
|
body: request.toJson(),
|
||||||
);
|
);
|
||||||
|
|
||||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
if (res.statusCode != 200) {
|
||||||
final response = LemmaInfoResponse.fromJson(decodedBody);
|
throw HttpException(
|
||||||
|
'Failed to fetch lemma info: ${res.statusCode} ${res.reasonPhrase}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
set(request, response);
|
return LemmaInfoResponse.fromJson(
|
||||||
return response;
|
jsonDecode(utf8.decode(res.bodyBytes)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Result<LemmaInfoResponse>>? _getCached(
|
||||||
|
LemmaInfoRequest request,
|
||||||
|
) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final key = request.hashCode.toString();
|
||||||
|
|
||||||
|
// Remove stale entries first
|
||||||
|
_cache.removeWhere(
|
||||||
|
(_, item) => now.difference(item.timestamp) >= _cacheDuration,
|
||||||
|
);
|
||||||
|
|
||||||
|
final item = _cache[key];
|
||||||
|
return item?.resultFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> writeToDisk(
|
||||||
|
LemmaInfoRequest request,
|
||||||
|
Future<Result<LemmaInfoResponse>> resultFuture,
|
||||||
|
) async {
|
||||||
|
final result = await resultFuture; // SAFE: never throws
|
||||||
|
|
||||||
|
if (!result.isValue) return; // only cache successful responses
|
||||||
|
await set(request, result.asValue!.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LemmaInfoResponse? _getStored(
|
||||||
|
LemmaInfoRequest request,
|
||||||
|
) {
|
||||||
|
final key = request.hashCode.toString();
|
||||||
|
try {
|
||||||
|
final entry = _storage.read(key);
|
||||||
|
if (entry == null) return null;
|
||||||
|
|
||||||
|
return LemmaInfoResponse.fromJson(entry);
|
||||||
|
} catch (e, s) {
|
||||||
|
ErrorHandler.logError(
|
||||||
|
e: e,
|
||||||
|
s: s,
|
||||||
|
data: {'lemma': request.lemma},
|
||||||
|
);
|
||||||
|
_storage.remove(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,10 @@ import 'package:fluffychat/pangea/events/models/content_feedback.dart';
|
||||||
class LemmaInfoResponse implements JsonSerializable {
|
class LemmaInfoResponse implements JsonSerializable {
|
||||||
final List<String> emoji;
|
final List<String> emoji;
|
||||||
final String meaning;
|
final String meaning;
|
||||||
DateTime? expireAt;
|
|
||||||
|
|
||||||
LemmaInfoResponse({
|
LemmaInfoResponse({
|
||||||
required this.emoji,
|
required this.emoji,
|
||||||
required this.meaning,
|
required this.meaning,
|
||||||
this.expireAt,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory LemmaInfoResponse.fromJson(Map<String, dynamic> json) {
|
factory LemmaInfoResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -16,18 +14,19 @@ class LemmaInfoResponse implements JsonSerializable {
|
||||||
// NOTE: This is a workaround for the fact that the server sometimes sends more than 3 emojis
|
// NOTE: This is a workaround for the fact that the server sometimes sends more than 3 emojis
|
||||||
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
|
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
meaning: json['meaning'] as String,
|
meaning: json['meaning'] as String,
|
||||||
expireAt: json['expireAt'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['expireAt'] as String),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static LemmaInfoResponse get error => LemmaInfoResponse(
|
||||||
|
emoji: [],
|
||||||
|
meaning: 'ERROR',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'emoji': emoji,
|
'emoji': emoji,
|
||||||
'meaning': meaning,
|
'meaning': meaning,
|
||||||
'expireAt': expireAt?.toIso8601String(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pangea/common/utils/async_state.dart';
|
||||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||||
import 'package:fluffychat/pangea/languages/language_constants.dart';
|
import 'package:fluffychat/pangea/languages/language_constants.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
||||||
|
|
@ -7,6 +8,25 @@ import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
|
class _LemmaMeaningLoader extends AsyncLoader<LemmaInfoResponse> {
|
||||||
|
final LemmaInfoRequest request;
|
||||||
|
_LemmaMeaningLoader(this.request) : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LemmaInfoResponse> fetch() async {
|
||||||
|
final result = await LemmaInfoRepo.get(
|
||||||
|
MatrixState.pangeaController.userController.accessToken,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isError) {
|
||||||
|
throw result.asError!.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.asValue!.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LemmaMeaningBuilder extends StatefulWidget {
|
class LemmaMeaningBuilder extends StatefulWidget {
|
||||||
final String langCode;
|
final String langCode;
|
||||||
final ConstructIdentifier constructId;
|
final ConstructIdentifier constructId;
|
||||||
|
|
@ -27,14 +47,12 @@ class LemmaMeaningBuilder extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LemmaMeaningBuilderState extends State<LemmaMeaningBuilder> {
|
class LemmaMeaningBuilderState extends State<LemmaMeaningBuilder> {
|
||||||
LemmaInfoResponse? lemmaInfo;
|
late _LemmaMeaningLoader _loader;
|
||||||
bool isLoading = true;
|
|
||||||
Object? error;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_fetchLemmaMeaning();
|
_reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -42,10 +60,25 @@ class LemmaMeaningBuilderState extends State<LemmaMeaningBuilder> {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.constructId != widget.constructId ||
|
if (oldWidget.constructId != widget.constructId ||
|
||||||
oldWidget.langCode != widget.langCode) {
|
oldWidget.langCode != widget.langCode) {
|
||||||
_fetchLemmaMeaning();
|
_loader.dispose();
|
||||||
|
_reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_loader.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isLoading => _loader.isLoading;
|
||||||
|
bool get isError => _loader.isError;
|
||||||
|
|
||||||
|
Object? get error =>
|
||||||
|
isError ? (_loader.state.value as AsyncError).error : null;
|
||||||
|
|
||||||
|
LemmaInfoResponse? get lemmaInfo => _loader.value;
|
||||||
|
|
||||||
LemmaInfoRequest get _request => LemmaInfoRequest(
|
LemmaInfoRequest get _request => LemmaInfoRequest(
|
||||||
lemma: widget.constructId.lemma,
|
lemma: widget.constructId.lemma,
|
||||||
partOfSpeech: widget.constructId.category,
|
partOfSpeech: widget.constructId.category,
|
||||||
|
|
@ -54,27 +87,19 @@ class LemmaMeaningBuilderState extends State<LemmaMeaningBuilder> {
|
||||||
LanguageKeys.defaultLanguage,
|
LanguageKeys.defaultLanguage,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> _fetchLemmaMeaning() async {
|
void _reload() {
|
||||||
setState(() {
|
_loader = _LemmaMeaningLoader(_request);
|
||||||
isLoading = true;
|
_loader.load();
|
||||||
error = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
final resp = await LemmaInfoRepo.get(_request);
|
|
||||||
lemmaInfo = resp;
|
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
} finally {
|
|
||||||
if (mounted) setState(() => isLoading = false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return widget.builder(
|
return ValueListenableBuilder(
|
||||||
context,
|
valueListenable: _loader.state,
|
||||||
this,
|
builder: (context, _, __) => widget.builder(
|
||||||
|
context,
|
||||||
|
this,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pangea/common/utils/async_state.dart';
|
||||||
|
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||||
|
import 'package:fluffychat/pangea/languages/language_arc_model.dart';
|
||||||
|
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||||
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
import 'phonetic_transcription_repo.dart';
|
||||||
|
|
||||||
|
class _TranscriptLoader extends AsyncLoader<String> {
|
||||||
|
final PhoneticTranscriptionRequest request;
|
||||||
|
_TranscriptLoader(this.request) : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> fetch() async {
|
||||||
|
final resp = await PhoneticTranscriptionRepo.get(
|
||||||
|
MatrixState.pangeaController.userController.accessToken,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resp.isError) {
|
||||||
|
throw resp.asError!.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.asValue!.value.phoneticTranscriptionResult.phoneticTranscription
|
||||||
|
.first.phoneticL1Transcription.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PhoneticTranscriptionBuilder extends StatefulWidget {
|
||||||
|
final LanguageModel textLanguage;
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
PhoneticTranscriptionBuilderState controller,
|
||||||
|
) builder;
|
||||||
|
|
||||||
|
const PhoneticTranscriptionBuilder({
|
||||||
|
super.key,
|
||||||
|
required this.textLanguage,
|
||||||
|
required this.text,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
PhoneticTranscriptionBuilderState createState() =>
|
||||||
|
PhoneticTranscriptionBuilderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PhoneticTranscriptionBuilderState
|
||||||
|
extends State<PhoneticTranscriptionBuilder> {
|
||||||
|
late _TranscriptLoader _loader;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant PhoneticTranscriptionBuilder oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.text != widget.text ||
|
||||||
|
oldWidget.textLanguage != widget.textLanguage) {
|
||||||
|
_loader.dispose();
|
||||||
|
_reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_loader.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isLoading => _loader.isLoading;
|
||||||
|
bool get isError => _loader.isError;
|
||||||
|
|
||||||
|
Object? get error =>
|
||||||
|
isError ? (_loader.state.value as AsyncError).error : null;
|
||||||
|
|
||||||
|
String? get transcription => _loader.value;
|
||||||
|
|
||||||
|
PhoneticTranscriptionRequest get _transcriptRequest =>
|
||||||
|
PhoneticTranscriptionRequest(
|
||||||
|
arc: LanguageArc(
|
||||||
|
l1: MatrixState.pangeaController.userController.userL1!,
|
||||||
|
l2: widget.textLanguage,
|
||||||
|
),
|
||||||
|
content: PangeaTokenText.fromString(widget.text),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _reload() {
|
||||||
|
_loader = _TranscriptLoader(_transcriptRequest);
|
||||||
|
_loader.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: _loader.state,
|
||||||
|
builder: (context, _, __) => widget.builder(
|
||||||
|
context,
|
||||||
|
this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
|
|
@ -12,39 +11,95 @@ import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
||||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart';
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
|
||||||
|
class _PhoneticTranscriptionCacheItem {
|
||||||
|
final Future<Result<PhoneticTranscriptionResponse>> resultFuture;
|
||||||
|
final DateTime timestamp;
|
||||||
|
|
||||||
|
const _PhoneticTranscriptionCacheItem({
|
||||||
|
required this.resultFuture,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class PhoneticTranscriptionRepo {
|
class PhoneticTranscriptionRepo {
|
||||||
|
// In-memory cache
|
||||||
|
static final Map<String, _PhoneticTranscriptionCacheItem> _cache = {};
|
||||||
|
static const Duration _cacheDuration = Duration(minutes: 10);
|
||||||
|
|
||||||
|
// Persistent storage
|
||||||
static final GetStorage _storage =
|
static final GetStorage _storage =
|
||||||
GetStorage('phonetic_transcription_storage');
|
GetStorage('phonetic_transcription_storage');
|
||||||
|
|
||||||
|
static Future<Result<PhoneticTranscriptionResponse>> get(
|
||||||
|
String accessToken,
|
||||||
|
PhoneticTranscriptionRequest request,
|
||||||
|
) {
|
||||||
|
// 1. Try memory cache
|
||||||
|
final cached = _getCached(request);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try disk cache
|
||||||
|
final stored = _getStored(request);
|
||||||
|
if (stored != null) {
|
||||||
|
return Future.value(Result.value(stored));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch from network (safe future)
|
||||||
|
final future = _safeFetch(accessToken, request);
|
||||||
|
|
||||||
|
// 4. Save to in-memory cache
|
||||||
|
_cache[request.hashCode.toString()] = _PhoneticTranscriptionCacheItem(
|
||||||
|
resultFuture: future,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Write to disk *after* the fetch finishes, without rethrowing
|
||||||
|
writeToDisk(request, future);
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> set(
|
static Future<void> set(
|
||||||
PhoneticTranscriptionRequest request,
|
PhoneticTranscriptionRequest request,
|
||||||
PhoneticTranscriptionResponse response,
|
PhoneticTranscriptionResponse resultFuture,
|
||||||
) async {
|
) async {
|
||||||
response.expireAt ??= DateTime.now().add(const Duration(days: 100));
|
final key = request.hashCode.toString();
|
||||||
await _storage.write(request.storageKey, response.toJson());
|
try {
|
||||||
|
await _storage.write(key, resultFuture.toJson());
|
||||||
|
_cache.remove(key); // Invalidate in-memory cache
|
||||||
|
} catch (e, s) {
|
||||||
|
ErrorHandler.logError(
|
||||||
|
e: e,
|
||||||
|
s: s,
|
||||||
|
data: {'request': request.toJson()},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Result<PhoneticTranscriptionResponse>> _safeFetch(
|
||||||
|
String token,
|
||||||
|
PhoneticTranscriptionRequest request,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final resp = await _fetch(token, request);
|
||||||
|
return Result.value(resp);
|
||||||
|
} catch (e, s) {
|
||||||
|
// Ensure error is logged and converted to a Result
|
||||||
|
ErrorHandler.logError(e: e, s: s, data: request.toJson());
|
||||||
|
return Result.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<PhoneticTranscriptionResponse> _fetch(
|
static Future<PhoneticTranscriptionResponse> _fetch(
|
||||||
|
String accessToken,
|
||||||
PhoneticTranscriptionRequest request,
|
PhoneticTranscriptionRequest request,
|
||||||
) async {
|
) async {
|
||||||
final cachedJson = _storage.read(request.storageKey);
|
final req = Requests(
|
||||||
final cached = cachedJson == null
|
|
||||||
? null
|
|
||||||
: PhoneticTranscriptionResponse.fromJson(cachedJson);
|
|
||||||
|
|
||||||
if (cached != null) {
|
|
||||||
if (DateTime.now().isBefore(cached.expireAt!)) {
|
|
||||||
return cached;
|
|
||||||
} else {
|
|
||||||
_storage.remove(request.storageKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final Requests req = Requests(
|
|
||||||
choreoApiKey: Environment.choreoApiKey,
|
choreoApiKey: Environment.choreoApiKey,
|
||||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
accessToken: accessToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
final Response res = await req.post(
|
final Response res = await req.post(
|
||||||
|
|
@ -52,21 +107,59 @@ class PhoneticTranscriptionRepo {
|
||||||
body: request.toJson(),
|
body: request.toJson(),
|
||||||
);
|
);
|
||||||
|
|
||||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
if (res.statusCode != 200) {
|
||||||
final response = PhoneticTranscriptionResponse.fromJson(decodedBody);
|
throw HttpException(
|
||||||
set(request, response);
|
'Failed to fetch phonetic transcription: ${res.statusCode} ${res.reasonPhrase}',
|
||||||
return response;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PhoneticTranscriptionResponse.fromJson(
|
||||||
|
jsonDecode(utf8.decode(res.bodyBytes)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<PhoneticTranscriptionResponse> get(
|
static Future<Result<PhoneticTranscriptionResponse>>? _getCached(
|
||||||
PhoneticTranscriptionRequest request,
|
PhoneticTranscriptionRequest request,
|
||||||
|
) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final key = request.hashCode.toString();
|
||||||
|
|
||||||
|
// Remove stale entries first
|
||||||
|
_cache.removeWhere(
|
||||||
|
(_, item) => now.difference(item.timestamp) >= _cacheDuration,
|
||||||
|
);
|
||||||
|
|
||||||
|
final item = _cache[key];
|
||||||
|
return item?.resultFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> writeToDisk(
|
||||||
|
PhoneticTranscriptionRequest request,
|
||||||
|
Future<Result<PhoneticTranscriptionResponse>> resultFuture,
|
||||||
) async {
|
) async {
|
||||||
|
final result = await resultFuture; // SAFE: never throws
|
||||||
|
|
||||||
|
if (!result.isValue) return; // only cache successful responses
|
||||||
|
await set(request, result.asValue!.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PhoneticTranscriptionResponse? _getStored(
|
||||||
|
PhoneticTranscriptionRequest request,
|
||||||
|
) {
|
||||||
|
final key = request.hashCode.toString();
|
||||||
try {
|
try {
|
||||||
return await _fetch(request);
|
final entry = _storage.read(key);
|
||||||
} catch (e) {
|
if (entry == null) return null;
|
||||||
debugger(when: kDebugMode);
|
|
||||||
ErrorHandler.logError(e: e, data: request.toJson());
|
return PhoneticTranscriptionResponse.fromJson(entry);
|
||||||
rethrow;
|
} catch (e, s) {
|
||||||
|
ErrorHandler.logError(
|
||||||
|
e: e,
|
||||||
|
s: s,
|
||||||
|
data: {'request': request.toJson()},
|
||||||
|
);
|
||||||
|
_storage.remove(key);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,16 @@ class PhoneticTranscriptionRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}';
|
String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}';
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
content.hashCode ^ arc.hashCode ^ requiresTokenization.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is PhoneticTranscriptionRequest &&
|
||||||
|
other.content == content &&
|
||||||
|
other.arc == arc &&
|
||||||
|
other.requiresTokenization == requiresTokenization;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,14 +102,12 @@ class PhoneticTranscriptionResponse {
|
||||||
final Map<String, dynamic>
|
final Map<String, dynamic>
|
||||||
tokenization; // You can define a typesafe model if needed
|
tokenization; // You can define a typesafe model if needed
|
||||||
final PhoneticTranscription phoneticTranscriptionResult;
|
final PhoneticTranscription phoneticTranscriptionResult;
|
||||||
DateTime? expireAt;
|
|
||||||
|
|
||||||
PhoneticTranscriptionResponse({
|
PhoneticTranscriptionResponse({
|
||||||
required this.arc,
|
required this.arc,
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.tokenization,
|
required this.tokenization,
|
||||||
required this.phoneticTranscriptionResult,
|
required this.phoneticTranscriptionResult,
|
||||||
this.expireAt,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory PhoneticTranscriptionResponse.fromJson(Map<String, dynamic> json) {
|
factory PhoneticTranscriptionResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -121,9 +119,6 @@ class PhoneticTranscriptionResponse {
|
||||||
phoneticTranscriptionResult: PhoneticTranscription.fromJson(
|
phoneticTranscriptionResult: PhoneticTranscription.fromJson(
|
||||||
json['phonetic_transcription_result'] as Map<String, dynamic>,
|
json['phonetic_transcription_result'] as Map<String, dynamic>,
|
||||||
),
|
),
|
||||||
expireAt: json['expireAt'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['expireAt'] as String),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,7 +128,6 @@ class PhoneticTranscriptionResponse {
|
||||||
'content': content.toJson(),
|
'content': content.toJson(),
|
||||||
'tokenization': tokenization,
|
'tokenization': tokenization,
|
||||||
'phonetic_transcription_result': phoneticTranscriptionResult.toJson(),
|
'phonetic_transcription_result': phoneticTranscriptionResult.toJson(),
|
||||||
'expireAt': expireAt?.toIso8601String(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,9 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/l10n/l10n.dart';
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|
||||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
|
||||||
import 'package:fluffychat/pangea/languages/language_arc_model.dart';
|
|
||||||
import 'package:fluffychat/pangea/languages/language_model.dart';
|
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart';
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_builder.dart';
|
||||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
|
||||||
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
|
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
|
||||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
@ -43,79 +39,6 @@ class PhoneticTranscriptionWidget extends StatefulWidget {
|
||||||
class _PhoneticTranscriptionWidgetState
|
class _PhoneticTranscriptionWidgetState
|
||||||
extends State<PhoneticTranscriptionWidget> {
|
extends State<PhoneticTranscriptionWidget> {
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
bool _isLoading = false;
|
|
||||||
Object? _error;
|
|
||||||
|
|
||||||
String? _transcription;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_fetchTranscription();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(
|
|
||||||
covariant PhoneticTranscriptionWidget oldWidget,
|
|
||||||
) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.text != widget.text ||
|
|
||||||
oldWidget.textLanguage != widget.textLanguage) {
|
|
||||||
_fetchTranscription();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchTranscription() async {
|
|
||||||
try {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
_transcription = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (MatrixState.pangeaController.userController.userL1 == null) {
|
|
||||||
ErrorHandler.logError(
|
|
||||||
e: Exception('User L1 is not set'),
|
|
||||||
data: {
|
|
||||||
'text': widget.text,
|
|
||||||
'textLanguageCode': widget.textLanguage.langCode,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
_error = Exception('User L1 is not set');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final req = PhoneticTranscriptionRequest(
|
|
||||||
arc: LanguageArc(
|
|
||||||
l1: MatrixState.pangeaController.userController.userL1!,
|
|
||||||
l2: widget.textLanguage,
|
|
||||||
),
|
|
||||||
content: PangeaTokenText.fromString(widget.text),
|
|
||||||
// arc can be omitted for default empty map
|
|
||||||
);
|
|
||||||
final res = await PhoneticTranscriptionRepo.get(req);
|
|
||||||
_transcription = res.phoneticTranscriptionResult.phoneticTranscription
|
|
||||||
.first.phoneticL1Transcription.content;
|
|
||||||
} catch (e, s) {
|
|
||||||
_error = e;
|
|
||||||
if (e is! UnsubscribedException) {
|
|
||||||
ErrorHandler.logError(
|
|
||||||
e: e,
|
|
||||||
s: s,
|
|
||||||
data: {
|
|
||||||
'text': widget.text,
|
|
||||||
'textLanguageCode': widget.textLanguage.langCode,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
widget.onTranscriptionFetched?.call();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleAudioTap() async {
|
Future<void> _handleAudioTap() async {
|
||||||
if (_isPlaying) {
|
if (_isPlaying) {
|
||||||
|
|
@ -156,14 +79,15 @@ class _PhoneticTranscriptionWidgetState
|
||||||
link: MatrixState.pAnyState
|
link: MatrixState.pAnyState
|
||||||
.layerLinkAndKey("phonetic-transcription-${widget.text}")
|
.layerLinkAndKey("phonetic-transcription-${widget.text}")
|
||||||
.link,
|
.link,
|
||||||
child: Row(
|
child: PhoneticTranscriptionBuilder(
|
||||||
key: MatrixState.pAnyState
|
key: MatrixState.pAnyState
|
||||||
.layerLinkAndKey("phonetic-transcription-${widget.text}")
|
.layerLinkAndKey("phonetic-transcription-${widget.text}")
|
||||||
.key,
|
.key,
|
||||||
mainAxisSize: MainAxisSize.min,
|
textLanguage: widget.textLanguage,
|
||||||
children: [
|
text: widget.text,
|
||||||
if (_error != null)
|
builder: (context, controller) {
|
||||||
_error is UnsubscribedException
|
if (controller.isError) {
|
||||||
|
return controller.error is UnsubscribedException
|
||||||
? ErrorIndicator(
|
? ErrorIndicator(
|
||||||
message: L10n.of(context)
|
message: L10n.of(context)
|
||||||
.subscribeToUnlockTranscriptions,
|
.subscribeToUnlockTranscriptions,
|
||||||
|
|
@ -176,37 +100,44 @@ class _PhoneticTranscriptionWidgetState
|
||||||
: ErrorIndicator(
|
: ErrorIndicator(
|
||||||
message:
|
message:
|
||||||
L10n.of(context).failedToFetchTranscription,
|
L10n.of(context).failedToFetchTranscription,
|
||||||
)
|
);
|
||||||
else if (_isLoading || _transcription == null)
|
}
|
||||||
const SizedBox(
|
|
||||||
|
if (controller.isLoading ||
|
||||||
|
controller.transcription == null) {
|
||||||
|
return const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator.adaptive(),
|
child: CircularProgressIndicator.adaptive(),
|
||||||
)
|
);
|
||||||
else
|
}
|
||||||
Flexible(
|
|
||||||
child: Text(
|
return Row(
|
||||||
_transcription!,
|
spacing: 8.0,
|
||||||
textScaler: TextScaler.noScaling,
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: widget.style ??
|
children: [
|
||||||
Theme.of(context).textTheme.bodyMedium,
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
controller.transcription!,
|
||||||
|
textScaler: TextScaler.noScaling,
|
||||||
|
style: widget.style ??
|
||||||
|
Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Tooltip(
|
||||||
if (_transcription != null && _error == null)
|
message: _isPlaying
|
||||||
const SizedBox(width: 8),
|
? L10n.of(context).stop
|
||||||
if (_transcription != null && _error == null)
|
: L10n.of(context).playAudio,
|
||||||
Tooltip(
|
child: Icon(
|
||||||
message: _isPlaying
|
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
|
||||||
? L10n.of(context).stop
|
size: widget.iconSize ?? 24,
|
||||||
: L10n.of(context).playAudio,
|
color: widget.iconColor ??
|
||||||
child: Icon(
|
Theme.of(context).iconTheme.color,
|
||||||
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
|
),
|
||||||
size: widget.iconSize ?? 24,
|
|
||||||
color: widget.iconColor ??
|
|
||||||
Theme.of(context).iconTheme.color,
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:async/async.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||||
|
|
@ -35,15 +37,20 @@ class EmojiActivityGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Future<LemmaInfoResponse>> lemmaInfoFutures = missingEmojis
|
final List<Future<Result<LemmaInfoResponse>>> lemmaInfoFutures =
|
||||||
.map((token) => token.vocabConstructID.getLemmaInfo())
|
missingEmojis
|
||||||
.toList();
|
.map((token) => token.vocabConstructID.getLemmaInfo())
|
||||||
|
.toList();
|
||||||
|
|
||||||
final List<LemmaInfoResponse> lemmaInfos =
|
final List<Result<LemmaInfoResponse>> lemmaInfos =
|
||||||
await Future.wait(lemmaInfoFutures);
|
await Future.wait(lemmaInfoFutures);
|
||||||
|
|
||||||
for (int i = 0; i < missingEmojis.length; i++) {
|
for (int i = 0; i < missingEmojis.length; i++) {
|
||||||
final e = lemmaInfos[i].emoji.firstWhere(
|
if (lemmaInfos[i].isError) {
|
||||||
|
throw lemmaInfos[i].asError!.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
final e = lemmaInfos[i].asValue!.value.emoji.firstWhere(
|
||||||
(e) => !usedEmojis.contains(e),
|
(e) => !usedEmojis.contains(e),
|
||||||
orElse: () => throw Exception(
|
orElse: () => throw Exception(
|
||||||
"Not enough unique emojis for tokens in message",
|
"Not enough unique emojis for tokens in message",
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,34 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.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/activity_type_enum.dart';
|
||||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.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_activity_model.dart';
|
||||||
import 'package:fluffychat/pangea/practice_activities/practice_match.dart';
|
import 'package:fluffychat/pangea/practice_activities/practice_match.dart';
|
||||||
|
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||||
|
|
||||||
class LemmaMeaningActivityGenerator {
|
class LemmaMeaningActivityGenerator {
|
||||||
static Future<MessageActivityResponse> get(
|
static Future<MessageActivityResponse> get(
|
||||||
MessageActivityRequest req,
|
MessageActivityRequest req,
|
||||||
) async {
|
) async {
|
||||||
final List<Future<LemmaInfoResponse>> lemmaInfoFutures = req.targetTokens
|
final List<Future<Result<LemmaInfoResponse>>> lemmaInfoFutures = req
|
||||||
|
.targetTokens
|
||||||
.map((token) => token.vocabConstructID.getLemmaInfo())
|
.map((token) => token.vocabConstructID.getLemmaInfo())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final List<LemmaInfoResponse> lemmaInfos =
|
final List<Result<LemmaInfoResponse>> lemmaInfos =
|
||||||
await Future.wait(lemmaInfoFutures);
|
await Future.wait(lemmaInfoFutures);
|
||||||
|
|
||||||
|
if (lemmaInfos.any((result) => result.isError)) {
|
||||||
|
throw lemmaInfos.firstWhere((result) => result.isError).error!;
|
||||||
|
}
|
||||||
|
|
||||||
final Map<ConstructForm, List<String>> matchInfo = Map.fromIterables(
|
final Map<ConstructForm, List<String>> matchInfo = Map.fromIterables(
|
||||||
req.targetTokens.map((token) => token.vocabForm),
|
req.targetTokens.map((token) => token.vocabForm),
|
||||||
lemmaInfos.map((lemmaInfo) => [lemmaInfo.meaning]),
|
lemmaInfos.map((lemmaInfo) => [lemmaInfo.asValue!.value.meaning]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return MessageActivityResponse(
|
return MessageActivityResponse(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
|
@ -69,6 +70,8 @@ class PracticeRepo {
|
||||||
|
|
||||||
_setCached(req, res);
|
_setCached(req, res);
|
||||||
return Result.value(res.activity);
|
return Result.value(res.activity);
|
||||||
|
} on HttpException catch (e, s) {
|
||||||
|
return Result.error(e, s);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
ErrorHandler.logError(
|
ErrorHandler.logError(
|
||||||
e: e,
|
e: e,
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'
|
||||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||||
import 'package:fluffychat/pangea/languages/language_arc_model.dart';
|
import 'package:fluffychat/pangea/languages/language_arc_model.dart';
|
||||||
import 'package:fluffychat/pangea/languages/p_language_store.dart';
|
import 'package:fluffychat/pangea/languages/p_language_store.dart';
|
||||||
|
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
|
|
||||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart';
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart';
|
||||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
||||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart';
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart';
|
||||||
|
|
@ -115,19 +115,11 @@ class TokenInfoFeedbackDialog extends StatelessWidget {
|
||||||
Future<void> _updateLemmaInfo(
|
Future<void> _updateLemmaInfo(
|
||||||
PangeaToken token,
|
PangeaToken token,
|
||||||
LemmaInfoResponse response,
|
LemmaInfoResponse response,
|
||||||
) async {
|
) =>
|
||||||
final construct = token.vocabConstructID;
|
LemmaInfoRepo.set(
|
||||||
|
token.vocabConstructID.lemmaInfoRequest,
|
||||||
final currentLemmaInfo = construct.userLemmaInfo;
|
response,
|
||||||
final updatedLemmaInfo = UserSetLemmaInfo(
|
);
|
||||||
meaning: response.meaning,
|
|
||||||
emojis: response.emoji,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentLemmaInfo != updatedLemmaInfo) {
|
|
||||||
await construct.setUserLemmaInfo(updatedLemmaInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updatePhoneticTranscription(
|
Future<void> _updatePhoneticTranscription(
|
||||||
PhoneticTranscriptionResponse response,
|
PhoneticTranscriptionResponse response,
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ class TokenInfoFeedbackRequestData {
|
||||||
final String detectedLanguage;
|
final String detectedLanguage;
|
||||||
final List<PangeaToken> tokens;
|
final List<PangeaToken> tokens;
|
||||||
final int selectedToken;
|
final int selectedToken;
|
||||||
final LemmaInfoResponse? lemmaInfo;
|
final LemmaInfoResponse lemmaInfo;
|
||||||
final String? phonetics;
|
final String phonetics;
|
||||||
final String wordCardL1;
|
final String wordCardL1;
|
||||||
|
|
||||||
TokenInfoFeedbackRequestData({
|
TokenInfoFeedbackRequestData({
|
||||||
|
|
@ -19,8 +19,8 @@ class TokenInfoFeedbackRequestData {
|
||||||
required this.detectedLanguage,
|
required this.detectedLanguage,
|
||||||
required this.tokens,
|
required this.tokens,
|
||||||
required this.selectedToken,
|
required this.selectedToken,
|
||||||
this.lemmaInfo,
|
required this.lemmaInfo,
|
||||||
this.phonetics,
|
required this.phonetics,
|
||||||
required this.wordCardL1,
|
required this.wordCardL1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ class TokenInfoFeedbackRequest {
|
||||||
'detected_language': data.detectedLanguage,
|
'detected_language': data.detectedLanguage,
|
||||||
'tokens': data.tokens.map((token) => token.toJson()).toList(),
|
'tokens': data.tokens.map((token) => token.toJson()).toList(),
|
||||||
'selected_token': data.selectedToken,
|
'selected_token': data.selectedToken,
|
||||||
'lemma_info': data.lemmaInfo?.toJson(),
|
'lemma_info': data.lemmaInfo.toJson(),
|
||||||
'phonetics': data.phonetics,
|
'phonetics': data.phonetics,
|
||||||
'user_feedback': userFeedback,
|
'user_feedback': userFeedback,
|
||||||
'word_card_l1': data.wordCardL1,
|
'word_card_l1': data.wordCardL1,
|
||||||
|
|
|
||||||
72
lib/pangea/toolbar/word_card/lemma_meaning_display.dart
Normal file
72
lib/pangea/toolbar/word_card/lemma_meaning_display.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
|
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||||
|
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||||
|
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
|
||||||
|
|
||||||
|
class LemmaMeaningDisplay extends StatelessWidget {
|
||||||
|
final String langCode;
|
||||||
|
final ConstructIdentifier constructId;
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const LemmaMeaningDisplay({
|
||||||
|
super.key,
|
||||||
|
required this.langCode,
|
||||||
|
required this.constructId,
|
||||||
|
required this.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LemmaMeaningBuilder(
|
||||||
|
langCode: langCode,
|
||||||
|
constructId: constructId,
|
||||||
|
builder: (context, controller) {
|
||||||
|
if (controller.isError) {
|
||||||
|
return ErrorIndicator(
|
||||||
|
message: L10n.of(context).errorFetchingDefinition,
|
||||||
|
style: const TextStyle(fontSize: 14.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.isLoading || controller.lemmaInfo == null) {
|
||||||
|
return const CircularProgressIndicator.adaptive();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constructId.lemma.toLowerCase() == text.toLowerCase()) {
|
||||||
|
return Text(
|
||||||
|
controller.lemmaInfo!.meaning,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: DefaultTextStyle.of(context).style.copyWith(
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: constructId.lemma,
|
||||||
|
),
|
||||||
|
const WidgetSpan(
|
||||||
|
child: SizedBox(width: 8.0),
|
||||||
|
),
|
||||||
|
const TextSpan(text: ":"),
|
||||||
|
const WidgetSpan(
|
||||||
|
child: SizedBox(width: 8.0),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: controller.lemmaInfo!.meaning,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,21 +4,26 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||||
|
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||||
|
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
|
||||||
import 'package:fluffychat/pangea/toolbar/reading_assistance/lemma_emoji_picker.dart';
|
import 'package:fluffychat/pangea/toolbar/reading_assistance/lemma_emoji_picker.dart';
|
||||||
|
|
||||||
class LemmaReactionPicker extends StatelessWidget {
|
class LemmaReactionPicker extends StatelessWidget {
|
||||||
final List<String> emojis;
|
|
||||||
final bool loading;
|
|
||||||
final Event? event;
|
final Event? event;
|
||||||
|
final ConstructIdentifier construct;
|
||||||
|
final String langCode;
|
||||||
|
|
||||||
const LemmaReactionPicker({
|
const LemmaReactionPicker({
|
||||||
super.key,
|
super.key,
|
||||||
required this.emojis,
|
required this.construct,
|
||||||
required this.loading,
|
required this.langCode,
|
||||||
required this.event,
|
this.event,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> setEmoji(String emoji) async {
|
Future<void> setEmoji(
|
||||||
|
String emoji,
|
||||||
|
List<String> emojis,
|
||||||
|
) async {
|
||||||
if (event?.room.timeline == null) {
|
if (event?.room.timeline == null) {
|
||||||
throw Exception("Timeline is null in reaction picker");
|
throw Exception("Timeline is null in reaction picker");
|
||||||
}
|
}
|
||||||
|
|
@ -63,33 +68,44 @@ class LemmaReactionPicker extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sentReactions = <String>{};
|
return LemmaMeaningBuilder(
|
||||||
if (event?.room.timeline != null) {
|
langCode: langCode,
|
||||||
sentReactions.addAll(
|
constructId: construct,
|
||||||
event!
|
builder: (context, controller) {
|
||||||
.aggregatedEvents(
|
final sentReactions = <String>{};
|
||||||
event!.room.timeline!,
|
if (event?.room.timeline != null) {
|
||||||
RelationshipTypes.reaction,
|
sentReactions.addAll(
|
||||||
)
|
event!
|
||||||
.where(
|
.aggregatedEvents(
|
||||||
(event) =>
|
event!.room.timeline!,
|
||||||
event.senderId == event.room.client.userID &&
|
RelationshipTypes.reaction,
|
||||||
event.type == 'm.reaction',
|
)
|
||||||
)
|
.where(
|
||||||
.map(
|
(event) =>
|
||||||
(event) => event.content
|
event.senderId == event.room.client.userID &&
|
||||||
.tryGetMap<String, Object?>('m.relates_to')
|
event.type == 'm.reaction',
|
||||||
?.tryGet<String>('key'),
|
)
|
||||||
)
|
.map(
|
||||||
.whereType<String>(),
|
(event) => event.content
|
||||||
);
|
.tryGetMap<String, Object?>('m.relates_to')
|
||||||
}
|
?.tryGet<String>('key'),
|
||||||
|
)
|
||||||
|
.whereType<String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return LemmaEmojiPicker(
|
return LemmaEmojiPicker(
|
||||||
emojis: emojis,
|
emojis: controller.lemmaInfo?.emoji ?? [],
|
||||||
onSelect: event?.room.timeline != null ? setEmoji : null,
|
onSelect: event?.room.timeline != null
|
||||||
disabled: (emoji) => sentReactions.contains(emoji),
|
? (emoji) => setEmoji(
|
||||||
loading: loading,
|
emoji,
|
||||||
|
controller.lemmaInfo?.emoji ?? [],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
disabled: (emoji) => sentReactions.contains(emoji),
|
||||||
|
loading: controller.isLoading,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
|
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||||
import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart';
|
import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart';
|
||||||
import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart';
|
import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart';
|
||||||
import 'package:fluffychat/pangea/toolbar/word_card/word_zoom_widget.dart';
|
import 'package:fluffychat/pangea/toolbar/word_card/word_zoom_widget.dart';
|
||||||
|
|
@ -50,7 +51,7 @@ class ReadingAssistanceContent extends StatelessWidget {
|
||||||
onClose: () => overlayController.updateSelectedSpan(null),
|
onClose: () => overlayController.updateSelectedSpan(null),
|
||||||
langCode: overlayController.pangeaMessageEvent.messageDisplayLangCode,
|
langCode: overlayController.pangeaMessageEvent.messageDisplayLangCode,
|
||||||
onDismissNewWordOverlay: () => overlayController.setState(() {}),
|
onDismissNewWordOverlay: () => overlayController.setState(() {}),
|
||||||
onFlagTokenInfo: () {
|
onFlagTokenInfo: (LemmaInfoResponse lemmaInfo, String phonetics) {
|
||||||
if (selectedTokenIndex < 0) return;
|
if (selectedTokenIndex < 0) return;
|
||||||
final requestData = TokenInfoFeedbackRequestData(
|
final requestData = TokenInfoFeedbackRequestData(
|
||||||
userId: Matrix.of(context).client.userID!,
|
userId: Matrix.of(context).client.userID!,
|
||||||
|
|
@ -61,6 +62,8 @@ class ReadingAssistanceContent extends StatelessWidget {
|
||||||
tokens: tokens ?? [],
|
tokens: tokens ?? [],
|
||||||
selectedToken: selectedTokenIndex,
|
selectedToken: selectedTokenIndex,
|
||||||
wordCardL1: MatrixState.pangeaController.userController.userL1Code!,
|
wordCardL1: MatrixState.pangeaController.userController.userL1Code!,
|
||||||
|
lemmaInfo: lemmaInfo,
|
||||||
|
phonetics: phonetics,
|
||||||
);
|
);
|
||||||
overlayController.widget.chatController.showTokenFeedbackDialog(
|
overlayController.widget.chatController.showTokenFeedbackDialog(
|
||||||
requestData,
|
requestData,
|
||||||
|
|
|
||||||
62
lib/pangea/toolbar/word_card/token_feedback_button.dart
Normal file
62
lib/pangea/toolbar/word_card/token_feedback_button.dart
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
|
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||||
|
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||||
|
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||||
|
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
|
||||||
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_builder.dart';
|
||||||
|
|
||||||
|
class TokenFeedbackButton extends StatelessWidget {
|
||||||
|
final LanguageModel textLanguage;
|
||||||
|
final ConstructIdentifier constructId;
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
final Function(LemmaInfoResponse, String) onFlagTokenInfo;
|
||||||
|
|
||||||
|
const TokenFeedbackButton({
|
||||||
|
super.key,
|
||||||
|
required this.textLanguage,
|
||||||
|
required this.constructId,
|
||||||
|
required this.text,
|
||||||
|
required this.onFlagTokenInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LemmaMeaningBuilder(
|
||||||
|
langCode: textLanguage.langCode,
|
||||||
|
constructId: constructId,
|
||||||
|
builder: (context, lemmaController) {
|
||||||
|
return PhoneticTranscriptionBuilder(
|
||||||
|
textLanguage: textLanguage,
|
||||||
|
text: text,
|
||||||
|
builder: (context, transcriptController) {
|
||||||
|
final enabled = (lemmaController.lemmaInfo != null ||
|
||||||
|
lemmaController.isError) &&
|
||||||
|
(transcriptController.transcription != null ||
|
||||||
|
transcriptController.isError);
|
||||||
|
|
||||||
|
final lemmaInfo =
|
||||||
|
lemmaController.lemmaInfo ?? LemmaInfoResponse.error;
|
||||||
|
|
||||||
|
final transcript = transcriptController.transcription ?? 'ERROR';
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.flag_outlined),
|
||||||
|
onPressed: enabled
|
||||||
|
? () {
|
||||||
|
onFlagTokenInfo(
|
||||||
|
lemmaInfo,
|
||||||
|
transcript,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
tooltip: enabled ? L10n.of(context).reportWordIssueTooltip : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,18 +3,18 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/l10n/l10n.dart';
|
|
||||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
|
||||||
import 'package:fluffychat/pangea/common/widgets/word_audio_button.dart';
|
import 'package:fluffychat/pangea/common/widgets/word_audio_button.dart';
|
||||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||||
import 'package:fluffychat/pangea/languages/language_model.dart';
|
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||||
import 'package:fluffychat/pangea/languages/p_language_store.dart';
|
import 'package:fluffychat/pangea/languages/p_language_store.dart';
|
||||||
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
|
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
|
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
|
||||||
import 'package:fluffychat/pangea/toolbar/reading_assistance/new_word_overlay.dart';
|
import 'package:fluffychat/pangea/toolbar/reading_assistance/new_word_overlay.dart';
|
||||||
|
import 'package:fluffychat/pangea/toolbar/word_card/lemma_meaning_display.dart';
|
||||||
import 'package:fluffychat/pangea/toolbar/word_card/lemma_reaction_picker.dart';
|
import 'package:fluffychat/pangea/toolbar/word_card/lemma_reaction_picker.dart';
|
||||||
import 'package:fluffychat/pangea/toolbar/word_card/message_unsubscribed_card.dart';
|
import 'package:fluffychat/pangea/toolbar/word_card/message_unsubscribed_card.dart';
|
||||||
|
import 'package:fluffychat/pangea/toolbar/word_card/token_feedback_button.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
class WordZoomWidget extends StatelessWidget {
|
class WordZoomWidget extends StatelessWidget {
|
||||||
|
|
@ -28,10 +28,7 @@ class WordZoomWidget extends StatelessWidget {
|
||||||
final Event? event;
|
final Event? event;
|
||||||
|
|
||||||
final VoidCallback? onDismissNewWordOverlay;
|
final VoidCallback? onDismissNewWordOverlay;
|
||||||
final VoidCallback? onFlagTokenInfo;
|
final Function(LemmaInfoResponse, String)? onFlagTokenInfo;
|
||||||
|
|
||||||
// final TokenInfoFeedbackRequestData? requestData;
|
|
||||||
// final PangeaMessageEvent? pangeaMessageEvent;
|
|
||||||
|
|
||||||
const WordZoomWidget({
|
const WordZoomWidget({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -55,6 +52,8 @@ class WordZoomWidget extends StatelessWidget {
|
||||||
final bool? subscribed =
|
final bool? subscribed =
|
||||||
MatrixState.pangeaController.subscriptionController.isSubscribed;
|
MatrixState.pangeaController.subscriptionController.isSubscribed;
|
||||||
final overlayColor = Theme.of(context).scaffoldBackgroundColor;
|
final overlayColor = Theme.of(context).scaffoldBackgroundColor;
|
||||||
|
final showTranscript =
|
||||||
|
MatrixState.pangeaController.userController.showTranscription;
|
||||||
|
|
||||||
final Widget content = subscribed != null && !subscribed
|
final Widget content = subscribed != null && !subscribed
|
||||||
? const MessageUnsubscribedCard()
|
? const MessageUnsubscribedCard()
|
||||||
|
|
@ -106,11 +105,14 @@ class WordZoomWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onFlagTokenInfo != null
|
onFlagTokenInfo != null
|
||||||
? IconButton(
|
? TokenFeedbackButton(
|
||||||
icon: const Icon(Icons.flag_outlined),
|
textLanguage: PLanguageStore.byLangCode(
|
||||||
onPressed: onFlagTokenInfo,
|
langCode,
|
||||||
tooltip:
|
) ??
|
||||||
L10n.of(context).reportWordIssueTooltip,
|
LanguageModel.unknown,
|
||||||
|
constructId: construct,
|
||||||
|
text: token.content,
|
||||||
|
onFlagTokenInfo: onFlagTokenInfo!,
|
||||||
)
|
)
|
||||||
: const SizedBox(
|
: const SizedBox(
|
||||||
width: 40.0,
|
width: 40.0,
|
||||||
|
|
@ -118,17 +120,12 @@ class WordZoomWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
LemmaMeaningBuilder(
|
Column(
|
||||||
langCode: langCode,
|
spacing: 12.0,
|
||||||
constructId: construct,
|
mainAxisSize: MainAxisSize.min,
|
||||||
builder: (context, controller) {
|
children: [
|
||||||
return Column(
|
showTranscript
|
||||||
spacing: 12.0,
|
? PhoneticTranscriptionWidget(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (MatrixState.pangeaController.userController
|
|
||||||
.showTranscription)
|
|
||||||
PhoneticTranscriptionWidget(
|
|
||||||
text: token.content,
|
text: token.content,
|
||||||
textLanguage: PLanguageStore.byLangCode(
|
textLanguage: PLanguageStore.byLangCode(
|
||||||
langCode,
|
langCode,
|
||||||
|
|
@ -137,62 +134,23 @@ class WordZoomWidget extends StatelessWidget {
|
||||||
style: const TextStyle(fontSize: 14.0),
|
style: const TextStyle(fontSize: 14.0),
|
||||||
iconSize: 24.0,
|
iconSize: 24.0,
|
||||||
)
|
)
|
||||||
else
|
: WordAudioButton(
|
||||||
WordAudioButton(
|
|
||||||
text: token.content,
|
text: token.content,
|
||||||
uniqueID: "lemma-content-${token.content}",
|
uniqueID: "lemma-content-${token.content}",
|
||||||
langCode: langCode,
|
langCode: langCode,
|
||||||
iconSize: 24.0,
|
iconSize: 24.0,
|
||||||
),
|
),
|
||||||
LemmaReactionPicker(
|
LemmaReactionPicker(
|
||||||
emojis: controller.lemmaInfo?.emoji ?? [],
|
construct: construct,
|
||||||
loading: controller.isLoading,
|
langCode: langCode,
|
||||||
event: event,
|
event: event,
|
||||||
),
|
),
|
||||||
if (controller.error != null)
|
LemmaMeaningDisplay(
|
||||||
ErrorIndicator(
|
langCode: langCode,
|
||||||
message: L10n.of(context)
|
constructId: construct,
|
||||||
.errorFetchingDefinition,
|
text: token.content,
|
||||||
style: const TextStyle(fontSize: 14.0),
|
),
|
||||||
)
|
],
|
||||||
else if (controller.isLoading ||
|
|
||||||
controller.lemmaInfo == null)
|
|
||||||
const CircularProgressIndicator.adaptive()
|
|
||||||
else
|
|
||||||
construct.lemma.toLowerCase() ==
|
|
||||||
token.content.toLowerCase()
|
|
||||||
? Text(
|
|
||||||
controller.lemmaInfo!.meaning,
|
|
||||||
style:
|
|
||||||
const TextStyle(fontSize: 14.0),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
)
|
|
||||||
: RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
style: DefaultTextStyle.of(context)
|
|
||||||
.style
|
|
||||||
.copyWith(
|
|
||||||
fontSize: 14.0,
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
TextSpan(text: construct.lemma),
|
|
||||||
const WidgetSpan(
|
|
||||||
child: SizedBox(width: 8.0),
|
|
||||||
),
|
|
||||||
const TextSpan(text: ":"),
|
|
||||||
const WidgetSpan(
|
|
||||||
child: SizedBox(width: 8.0),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: controller
|
|
||||||
.lemmaInfo!.meaning,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue