Merge branch 'main' into 819-cant-type-with-it-bar
This commit is contained in:
commit
1cc4551e12
32 changed files with 434 additions and 429 deletions
|
|
@ -23,6 +23,8 @@ abstract class AppConfig {
|
|||
static const bool allowOtherHomeservers = true;
|
||||
static const bool enableRegistration = true;
|
||||
static const double toolbarMaxHeight = 300.0;
|
||||
static const double toolbarMinHeight = 70.0;
|
||||
static const double toolbarMinWidth = 270.0;
|
||||
// #Pangea
|
||||
// static const Color primaryColor = Color(0xFF5625BA);
|
||||
// static const Color primaryColorLight = Color(0xFFCCBDEA);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
final Event? event;
|
||||
final PangeaAudioFile? matrixFile;
|
||||
final bool autoplay;
|
||||
final Function(bool)? setIsPlayingAudio;
|
||||
// Pangea#
|
||||
|
||||
static String? currentId;
|
||||
|
|
@ -41,6 +42,7 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
this.autoplay = false,
|
||||
this.sectionStartMS,
|
||||
this.sectionEndMS,
|
||||
this.setIsPlayingAudio,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -204,8 +206,13 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
if (max == null || max == Duration.zero) return;
|
||||
setState(() => maxPosition = max.inMilliseconds.toDouble());
|
||||
});
|
||||
onPlayerStateChanged ??=
|
||||
audioPlayer.playingStream.listen((_) => setState(() {}));
|
||||
onPlayerStateChanged ??= audioPlayer.playingStream.listen(
|
||||
(isPlaying) => setState(() {
|
||||
// #Pangea
|
||||
widget.setIsPlayingAudio?.call(isPlaying);
|
||||
// Pangea#
|
||||
}),
|
||||
);
|
||||
final audioFile = this.audioFile;
|
||||
if (audioFile != null) {
|
||||
audioPlayer.setFilePath(audioFile.path);
|
||||
|
|
@ -467,7 +474,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
height: 32 * (waveform[i] / 1024),
|
||||
width: 1.5,
|
||||
width: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import 'dart:async';
|
||||
|
||||
class BaseController<T> {
|
||||
final StreamController<T> stateListener = StreamController<T>();
|
||||
final StreamController<T> _stateListener = StreamController<T>();
|
||||
late Stream<T> stateStream;
|
||||
|
||||
BaseController() {
|
||||
stateStream = stateListener.stream.asBroadcastStream();
|
||||
stateStream = _stateListener.stream.asBroadcastStream();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
stateListener.close();
|
||||
_stateListener.close();
|
||||
}
|
||||
|
||||
setState(T data) {
|
||||
stateListener.add(data);
|
||||
_stateListener.add(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
class GetAnalyticsController {
|
||||
late PangeaController _pangeaController;
|
||||
final List<AnalyticsCacheEntry> _cache = [];
|
||||
StreamSubscription<AnalyticsUpdateType>? _analyticsUpdateSubscription;
|
||||
StreamSubscription<AnalyticsUpdate>? _analyticsUpdateSubscription;
|
||||
CachedStreamController<List<OneConstructUse>> analyticsStream =
|
||||
CachedStreamController<List<OneConstructUse>>();
|
||||
|
||||
|
|
@ -87,8 +87,9 @@ class GetAnalyticsController {
|
|||
prevXP = null;
|
||||
}
|
||||
|
||||
Future<void> onAnalyticsUpdate(AnalyticsUpdateType type) async {
|
||||
if (type == AnalyticsUpdateType.server) {
|
||||
Future<void> onAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async {
|
||||
if (analyticsUpdate.isLogout) return;
|
||||
if (analyticsUpdate.type == AnalyticsUpdateType.server) {
|
||||
await getConstructs(forceUpdate: true);
|
||||
}
|
||||
updateAnalyticsStream();
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ enum AnalyticsUpdateType { server, local }
|
|||
/// 2) constructs used by the user, both in sending messages and doing practice activities
|
||||
class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
||||
late PangeaController _pangeaController;
|
||||
CachedStreamController<AnalyticsUpdateType> analyticsUpdateStream =
|
||||
CachedStreamController<AnalyticsUpdateType>();
|
||||
CachedStreamController<AnalyticsUpdate> analyticsUpdateStream =
|
||||
CachedStreamController<AnalyticsUpdate>();
|
||||
StreamSubscription<AnalyticsStream>? _analyticsStream;
|
||||
Timer? _updateTimer;
|
||||
|
||||
|
|
@ -237,7 +237,9 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
final int newLevel = _pangeaController.analytics.level;
|
||||
newLevel > prevLevel
|
||||
? sendLocalAnalyticsToAnalyticsRoom()
|
||||
: analyticsUpdateStream.add(AnalyticsUpdateType.local);
|
||||
: analyticsUpdateStream.add(
|
||||
AnalyticsUpdate(AnalyticsUpdateType.local),
|
||||
);
|
||||
}
|
||||
|
||||
/// Clears the local cache of recently sent constructs. Called before updating analytics
|
||||
|
|
@ -281,7 +283,9 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
/// for the completion of the previous update and returns. Otherwise, it creates a new [_updateCompleter] and
|
||||
/// proceeds with the update process. If the update is successful, it clears any messages that were received
|
||||
/// since the last update and notifies the [analyticsUpdateStream].
|
||||
Future<void> sendLocalAnalyticsToAnalyticsRoom() async {
|
||||
Future<void> sendLocalAnalyticsToAnalyticsRoom({
|
||||
onLogout = false,
|
||||
}) async {
|
||||
if (_pangeaController.matrixState.client.userID == null) return;
|
||||
if (!(_updateCompleter?.isCompleted ?? true)) {
|
||||
await _updateCompleter!.future;
|
||||
|
|
@ -293,7 +297,12 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
clearMessagesSinceUpdate();
|
||||
|
||||
lastUpdated = DateTime.now();
|
||||
analyticsUpdateStream.add(AnalyticsUpdateType.server);
|
||||
analyticsUpdateStream.add(
|
||||
AnalyticsUpdate(
|
||||
AnalyticsUpdateType.server,
|
||||
isLogout: onLogout,
|
||||
),
|
||||
);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
|
|
@ -340,3 +349,10 @@ class AnalyticsStream {
|
|||
required this.constructs,
|
||||
});
|
||||
}
|
||||
|
||||
class AnalyticsUpdate {
|
||||
final AnalyticsUpdateType type;
|
||||
final bool isLogout;
|
||||
|
||||
AnalyticsUpdate(this.type, {this.isLogout = false});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
import 'dart:collection';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
|
|
@ -26,66 +25,60 @@ class PracticeActivityRecordController {
|
|||
static const int maxStoredEvents = 100;
|
||||
static final Map<int, _RecordCacheItem> _cache = {};
|
||||
late final PangeaController _pangeaController;
|
||||
Timer? _cacheClearTimer;
|
||||
|
||||
PracticeActivityRecordController(this._pangeaController) {
|
||||
_initializeCacheClearing();
|
||||
PracticeActivityRecordController(this._pangeaController);
|
||||
|
||||
int getCompletedActivityCount(String messageID) {
|
||||
return _completedActivities[messageID] ?? 0;
|
||||
}
|
||||
|
||||
LinkedHashMap<String, int> get completedActivities {
|
||||
try {
|
||||
final dynamic locallySaved = _pangeaController.pStoreService.read(
|
||||
PLocalKey.completedActivities,
|
||||
);
|
||||
if (locallySaved == null) return LinkedHashMap<String, int>();
|
||||
try {
|
||||
final LinkedHashMap<String, int> cache =
|
||||
LinkedHashMap<String, int>.from(locallySaved);
|
||||
return cache;
|
||||
} catch (err) {
|
||||
_pangeaController.pStoreService.delete(
|
||||
PLocalKey.completedActivities,
|
||||
);
|
||||
return LinkedHashMap<String, int>();
|
||||
}
|
||||
} catch (exception, stackTrace) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError(
|
||||
"Failed to get completed activities from cache: $exception",
|
||||
),
|
||||
s: stackTrace,
|
||||
m: 'Failed to get completed activities from cache',
|
||||
);
|
||||
return LinkedHashMap<String, int>();
|
||||
}
|
||||
}
|
||||
final LinkedHashMap<String, int> _completedActivities =
|
||||
LinkedHashMap<String, int>();
|
||||
|
||||
// LinkedHashMap<String, int> get _completedActivities {
|
||||
// try {
|
||||
// final dynamic locallySaved = _pangeaController.pStoreService.read(
|
||||
// PLocalKey.completedActivities,
|
||||
// );
|
||||
// if (locallySaved == null) return LinkedHashMap<String, int>();
|
||||
// try {
|
||||
// final LinkedHashMap<String, int> cache =
|
||||
// LinkedHashMap<String, int>.from(locallySaved);
|
||||
// return cache;
|
||||
// } catch (err) {
|
||||
// _pangeaController.pStoreService.delete(
|
||||
// PLocalKey.completedActivities,
|
||||
// );
|
||||
// return LinkedHashMap<String, int>();
|
||||
// }
|
||||
// } catch (exception, stackTrace) {
|
||||
// ErrorHandler.logError(
|
||||
// e: PangeaWarningError(
|
||||
// "Failed to get completed activities from cache: $exception",
|
||||
// ),
|
||||
// s: stackTrace,
|
||||
// m: 'Failed to get completed activities from cache',
|
||||
// );
|
||||
// return LinkedHashMap<String, int>();
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<void> completeActivity(String messageID) async {
|
||||
final LinkedHashMap<String, int> currentCache = completedActivities;
|
||||
final numCompleted = currentCache[messageID] ?? 0;
|
||||
currentCache[messageID] = numCompleted + 1;
|
||||
final numCompleted = _completedActivities[messageID] ?? 0;
|
||||
_completedActivities[messageID] = numCompleted + 1;
|
||||
// final LinkedHashMap<String, int> currentCache = _completedActivities;
|
||||
// final numCompleted = currentCache[messageID] ?? 0;
|
||||
// currentCache[messageID] = numCompleted + 1;
|
||||
|
||||
if (currentCache.length > maxStoredEvents) {
|
||||
currentCache.remove(currentCache.keys.first);
|
||||
}
|
||||
// if (currentCache.length > maxStoredEvents) {
|
||||
// currentCache.remove(currentCache.keys.first);
|
||||
// }
|
||||
|
||||
await _pangeaController.pStoreService.save(
|
||||
PLocalKey.completedActivities,
|
||||
currentCache,
|
||||
);
|
||||
}
|
||||
|
||||
void _initializeCacheClearing() {
|
||||
const duration = Duration(minutes: 2);
|
||||
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
|
||||
}
|
||||
|
||||
void _clearCache() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_cacheClearTimer?.cancel();
|
||||
// await _pangeaController.pStoreService.save(
|
||||
// PLocalKey.completedActivities,
|
||||
// currentCache,
|
||||
// );
|
||||
debugPrint("completed activities is now: $_completedActivities");
|
||||
}
|
||||
|
||||
/// Sends a practice activity record to the server and returns the corresponding event.
|
||||
|
|
|
|||
|
|
@ -121,19 +121,26 @@ class UserController extends BaseController {
|
|||
/// Initializes the user's profile by waiting for account data to load, reading in account
|
||||
/// data to profile, and migrating from the pangea profile if the account data is not present.
|
||||
Future<void> _initialize() async {
|
||||
// wait for account data to load
|
||||
// as long as it's not null, then this we've already migrated the profile
|
||||
await _pangeaController.matrixState.client.waitForAccountData();
|
||||
if (profile.userSettings.dateOfBirth != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we used to store the user's profile in the pangea server
|
||||
// we now store it in the matrix account data
|
||||
final PangeaProfileResponse? resp = await PUserRepo.fetchPangeaUserInfo(
|
||||
userID: userId!,
|
||||
matrixAccessToken: _matrixAccessToken!,
|
||||
);
|
||||
|
||||
// if it's null, we don't have a profile in the pangea server
|
||||
if (resp?.profile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if we have a profile in the pangea server, we need to migrate it to the matrix account data
|
||||
final userSetting = UserSettings.fromJson(resp!.profile.toJson());
|
||||
final newProfile = Profile(userSettings: userSetting);
|
||||
await newProfile.saveProfileData(waitForDataInSync: true);
|
||||
|
|
|
|||
|
|
@ -125,3 +125,12 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructUseTypeUtil {
|
||||
static ConstructUseTypeEnum fromString(String value) {
|
||||
return ConstructUseTypeEnum.values.firstWhere(
|
||||
(e) => e.string == value,
|
||||
orElse: () => ConstructUseTypeEnum.nan,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room {
|
|||
await analyticsRoom.requestParticipants();
|
||||
}
|
||||
|
||||
final List<User> participants = await analyticsRoom.requestParticipants();
|
||||
final List<User> participants = analyticsRoom.getParticipants();
|
||||
final List<User> uninvitedTeachers = teachersLocal
|
||||
.where((teacher) => !participants.contains(teacher))
|
||||
.toList();
|
||||
|
|
@ -110,12 +110,8 @@ extension AnalyticsRoomExtension on Room {
|
|||
(teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to invite teacher to analytics room",
|
||||
m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}",
|
||||
s: s,
|
||||
data: {
|
||||
"teacherId": teacher.id,
|
||||
"analyticsRoomId": analyticsRoom.id,
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -540,8 +540,7 @@ class PangeaMessageEvent {
|
|||
|
||||
int get numberOfActivitiesCompleted {
|
||||
return MatrixState.pangeaController.activityRecordController
|
||||
.completedActivities[eventId] ??
|
||||
0;
|
||||
.getCompletedActivityCount(eventId);
|
||||
}
|
||||
|
||||
String? get l2Code =>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -106,9 +105,7 @@ class OneConstructUse {
|
|||
debugger(when: kDebugMode && constructType == null);
|
||||
|
||||
return OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.values
|
||||
.firstWhereOrNull((e) => e.string == json['useType']) ??
|
||||
ConstructUseTypeEnum.unk,
|
||||
useType: ConstructUseTypeUtil.fromString(json['useType']),
|
||||
lemma: json['lemma'],
|
||||
form: json['form'],
|
||||
categories: json['categories'] != null
|
||||
|
|
|
|||
|
|
@ -27,12 +27,7 @@ class ConstructWithXP {
|
|||
? DateTime.parse(json['last_used'] as String)
|
||||
: null,
|
||||
condensedConstructUses: (json['uses'] as List<String>).map((e) {
|
||||
return ConstructUseTypeEnum.values.firstWhereOrNull(
|
||||
(element) =>
|
||||
element.string == e ||
|
||||
element.toString().split('.').last == e,
|
||||
) ??
|
||||
ConstructUseTypeEnum.nan;
|
||||
return ConstructUseTypeUtil.fromString(e);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class InlineTooltip extends StatelessWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Lightbulb icon on the left
|
||||
Icon(
|
||||
|
|
@ -39,16 +40,14 @@ class InlineTooltip extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
// Text in the middle
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
instructionsEnum.body(context),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
Center(
|
||||
child: Text(
|
||||
instructionsEnum.body(context),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
// Close button on the right
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async {
|
|||
|
||||
// before wiping out locally cached construct data, save it to the server
|
||||
await MatrixState.pangeaController.myAnalytics
|
||||
.sendLocalAnalyticsToAnalyticsRoom();
|
||||
.sendLocalAnalyticsToAnalyticsRoom(onLogout: true);
|
||||
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
|
|
@ -8,7 +9,6 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar
|
|||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
|
|
@ -21,11 +21,15 @@ class MessageAudioCard extends StatefulWidget {
|
|||
final PangeaMessageEvent messageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final PangeaTokenText? selection;
|
||||
final TtsController tts;
|
||||
final Function(bool) setIsPlayingAudio;
|
||||
|
||||
const MessageAudioCard({
|
||||
super.key,
|
||||
required this.messageEvent,
|
||||
required this.overlayController,
|
||||
required this.tts,
|
||||
required this.setIsPlayingAudio,
|
||||
this.selection,
|
||||
});
|
||||
|
||||
|
|
@ -40,8 +44,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
int? sectionStartMS;
|
||||
int? sectionEndMS;
|
||||
|
||||
TtsController tts = TtsController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -56,7 +58,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
if (oldWidget.selection != widget.selection) {
|
||||
if (oldWidget.selection != widget.selection && widget.selection != null) {
|
||||
debugPrint('selection changed');
|
||||
setSectionStartAndEndFromSelection();
|
||||
playSelectionAudio();
|
||||
|
|
@ -65,10 +67,11 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
}
|
||||
|
||||
Future<void> playSelectionAudio() async {
|
||||
if (widget.selection == null) return;
|
||||
final PangeaTokenText selection = widget.selection!;
|
||||
final tokenText = selection.content;
|
||||
|
||||
await tts.speak(tokenText);
|
||||
await widget.tts.speak(tokenText);
|
||||
}
|
||||
|
||||
void setSectionStartAndEnd(int? start, int? end) => mounted
|
||||
|
|
@ -190,7 +193,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
child: _isLoading
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
|
|
@ -204,12 +206,14 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
sectionEndMS: sectionEndMS,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
setIsPlayingAudio: widget.setIsPlayingAudio,
|
||||
),
|
||||
tts.missingVoiceButton,
|
||||
widget.tts.missingVoiceButton,
|
||||
],
|
||||
)
|
||||
: const CardErrorWidget(
|
||||
error: "Null audio file in message_audio_card",
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart';
|
|||
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -61,11 +62,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
/// The number of activities that need to be completed before the toolbar is unlocked
|
||||
/// If we don't have any good activities for them, we'll decrease this number
|
||||
static const int neededActivities = 3;
|
||||
|
||||
int activitiesLeftToComplete = neededActivities;
|
||||
|
||||
PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent;
|
||||
|
||||
final TtsController tts = TtsController();
|
||||
bool isPlayingAudio = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -98,6 +100,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
).listen((_) => setState(() {}));
|
||||
|
||||
setInitialToolbarMode();
|
||||
tts.setupTTS();
|
||||
}
|
||||
|
||||
/// We need to check if the setState call is safe to call immediately
|
||||
|
|
@ -198,9 +201,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
PangeaToken token,
|
||||
) {
|
||||
if ([
|
||||
MessageMode.practiceActivity,
|
||||
// MessageMode.textToSpeech
|
||||
].contains(toolbarMode)) {
|
||||
MessageMode.practiceActivity,
|
||||
// MessageMode.textToSpeech
|
||||
].contains(toolbarMode) ||
|
||||
isPlayingAudio) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -271,6 +275,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
double get reactionsHeight => hasReactions ? 28 : 0;
|
||||
double get belowMessageHeight => toolbarButtonsHeight + reactionsHeight;
|
||||
|
||||
void setIsPlayingAudio(bool isPlaying) {
|
||||
if (mounted) {
|
||||
setState(() => isPlayingAudio = isPlaying);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
|
@ -359,6 +369,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_reactionSubscription?.cancel();
|
||||
tts.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -443,9 +454,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
final overlayMessage = Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
|
|
@ -457,6 +466,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
MessageToolbar(
|
||||
pangeaMessageEvent: widget._pangeaMessageEvent,
|
||||
overLayController: this,
|
||||
tts: tts,
|
||||
),
|
||||
SizedBox(
|
||||
height: adjustedMessageHeight,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
|
||||
|
|
@ -148,9 +149,12 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
|
|||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
//done fetchig but not results means some kind of error
|
||||
// done fetchig but not results means some kind of error
|
||||
if (speechToTextResponse == null) {
|
||||
return CardErrorWidget(error: error);
|
||||
return CardErrorWidget(
|
||||
error: error,
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: find better icons
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
|||
import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/select_to_define.dart';
|
||||
|
|
@ -22,11 +23,13 @@ const double minCardHeight = 70;
|
|||
class MessageToolbar extends StatelessWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overLayController;
|
||||
final TtsController tts;
|
||||
|
||||
const MessageToolbar({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overLayController,
|
||||
required this.tts,
|
||||
});
|
||||
|
||||
Widget get toolbarContent {
|
||||
|
|
@ -50,6 +53,8 @@ class MessageToolbar extends StatelessWidget {
|
|||
messageEvent: pangeaMessageEvent,
|
||||
overlayController: overLayController,
|
||||
selection: overLayController.selectedSpan,
|
||||
tts: tts,
|
||||
setIsPlayingAudio: overLayController.setIsPlayingAudio,
|
||||
);
|
||||
case MessageMode.speechToText:
|
||||
return MessageSpeechToTextCard(
|
||||
|
|
@ -87,6 +92,7 @@ class MessageToolbar extends StatelessWidget {
|
|||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
overlayController: overLayController,
|
||||
tts: tts,
|
||||
);
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -114,6 +120,9 @@ class MessageToolbar extends StatelessWidget {
|
|||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
minWidth: AppConfig.toolbarMinWidth,
|
||||
minHeight: AppConfig.toolbarMinHeight,
|
||||
// maxWidth is set by MessageSelectionOverlay
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
|
@ -130,46 +131,51 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
if (!_fetchingTranslation &&
|
||||
repEvent == null &&
|
||||
selectionTranslation == null) {
|
||||
return const CardErrorWidget(error: "No translation found");
|
||||
return const CardErrorWidget(
|
||||
error: "No translation found",
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
);
|
||||
}
|
||||
|
||||
final loadingTranslation =
|
||||
(widget.selection != null && selectionTranslation == null) ||
|
||||
(widget.selection == null && repEvent == null);
|
||||
|
||||
if (_fetchingTranslation || loadingTranslation) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_fetchingTranslation
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
: Flexible(
|
||||
child: Column(
|
||||
children: [
|
||||
widget.selection != null
|
||||
? selectionTranslation != null
|
||||
? Text(
|
||||
selectionTranslation!,
|
||||
style: BotStyle.text(context),
|
||||
)
|
||||
: const ToolbarContentLoadingIndicator()
|
||||
: repEvent != null
|
||||
? Text(
|
||||
repEvent!.text,
|
||||
style: BotStyle.text(context),
|
||||
)
|
||||
: const ToolbarContentLoadingIndicator(),
|
||||
if (notGoingToTranslate && widget.selection == null)
|
||||
InlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.l1Translation,
|
||||
onClose: () => setState(() {}),
|
||||
),
|
||||
if (widget.selection != null)
|
||||
InlineTooltip(
|
||||
instructionsEnum:
|
||||
InstructionsEnum.clickAgainToDeselect,
|
||||
onClose: () => setState(() {}),
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.selection != null
|
||||
? selectionTranslation!
|
||||
: repEvent!.text,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (notGoingToTranslate && widget.selection == null)
|
||||
InlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.l1Translation,
|
||||
onClose: () => setState(() {}),
|
||||
),
|
||||
if (widget.selection != null)
|
||||
InlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.clickAgainToDeselect,
|
||||
onClose: () => setState(() {}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ class MessageUnsubscribedCard extends StatelessWidget {
|
|||
final bool inTrialWindow =
|
||||
MatrixState.pangeaController.userController.inTrialWindow;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ToolbarContentLoadingIndicator extends StatelessWidget {
|
||||
|
|
@ -8,10 +8,9 @@ class ToolbarContentLoadingIndicator extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
return SizedBox(
|
||||
width: AppConfig.toolbarMinWidth,
|
||||
height: AppConfig.toolbarMinHeight,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
height: 14,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ class TtsController {
|
|||
}
|
||||
|
||||
onError(dynamic message) => ErrorHandler.logError(
|
||||
m: 'TTS error',
|
||||
e: message,
|
||||
m: (message.toString().isNotEmpty) ? message.toString() : 'TTS error',
|
||||
data: {
|
||||
'message': message,
|
||||
},
|
||||
|
|
@ -82,13 +83,11 @@ class TtsController {
|
|||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
await tts.stop();
|
||||
}
|
||||
|
||||
Future<void> speak(String text) async {
|
||||
try {
|
||||
stop();
|
||||
|
||||
targetLanguage ??=
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ class OverlayContainer extends StatelessWidget {
|
|||
constraints: BoxConstraints(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
minHeight: 100,
|
||||
minWidth: 100,
|
||||
),
|
||||
//PTODO - position card above input/message
|
||||
// margin: const EdgeInsets.all(10),
|
||||
|
|
|
|||
|
|
@ -9,21 +9,26 @@ class CardErrorWidget extends StatelessWidget {
|
|||
final Object? error;
|
||||
final Choreographer? choreographer;
|
||||
final int? offset;
|
||||
final double? maxWidth;
|
||||
|
||||
const CardErrorWidget({
|
||||
super.key,
|
||||
this.error,
|
||||
this.choreographer,
|
||||
this.offset,
|
||||
this.maxWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ErrorCopy errorCopy = ErrorCopy(context, error);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
return ConstrainedBox(
|
||||
constraints: maxWidth != null
|
||||
? BoxConstraints(maxWidth: maxWidth!)
|
||||
: const BoxConstraints(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CardHeader(
|
||||
text: errorCopy.title,
|
||||
|
|
@ -32,11 +37,13 @@ class CardErrorWidget extends StatelessWidget {
|
|||
cursorOffset: offset,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Center(
|
||||
const SizedBox(height: 12.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
errorCopy.body,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../common/bot_face_svg.dart';
|
||||
|
||||
class CardHeader extends StatelessWidget {
|
||||
|
|
@ -22,33 +22,31 @@ class CardHeader extends StatelessWidget {
|
|||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 3.0),
|
||||
child: BotFace(
|
||||
width: 50.0,
|
||||
expression: botExpression,
|
||||
BotFace(
|
||||
width: 50.0,
|
||||
expression: botExpression,
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: BotStyle.text(context),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
Text(
|
||||
text,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
CircleAvatar(
|
||||
backgroundColor: AppConfig.primaryColor.withOpacity(0.1),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () {
|
||||
if (onClose != null) onClose!();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
},
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppConfig.primaryColorLight
|
||||
: AppConfig.primaryColor,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () {
|
||||
if (onClose != null) onClose!();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
},
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppConfig.primaryColorLight
|
||||
: AppConfig.primaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
|
|
@ -7,8 +8,7 @@ import 'package:fluffychat/pangea/models/language_model.dart';
|
|||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -166,71 +166,68 @@ class WordDataCardView extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.wordNetError != null) {
|
||||
return CardErrorWidget(error: controller.wordNetError);
|
||||
return CardErrorWidget(
|
||||
error: controller.wordNetError,
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
);
|
||||
}
|
||||
if (controller.activeL1 == null || controller.activeL2 == null) {
|
||||
ErrorHandler.logError(m: "should not be here");
|
||||
return CardErrorWidget(error: controller.noLanguages);
|
||||
return CardErrorWidget(
|
||||
error: controller.noLanguages,
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
);
|
||||
}
|
||||
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
child: Scrollbar(
|
||||
thumbVisibility: true,
|
||||
controller: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (controller.widget.choiceFeedback != null)
|
||||
Text(
|
||||
controller.widget.choiceFeedback!,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
if (controller.wordData != null &&
|
||||
controller.wordNetError == null &&
|
||||
controller.activeL1 != null &&
|
||||
controller.activeL2 != null)
|
||||
WordNetInfo(
|
||||
wordData: controller.wordData!,
|
||||
activeL1: controller.activeL1!,
|
||||
activeL2: controller.activeL2!,
|
||||
),
|
||||
if (controller.isLoadingWordNet) const PCircular(),
|
||||
const SizedBox(height: 5.0),
|
||||
// if (controller.widget.hasInfo &&
|
||||
// !controller.isLoadingContextualDefinition &&
|
||||
// controller.contextualDefinitionRes == null)
|
||||
// Material(
|
||||
// type: MaterialType.transparency,
|
||||
// child: ListTile(
|
||||
// leading: const BotFace(
|
||||
// width: 40, expression: BotExpression.surprised),
|
||||
// title: Text(L10n.of(context)!.askPangeaBot),
|
||||
// onTap: controller.handleGetDefinitionButtonPress,
|
||||
// ),
|
||||
// ),
|
||||
if (controller.isLoadingContextualDefinition) const PCircular(),
|
||||
if (controller.contextualDefinitionRes != null)
|
||||
Text(
|
||||
controller.contextualDefinitionRes!.text,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
if (controller.definitionError != null)
|
||||
Text(
|
||||
L10n.of(context)!.sorryNoResults,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.widget.choiceFeedback != null)
|
||||
Text(
|
||||
controller.widget.choiceFeedback!,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
if (controller.wordData != null &&
|
||||
controller.wordNetError == null &&
|
||||
controller.activeL1 != null &&
|
||||
controller.activeL2 != null)
|
||||
WordNetInfo(
|
||||
wordData: controller.wordData!,
|
||||
activeL1: controller.activeL1!,
|
||||
activeL2: controller.activeL2!,
|
||||
),
|
||||
if (controller.isLoadingWordNet)
|
||||
const ToolbarContentLoadingIndicator(),
|
||||
const SizedBox(height: 5.0),
|
||||
// if (controller.widget.hasInfo &&
|
||||
// !controller.isLoadingContextualDefinition &&
|
||||
// controller.contextualDefinitionRes == null)
|
||||
// Material(
|
||||
// type: MaterialType.transparency,
|
||||
// child: ListTile(
|
||||
// leading: const BotFace(
|
||||
// width: 40, expression: BotExpression.surprised),
|
||||
// title: Text(L10n.of(context)!.askPangeaBot),
|
||||
// onTap: controller.handleGetDefinitionButtonPress,
|
||||
// ),
|
||||
// ),
|
||||
if (controller.isLoadingContextualDefinition)
|
||||
const ToolbarContentLoadingIndicator(),
|
||||
if (controller.contextualDefinitionRes != null)
|
||||
Text(
|
||||
controller.contextualDefinitionRes!.text,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (controller.definitionError != null)
|
||||
Text(
|
||||
L10n.of(context)!.sorryNoResults,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -251,12 +248,14 @@ class WordNetInfo extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SensesForLanguage(
|
||||
wordData: wordData,
|
||||
languageType: LanguageType.target,
|
||||
language: activeL2,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SensesForLanguage(
|
||||
wordData: wordData,
|
||||
languageType: LanguageType.base,
|
||||
|
|
@ -273,52 +272,6 @@ enum LanguageType {
|
|||
}
|
||||
|
||||
class SensesForLanguage extends StatelessWidget {
|
||||
const SensesForLanguage({
|
||||
super.key,
|
||||
required this.wordData,
|
||||
required this.languageType,
|
||||
required this.language,
|
||||
});
|
||||
|
||||
final LanguageModel language;
|
||||
final LanguageType languageType;
|
||||
final WordData wordData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(7, 0, 0, 0),
|
||||
child: LanguageFlag(
|
||||
language: language,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PartOfSpeechBlock(
|
||||
wordData: wordData,
|
||||
languageType: languageType,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PartOfSpeechBlock extends StatelessWidget {
|
||||
final WordData wordData;
|
||||
final LanguageType languageType;
|
||||
|
||||
const PartOfSpeechBlock({
|
||||
super.key,
|
||||
required this.wordData,
|
||||
required this.languageType,
|
||||
});
|
||||
|
||||
String get exampleSentence => languageType == LanguageType.target
|
||||
? wordData.targetExampleSentence
|
||||
: wordData.baseExampleSentence;
|
||||
|
|
@ -336,70 +289,76 @@ class PartOfSpeechBlock extends StatelessWidget {
|
|||
return "$word (${wordData.formattedPartOfSpeech(languageType)})";
|
||||
}
|
||||
|
||||
const SensesForLanguage({
|
||||
super.key,
|
||||
required this.wordData,
|
||||
required this.languageType,
|
||||
required this.language,
|
||||
});
|
||||
|
||||
final LanguageModel language;
|
||||
final LanguageType languageType;
|
||||
final WordData wordData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
formattedTitle(context),
|
||||
style: BotStyle.text(context, italics: true, bold: false),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14.0, bottom: 10.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Column(
|
||||
children: [
|
||||
if (definition.isNotEmpty)
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: BotStyle.text(
|
||||
context,
|
||||
italics: false,
|
||||
bold: false,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: "${L10n.of(context)!.definition}: ",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: definition),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (exampleSentence.isNotEmpty)
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: BotStyle.text(
|
||||
context,
|
||||
italics: false,
|
||||
bold: false,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: "${L10n.of(context)!.exampleSentence}: ",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(text: exampleSentence),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LanguageFlag(language: language),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
formattedTitle(context),
|
||||
style: BotStyle.text(context, italics: true, bold: false),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (definition.isNotEmpty)
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: BotStyle.text(
|
||||
context,
|
||||
italics: false,
|
||||
bold: false,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: "${L10n.of(context)!.definition}: ",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: definition),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (exampleSentence.isNotEmpty)
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: BotStyle.text(
|
||||
context,
|
||||
italics: false,
|
||||
bold: false,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: "${L10n.of(context)!.exampleSentence}: ",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(text: exampleSentence),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
|||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -16,11 +17,13 @@ import 'package:flutter/material.dart';
|
|||
class MultipleChoiceActivity extends StatefulWidget {
|
||||
final PracticeActivityCardState practiceCardController;
|
||||
final PracticeActivityModel currentActivity;
|
||||
final TtsController tts;
|
||||
|
||||
const MultipleChoiceActivity({
|
||||
super.key,
|
||||
required this.practiceCardController,
|
||||
required this.currentActivity,
|
||||
required this.tts,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -112,7 +115,10 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
// #freeze-activity
|
||||
if (practiceActivity.activityType ==
|
||||
ActivityTypeEnum.wordFocusListening)
|
||||
WordAudioButton(text: practiceActivity.content.answer),
|
||||
WordAudioButton(
|
||||
text: practiceActivity.content.answer,
|
||||
ttsController: widget.tts,
|
||||
),
|
||||
ChoicesArray(
|
||||
isLoading: false,
|
||||
uniqueKeyForLayerLink: (index) => "multiple_choice_$index",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -71,18 +72,21 @@ class GamifiedTextWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
const StarAnimationWidget(),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
userMessage,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
return SizedBox(
|
||||
width: AppConfig.toolbarMinWidth,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
const StarAnimationWidget(),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
userMessage,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
|||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/content_issue_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart';
|
||||
|
|
@ -28,11 +29,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
class PracticeActivityCard extends StatefulWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final TtsController tts;
|
||||
|
||||
const PracticeActivityCard({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overlayController,
|
||||
required this.tts,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -286,16 +289,15 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
/// If there is no current activity, the widget returns a sizedbox with a height of 80.
|
||||
/// If the activity type is multiple choice, the widget returns a MultipleChoiceActivity.
|
||||
/// If the activity type is unknown, the widget logs an error and returns a text widget with an error message.
|
||||
Widget get activityWidget {
|
||||
if (currentActivity == null) {
|
||||
// return sizedbox with height of 80
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
switch (currentActivity!.activityType) {
|
||||
Widget? get activityWidget {
|
||||
switch (currentActivity?.activityType) {
|
||||
case null:
|
||||
return null;
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity!,
|
||||
tts: widget.tts,
|
||||
);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
// return WordFocusListeningActivity(
|
||||
|
|
@ -303,19 +305,20 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity!,
|
||||
tts: widget.tts,
|
||||
);
|
||||
default:
|
||||
ErrorHandler.logError(
|
||||
e: Exception('Unknown activity type'),
|
||||
m: 'Unknown activity type',
|
||||
data: {
|
||||
'activityType': currentActivity!.activityType,
|
||||
},
|
||||
);
|
||||
return Text(
|
||||
L10n.of(context)!.oopsSomethingWentWrong,
|
||||
style: BotStyle.text(context),
|
||||
);
|
||||
// default:
|
||||
// ErrorHandler.logError(
|
||||
// e: Exception('Unknown activity type'),
|
||||
// m: 'Unknown activity type',
|
||||
// data: {
|
||||
// 'activityType': currentActivity!.activityType,
|
||||
// },
|
||||
// );
|
||||
// return Text(
|
||||
// L10n.of(context)!.oopsSomethingWentWrong,
|
||||
// style: BotStyle.text(context),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,20 +337,15 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
const Positioned(
|
||||
child: PointsGainedAnimation(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 20, 8, 8),
|
||||
child: activityWidget,
|
||||
),
|
||||
if (activityWidget != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
child: activityWidget,
|
||||
),
|
||||
// Conditionally show the darkening and progress indicator based on the loading state
|
||||
if (!savoringTheJoy && fetchingActivity) ...[
|
||||
// Semi-transparent overlay
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.5), // Darkening effect
|
||||
),
|
||||
// Circular progress indicator in the center
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
const ToolbarContentLoadingIndicator(),
|
||||
],
|
||||
// Flag button in the top right corner
|
||||
Positioned(
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
|
||||
class WordAudioButton extends StatefulWidget {
|
||||
final String text;
|
||||
final TtsController ttsController;
|
||||
|
||||
const WordAudioButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.ttsController,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -17,22 +19,6 @@ class WordAudioButton extends StatefulWidget {
|
|||
class WordAudioButtonState extends State<WordAudioButton> {
|
||||
bool _isPlaying = false;
|
||||
|
||||
TtsController ttsController = TtsController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
debugPrint('initState WordAudioButton');
|
||||
super.initState();
|
||||
ttsController.setupTTS().then((value) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ttsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('build WordAudioButton');
|
||||
|
|
@ -54,7 +40,7 @@ class WordAudioButtonState extends State<WordAudioButton> {
|
|||
_isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio,
|
||||
onPressed: () async {
|
||||
if (_isPlaying) {
|
||||
await ttsController.tts.stop();
|
||||
await widget.ttsController.tts.stop();
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
|
|
@ -62,7 +48,7 @@ class WordAudioButtonState extends State<WordAudioButton> {
|
|||
if (mounted) {
|
||||
setState(() => _isPlaying = true);
|
||||
}
|
||||
await ttsController.speak(widget.text);
|
||||
await widget.ttsController.speak(widget.text);
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
|
|
@ -70,7 +56,7 @@ class WordAudioButtonState extends State<WordAudioButton> {
|
|||
}, // Disable button if language isn't supported
|
||||
),
|
||||
// #freeze-activity
|
||||
ttsController.missingVoiceButton,
|
||||
widget.ttsController.missingVoiceButton,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,18 +10,11 @@ class SelectToDefine extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context)!.selectToDefine,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
child: Text(
|
||||
L10n.of(context)!.selectToDefine,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
|
|||
# Pangea#
|
||||
publish_to: none
|
||||
# On version bump also increase the build number for F-Droid
|
||||
version: 1.21.5+3545
|
||||
version: 1.22.6+3556
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue