diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a4058486b..211d34df2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -108,6 +108,19 @@ + + + + + + + + + + + ???? CFBundleURLTypes + + CFBundleURLSchemes + + pangea + + CFBundleURLName + com.talktolearn.chat + CFBundleTypeRole Editor @@ -113,5 +121,7 @@ io.flutter.embedded_views_preview + FlutterDeepLinkingEnabled + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 2b2a88dd1..91e1a0719 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -1,18 +1,16 @@ - - aps-environment - development - com.apple.developer.associated-domains - - applinks:example.com - - com.apple.security.application-groups - - - group.com.talktolearn.chat - - - - \ No newline at end of file + + aps-environment + development + com.apple.developer.associated-domains + + applinks:app.pangea.chat + + com.apple.security.application-groups + + group.com.talktolearn.chat + + + diff --git a/lib/config/routes.dart b/lib/config/routes.dart index e7fb1d376..d2a3efc5e 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -31,9 +31,9 @@ import 'package:fluffychat/pages/settings_security/settings_security.dart'; import 'package:fluffychat/pages/settings_style/settings_style.dart'; import 'package:fluffychat/pangea/activity_generator/activity_generator.dart'; import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart'; -import 'package:fluffychat/pangea/activity_suggestions/suggestions_page.dart'; +import 'package:fluffychat/pangea/analytics_page/analytics_page.dart'; +import 'package:fluffychat/pangea/common/widgets/pangea_side_view.dart'; import 'package:fluffychat/pangea/find_your_people/find_your_people.dart'; -import 'package:fluffychat/pangea/find_your_people/find_your_people_side_view.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart'; import 'package:fluffychat/pangea/login/pages/login_or_signup_view.dart'; @@ -196,7 +196,8 @@ abstract class AppRoutes { // state.fullPath?.startsWith('/rooms/settings') == false FluffyThemes.isColumnMode(context) && state.fullPath?.startsWith('/rooms/settings') == false && - state.fullPath?.startsWith('/rooms/communities') == false + state.fullPath?.startsWith('/rooms/communities') == false && + state.fullPath?.startsWith('/rooms/analytics') == false // Pangea# ? TwoColumnLayout( mainView: ChatList( @@ -309,7 +310,7 @@ abstract class AppRoutes { state, FluffyThemes.isColumnMode(context) ? TwoColumnLayout( - mainView: const FindYourPeopleSideView(), + mainView: PangeaSideView(path: state.fullPath), sideView: child, dividerColor: Colors.transparent, ) @@ -325,37 +326,14 @@ abstract class AppRoutes { const FindYourPeople(), ), ), - ], - ), - GoRoute( - path: 'homepage', - redirect: loggedOutRedirect, - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const SuggestionsPage(), - ), - routes: [ - ...newRoomRoutes, GoRoute( - path: '/planner', + path: 'analytics', + redirect: loggedOutRedirect, pageBuilder: (context, state) => defaultPageBuilder( context, state, - const ActivityPlannerPage(), + const AnalyticsPage(), ), - redirect: loggedOutRedirect, - routes: [ - GoRoute( - path: '/generator', - redirect: loggedOutRedirect, - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const ActivityGenerator(), - ), - ), - ], ), ], ), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0ed36c50c..e3be07db2 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -4631,9 +4631,9 @@ "meaningSectionHeader": "Meaning:", "formSectionHeader": "Forms used in chats:", "noEmojiSelectedTooltip": "No emoji selected", - "writingExercisesTooltip": "Writing practice", - "listeningExercisesTooltip": "Listening practice", - "readingExercisesTooltip": "Reading practice", + "writingExercisesTooltip": "Writing", + "listeningExercisesTooltip": "Listening", + "readingExercisesTooltip": "Reading", "meaningNotFound": "Meaning could not be found.", "formsNotFound": "Forms could not be found.", "chooseBaseForm": "Choose the base form", @@ -5002,6 +5002,7 @@ "canBeFoundViaKnock": "\u2022 request to join and admin approval", "anyoneCanJoin": "Anyone can join! However, admin can kick and ban whoever misbehaves. Those who are banned may not return!", "createYourSpace": "Create your space", + "youHaveLeveledUp": "You have leveled up!", "sendActivities": "Send activities", "getStarted": "Get Started", "getStartedBotChatDesc": "Chatting with AI is a great place to start and Pangea reading, writing, listening and speaking tools make it easy!", @@ -5016,7 +5017,7 @@ "groupChat": "Group Chat", "directMessage": "Direct Message", "newDirectMessage": "New direct message", - "speakingExercisesTooltip": "Speaking practice", + "speakingExercisesTooltip": "Speaking", "noChatsFoundHereYet": "No chats found here yet", "duration": "Duration", "transcriptionFailed": "Failed to transcribe audio", @@ -5032,4 +5033,4 @@ }, "failedToFetchTranscription": "Failed to fetch transcription", "deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone." -} \ No newline at end of file +} diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 3b2a0dedf..bc820b81a 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -33,7 +33,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart'; diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index a07aecc36..105b393c3 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -5,7 +5,6 @@ import 'package:fluffychat/pangea/common/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/message_token_text/message_token_button.dart'; -import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/utils/token_rendering_util.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; @@ -203,12 +202,14 @@ class HtmlMessage extends StatelessWidget { } } + int position = 0; for (final PangeaToken token in tokens ?? []) { final String tokenText = token.text.content; final substringIndex = result.indexWhere( (string) => string.contains(tokenText) && !(string.startsWith('<') && string.endsWith('>')), + position, ); if (substringIndex == -1) continue; @@ -228,9 +229,68 @@ class HtmlMessage extends StatelessWidget { '$tokenText', if (after.isNotEmpty) after, ]); + + position = substringIndex; } - return result.join(); + if (pangeaMessageEvent?.textDirection == TextDirection.rtl) { + for (int i = 0; i < result.length; i++) { + final tag = result[i]; + if (blockHtmlTags.contains(tag.htmlTagName) || + fullLineHtmlTag.contains(tag.htmlTagName)) { + if (i > 0 && result[i - 1] == ", ") { + result[i - 1] = ""; + } + result[i] = ", "; + } + } + result.removeWhere((element) => element == ""); + if (result[0] == ", ") result[0] = ""; + if (result.last == ", ") result.last = ""; + final inverted = _invertTags(result); + return inverted.join().trim(); + } + return result.join().trim(); + } + + List _invertTags(List tags) { + final List<(String, int)> stack = []; + final List<(int, int)> invertedTags = []; + for (int i = 0; i < tags.length; i++) { + final tag = tags[i]; + if (!tag.contains('<') || tag.contains(" + element.$1.htmlTagName == tag.htmlTagName && + !element.$1.contains(" a.tokens.contains(token), - ) - : null, ), MouseRegion( cursor: SystemMouseCursors.click, @@ -385,6 +436,7 @@ class HtmlMessage extends StatelessWidget { ? () => onClick?.call(token) : null, child: RichText( + textDirection: pangeaMessageEvent?.textDirection, text: TextSpan( children: [ LinkifySpan( @@ -1013,3 +1065,8 @@ extension on String { extension on dom.Element { dom.Element get rootElement => parent?.rootElement ?? this; } + +extension on String { + String get htmlTagName => + replaceAll('<', '').replaceAll('>', '').replaceAll('/', '').split(' ')[0]; +} diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 06509fae9..4929e27a9 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -35,35 +35,6 @@ class InvitationSelectionController extends State { String? get roomId => widget.roomId; // #Pangea - final viewportKey = GlobalKey(); - - final participantListItemHeight = 72.0; - final goToChatButtonHeight = 50.0; - final shareButtonsHeight = 150.0; - final padding = 16.0 * 2; - final fixedParticipantHeight = 72.0; - - double? viewportHeight; - double get availableHeight => - (viewportHeight ?? 0) - - goToChatButtonHeight - - shareButtonsHeight - - padding; - - bool showShareButtons(int numParticipants) => - (fixedParticipantHeight * numParticipants) < availableHeight; - - @override - initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { - final context = viewportKey.currentContext; - if (context == null) return; - final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; - setState(() => viewportHeight = size.height); - }); - super.initState(); - } List? get participants { final room = Matrix.of(context).client.getRoomById(roomId!); diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index 59c6f6ad0..2df3b6a56 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -8,12 +8,10 @@ import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart'; import 'package:fluffychat/pangea/chat_settings/constants/room_settings_constants.dart'; -import 'package:fluffychat/pangea/chat_settings/widgets/space_invite_buttons.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; @@ -107,165 +105,153 @@ class InvitationSelectionView extends StatelessWidget { // #Pangea withScrolling: false, // Pangea# - child: Stack( - alignment: Alignment.bottomCenter, + child: Column( children: [ Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: 450, - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}", - errorWidget: (context, url, error) => const SizedBox(), - placeholder: (context, url) => const Center( - child: CircularProgressIndicator.adaptive(), + // #Pangea + // padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.only( + bottom: 16.0, + left: 16.0, + right: 16.0, + ), + // Pangea# + child: TextField( + textInputAction: TextInputAction.search, + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), ), + hintStyle: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + // #Pangea + hintText: L10n.of(context).inviteStudentByUserName, + // hintText: L10n.of(context).inviteContactToGroup(groupName), + // Pangea# + prefixIcon: controller.loading + ? const Padding( + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 12, + ), + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + : const Icon(Icons.search_outlined), ), + onChanged: controller.searchUserWithCoolDown, ), ), - Column( - children: [ - Padding( - // #Pangea - // padding: const EdgeInsets.all(16.0), - padding: const EdgeInsets.only( - bottom: 16.0, - left: 16.0, - right: 16.0, - ), - // Pangea# - child: TextField( - textInputAction: TextInputAction.search, - decoration: InputDecoration( - filled: true, - fillColor: theme.colorScheme.secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), - hintStyle: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.normal, - ), - // #Pangea - hintText: L10n.of(context).inviteStudentByUserName, - // hintText: L10n.of(context).inviteContactToGroup(groupName), - // Pangea# - prefixIcon: controller.loading - ? const Padding( - padding: EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 12, - ), - child: SizedBox.square( - dimension: 24, + // #Pangea + // StreamBuilder( + Expanded( + child: StreamBuilder( + // stream: room.client.onRoomState.stream + // .where((update) => update.roomId == room.id), + stream: room.client.onRoomState.stream + .where((update) => update.roomId == room.id) + .rateLimit(const Duration(seconds: 1)), + // Pangea# + builder: (context, snapshot) { + final participants = + room.getParticipants().map((user) => user.id).toSet(); + return controller.foundProfiles.isNotEmpty + ? ListView.builder( + // #Pangea + // physics: const NeverScrollableScrollPhysics(), + // shrinkWrap: true, + // Pangea# + itemCount: controller.foundProfiles.length, + itemBuilder: (BuildContext context, int i) => + _InviteContactListTile( + profile: controller.foundProfiles[i], + isMember: participants.contains( + controller.foundProfiles[i].userId, + ), + onTap: () => controller.inviteAction( + context, + controller.foundProfiles[i].userId, + controller.foundProfiles[i].displayName ?? + controller + .foundProfiles[i].userId.localpart ?? + L10n.of(context).user, + ), + ), + ) + : FutureBuilder>( + future: controller.getContacts(context), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) { + return const Center( child: CircularProgressIndicator.adaptive( strokeWidth: 2, ), - ), - ) - : const Icon(Icons.search_outlined), - ), - onChanged: controller.searchUserWithCoolDown, - ), - ), - // #Pangea - // StreamBuilder( - Expanded( - key: controller.viewportKey, - child: StreamBuilder( - // stream: room.client.onRoomState.stream - // .where((update) => update.roomId == room.id), - stream: room.client.onRoomState.stream - .where((update) => update.roomId == room.id) - .rateLimit(const Duration(seconds: 1)), - // Pangea# - builder: (context, snapshot) { - final participants = - room.getParticipants().map((user) => user.id).toSet(); - return controller.foundProfiles.isNotEmpty - ? ListView.builder( + ); + } + final contacts = snapshot.data!; + return ListView.builder( // #Pangea // physics: const NeverScrollableScrollPhysics(), // shrinkWrap: true, - // Pangea# - itemCount: controller.foundProfiles.length, - itemBuilder: (BuildContext context, int i) => - _InviteContactListTile( - profile: controller.foundProfiles[i], - isMember: participants.contains( - controller.foundProfiles[i].userId, - ), - onTap: () => controller.inviteAction( - context, - controller.foundProfiles[i].userId, - controller.foundProfiles[i].displayName ?? - controller - .foundProfiles[i].userId.localpart ?? - L10n.of(context).user, - ), - ), - ) - : FutureBuilder>( - future: controller.getContacts(context), - builder: (BuildContext context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, + // itemCount: contacts.length, + // itemBuilder: (BuildContext context, int i) => + // _InviteContactListTile( + itemCount: contacts.length + 1, + itemBuilder: (BuildContext context, int i) { + if (i == contacts.length) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: 450, + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}", + errorWidget: (context, url, error) => + const SizedBox(), + placeholder: (context, url) => + const Center( + child: CircularProgressIndicator + .adaptive(), + ), + ), ), ); } - final contacts = snapshot.data!; - return ListView.builder( - // #Pangea - // physics: const NeverScrollableScrollPhysics(), - // shrinkWrap: true, - // itemCount: contacts.length, - // itemBuilder: (BuildContext context, int i) => - // _InviteContactListTile( - itemCount: contacts.length + 1, - itemBuilder: (BuildContext context, int i) { - if (i == contacts.length) { - final showButtons = controller - .showShareButtons(contacts.length); - return AnimatedOpacity( - duration: - FluffyThemes.animationDuration, - opacity: showButtons ? 1.0 : 0.0, - child: SpaceInviteButtons(room: room), - ); - } - - return _InviteContactListTile( - // Pangea# - user: contacts[i], - profile: Profile( - avatarUrl: contacts[i].avatarUrl, - displayName: contacts[i].displayName ?? - contacts[i].id.localpart ?? - L10n.of(context).user, - userId: contacts[i].id, - ), - isMember: - participants.contains(contacts[i].id), - onTap: () => controller.inviteAction( - context, - contacts[i].id, - contacts[i].displayName ?? - contacts[i].id.localpart ?? - L10n.of(context).user, - ), - ); - }, + return _InviteContactListTile( + // Pangea# + user: contacts[i], + profile: Profile( + avatarUrl: contacts[i].avatarUrl, + displayName: contacts[i].displayName ?? + contacts[i].id.localpart ?? + L10n.of(context).user, + userId: contacts[i].id, + ), + isMember: + participants.contains(contacts[i].id), + onTap: () => controller.inviteAction( + context, + contacts[i].id, + contacts[i].displayName ?? + contacts[i].id.localpart ?? + L10n.of(context).user, + ), ); }, ); - }, - ), - ), - ], + }, + ); + }, + ), ), Padding( padding: const EdgeInsets.all(16.0), @@ -355,6 +341,8 @@ class _InviteContactListTile extends StatelessWidget { style: const TextStyle( fontSize: 12.0, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), LevelDisplayName(userId: profile.userId), ], diff --git a/lib/pangea/activity_generator/activity_generator.dart b/lib/pangea/activity_generator/activity_generator.dart index 35c517b17..2ed3dcd9b 100644 --- a/lib/pangea/activity_generator/activity_generator.dart +++ b/lib/pangea/activity_generator/activity_generator.dart @@ -73,7 +73,7 @@ class ActivityGeneratorState extends State { ActivitySettingRequestSchema get req => ActivitySettingRequestSchema( langCode: - MatrixState.pangeaController.languageController.userL2?.langCode ?? + MatrixState.pangeaController.languageController.userL1?.langCode ?? LanguageKeys.defaultLanguage, ); diff --git a/lib/pangea/activity_planner/activity_planner_page.dart b/lib/pangea/activity_planner/activity_planner_page.dart index 166cbb2d9..eac6c9596 100644 --- a/lib/pangea/activity_planner/activity_planner_page.dart +++ b/lib/pangea/activity_planner/activity_planner_page.dart @@ -14,8 +14,8 @@ enum PageMode { } class ActivityPlannerPage extends StatefulWidget { - final String? roomID; - const ActivityPlannerPage({super.key, this.roomID}); + final String roomID; + const ActivityPlannerPage({super.key, required this.roomID}); @override ActivityPlannerPageState createState() => ActivityPlannerPageState(); @@ -23,9 +23,7 @@ class ActivityPlannerPage extends StatefulWidget { class ActivityPlannerPageState extends State { PageMode pageMode = PageMode.featuredActivities; - Room? get room => widget.roomID != null - ? Matrix.of(context).client.getRoomById(widget.roomID!) - : null; + Room? get room => Matrix.of(context).client.getRoomById(widget.roomID); void _setPageMode(PageMode? mode) { if (mode == null) return; diff --git a/lib/pangea/activity_planner/activity_planner_page_appbar.dart b/lib/pangea/activity_planner/activity_planner_page_appbar.dart index 94ee72890..fd7352de7 100644 --- a/lib/pangea/activity_planner/activity_planner_page_appbar.dart +++ b/lib/pangea/activity_planner/activity_planner_page_appbar.dart @@ -12,11 +12,11 @@ import 'package:fluffychat/pangea/common/widgets/customized_svg.dart'; class ActivityPlannerPageAppBar extends StatelessWidget implements PreferredSizeWidget { final PageMode pageMode; - final String? roomID; + final String roomID; const ActivityPlannerPageAppBar({ required this.pageMode, - this.roomID, + required this.roomID, super.key, }); @@ -71,9 +71,8 @@ class ActivityPlannerPageAppBar extends StatelessWidget alignment: Alignment.center, child: InkWell( customBorder: const CircleBorder(), - onTap: () => roomID != null - ? context.go('/rooms/$roomID/details/planner/generator') - : context.go("/rooms/homepage/planner/generator"), + onTap: () => + context.go('/rooms/$roomID/details/planner/generator'), child: Container( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, @@ -114,9 +113,8 @@ class ActivityPlannerPageAppBar extends StatelessWidget ) : IconButton( icon: const Icon(Icons.add), - onPressed: () => roomID != null - ? context.go('/rooms/$roomID/details/planner/generator') - : context.go("/rooms/homepage/planner/generator"), + onPressed: () => + context.go('/rooms/$roomID/details/planner/generator'), ), ], ); diff --git a/lib/pangea/activity_suggestions/activity_suggestions_area.dart b/lib/pangea/activity_suggestions/activity_suggestions_area.dart index 82d0ca74b..848923717 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_area.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_area.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:shimmer/shimmer.dart'; @@ -25,14 +24,11 @@ import 'package:fluffychat/widgets/matrix.dart'; class ActivitySuggestionsArea extends StatefulWidget { final Axis? scrollDirection; - final bool showTitle; - final Room? room; const ActivitySuggestionsArea({ super.key, this.scrollDirection, - this.showTitle = false, this.room, }); @override @@ -141,7 +137,6 @@ class ActivitySuggestionsAreaState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final isColumnMode = FluffyThemes.isColumnMode(context); final List cards = _loading ? List.generate(5, (i) { @@ -196,29 +191,6 @@ class ActivitySuggestionsAreaState extends State { spacing: 8.0, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.showTitle) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - L10n.of(context).chatWithActivities, - style: isColumnMode - ? theme.textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold) - : theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.event_note_outlined), - onPressed: () => context.go('/rooms/homepage/planner'), - tooltip: L10n.of(context).activityPlannerTitle, - ), - ], - ), AnimatedSize( duration: FluffyThemes.animationDuration, child: (_timeout || !_loading && cards.isEmpty) diff --git a/lib/pangea/activity_suggestions/suggestions_page.dart b/lib/pangea/activity_suggestions/suggestions_page.dart deleted file mode 100644 index b8d2a9038..000000000 --- a/lib/pangea/activity_suggestions/suggestions_page.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:go_router/go_router.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart'; -import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart'; -import 'package:fluffychat/pangea/public_spaces/public_spaces_area.dart'; -import 'package:fluffychat/widgets/navigation_rail.dart'; - -class SuggestionsPage extends StatelessWidget { - const SuggestionsPage({super.key}); - - @override - Widget build(BuildContext context) { - final isColumnMode = FluffyThemes.isColumnMode(context); - return Scaffold( - resizeToAvoidBottomInset: true, - body: SafeArea( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isColumnMode && AppConfig.displayNavigationRail) ...[ - SpacesNavigationRail( - activeSpaceId: null, - onGoToChats: () => context.go('/rooms'), - onGoToSpaceId: (spaceId) => - context.go('/rooms?spaceId=$spaceId'), - ), - Container( - color: Theme.of(context).dividerColor, - width: 1, - ), - ], - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 16.0, - ), - child: Column( - spacing: 24.0, - children: [ - if (!isColumnMode) const LearningProgressIndicators(), - const ActivitySuggestionsArea( - showTitle: true, - scrollDirection: Axis.horizontal, - ), - const PublicSpacesArea(), - ], - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pangea/analytics_details_popup/analytics_details_popup.dart b/lib/pangea/analytics_details_popup/analytics_details_popup.dart index 9cb085479..e43cbc998 100644 --- a/lib/pangea/analytics_details_popup/analytics_details_popup.dart +++ b/lib/pangea/analytics_details_popup/analytics_details_popup.dart @@ -26,11 +26,33 @@ class AnalyticsPopupWrapper extends StatefulWidget { this.constructZoom, required this.view, this.backButtonOverride, + this.showAppBar = true, }); final ConstructTypeEnum view; final ConstructIdentifier? constructZoom; final Widget? backButtonOverride; + final bool showAppBar; + + static void show( + BuildContext context, { + ConstructIdentifier? constructZoom, + ConstructTypeEnum view = ConstructTypeEnum.vocab, + Widget? backButtonOverride, + }) { + showDialog( + context: context, + builder: (context) => FullWidthDialog( + maxWidth: 600, + maxHeight: 800, + dialogContent: AnalyticsPopupWrapper( + constructZoom: constructZoom, + view: view, + backButtonOverride: backButtonOverride, + ), + ), + ); + } @override AnalyticsPopupWrapperState createState() => AnalyticsPopupWrapperState(); @@ -58,6 +80,19 @@ class AnalyticsPopupWrapperState extends State { }); } + @override + void didUpdateWidget(covariant AnalyticsPopupWrapper oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.constructZoom != oldWidget.constructZoom) { + setConstructZoom(widget.constructZoom); + } + if (widget.view != oldWidget.view) { + localView = widget.view; + localConstructZoom = null; + setState(() {}); + } + } + @override void dispose() { searchController.dispose(); @@ -109,74 +144,82 @@ class AnalyticsPopupWrapperState extends State { @override Widget build(BuildContext context) { - return FullWidthDialog( - dialogContent: Scaffold( - appBar: AppBar( - title: kIsWeb - ? Text( - localView == ConstructTypeEnum.morph - ? ConstructTypeEnum.morph.indicator.tooltip(context) - : ConstructTypeEnum.vocab.indicator.tooltip(context), + return Scaffold( + appBar: widget.showAppBar + ? AppBar( + title: kIsWeb + ? Text( + localView == ConstructTypeEnum.morph + ? ConstructTypeEnum.morph.indicator.tooltip(context) + : ConstructTypeEnum.vocab.indicator.tooltip(context), + ) + : null, + leading: widget.backButtonOverride ?? + IconButton( + icon: localConstructZoom == null + ? const Icon(Icons.close) + : const Icon(Icons.arrow_back), + onPressed: localConstructZoom == null + ? () => Navigator.of(context).pop() + : () => setConstructZoom(null), + ), + actions: [ + TextButton.icon( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + backgroundColor: localView == ConstructTypeEnum.vocab + ? Theme.of(context).colorScheme.primary.withAlpha(50) + : Theme.of(context).colorScheme.surface, + ), + label: Text(L10n.of(context).vocab), + icon: const Icon(Symbols.dictionary), + onPressed: () => setState(() { + localView = ConstructTypeEnum.vocab; + localConstructZoom = null; + }), + ), + const SizedBox(width: 4.0), + TextButton.icon( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + backgroundColor: localView == ConstructTypeEnum.morph + ? Theme.of(context).colorScheme.primary.withAlpha(50) + : Theme.of(context).colorScheme.surface, + ), + label: Text(L10n.of(context).grammar), + icon: const Icon(Symbols.toys_and_games), + onPressed: () => setState(() { + localView = ConstructTypeEnum.morph; + localConstructZoom = null; + }), + ), + const SizedBox(width: 4.0), + if (kIsWeb) const DownloadAnalyticsButton(), + if (kIsWeb) const SizedBox(width: 4.0), + ], + ) + : localConstructZoom != null + ? AppBar( + leading: widget.backButtonOverride ?? + (localConstructZoom != null + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => setConstructZoom(null), + ) + : const SizedBox()), ) : null, - leading: widget.backButtonOverride ?? - IconButton( - icon: localConstructZoom == null - ? const Icon(Icons.close) - : const Icon(Icons.arrow_back), - onPressed: localConstructZoom == null - ? () => Navigator.of(context).pop() - : () => setConstructZoom(null), - ), - actions: [ - TextButton.icon( - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - backgroundColor: localView == ConstructTypeEnum.vocab - ? Theme.of(context).colorScheme.primary.withAlpha(50) - : Theme.of(context).colorScheme.surface, - ), - label: Text(L10n.of(context).vocab), - icon: const Icon(Symbols.dictionary), - onPressed: () => setState(() { - localView = ConstructTypeEnum.vocab; - localConstructZoom = null; - }), - ), - const SizedBox(width: 4.0), - TextButton.icon( - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - backgroundColor: localView == ConstructTypeEnum.morph - ? Theme.of(context).colorScheme.primary.withAlpha(50) - : Theme.of(context).colorScheme.surface, - ), - label: Text(L10n.of(context).grammar), - icon: const Icon(Symbols.toys_and_games), - onPressed: () => setState(() { - localView = ConstructTypeEnum.morph; - localConstructZoom = null; - }), - ), - const SizedBox(width: 4.0), - if (kIsWeb) const DownloadAnalyticsButton(), - if (kIsWeb) const SizedBox(width: 4.0), - ], - ), - body: localView == ConstructTypeEnum.morph - ? localConstructZoom == null - ? MorphAnalyticsListView(controller: this) - : MorphDetailsView(constructId: localConstructZoom!) - : localConstructZoom == null - ? VocabAnalyticsListView(controller: this) - : VocabDetailsView(constructId: localConstructZoom!), - ), - maxWidth: 600, - maxHeight: 800, + body: localView == ConstructTypeEnum.morph + ? localConstructZoom == null + ? MorphAnalyticsListView(controller: this) + : MorphDetailsView(constructId: localConstructZoom!) + : localConstructZoom == null + ? VocabAnalyticsListView(controller: this) + : VocabDetailsView(constructId: localConstructZoom!), ); } } diff --git a/lib/pangea/analytics_details_popup/morph_analytics_list_view.dart b/lib/pangea/analytics_details_popup/morph_analytics_list_view.dart index 7d5ccde38..bef1f92b2 100644 --- a/lib/pangea/analytics_details_popup/morph_analytics_list_view.dart +++ b/lib/pangea/analytics_details_popup/morph_analytics_list_view.dart @@ -227,18 +227,22 @@ class MorphTagChip extends StatelessWidget { color: Colors.white, ), ), - Text( - getGrammarCopy( - category: morphFeature, - lemma: morphTag, - context: context, - ) ?? - morphTag, - style: TextStyle( - fontWeight: FontWeight.bold, - color: theme.brightness == Brightness.dark - ? Colors.white - : Colors.black, + Flexible( + child: Text( + getGrammarCopy( + category: morphFeature, + lemma: morphTag, + context: context, + ) ?? + morphTag, + style: TextStyle( + fontWeight: FontWeight.bold, + color: theme.brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart index c1e00254a..7db44d463 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart'; import 'package:fluffychat/pangea/analytics_details_popup/vocab_analytics_list_tile.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -78,92 +79,82 @@ class VocabAnalyticsListView extends StatelessWidget { ), ); - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const InstructionsInlineTooltip( - instructionsEnum: InstructionsEnum.analyticsVocabList, + return Column( + children: [ + const InstructionsInlineTooltip( + instructionsEnum: InstructionsEnum.analyticsVocabList, + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + padding: EdgeInsets.symmetric( + horizontal: controller.isSearching ? 8.0 : 24.0, ), - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - padding: EdgeInsets.symmetric( - horizontal: controller.isSearching ? 8.0 : 24.0, - ), - child: Container( - height: 60, - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 250.0), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: child, - ), - child: controller.isSearching - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - key: const ValueKey('search'), - children: [ - Expanded( - child: TextField( - autofocus: true, - controller: controller.searchController, - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 12.0, - ), - isDense: true, - border: OutlineInputBorder(), - ), - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: controller.toggleSearching, - ), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - key: const ValueKey('filters'), - children: filters, + child: Container( + height: 60, + alignment: Alignment.center, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: child, + ), + child: controller.isSearching + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + key: const ValueKey('search'), + children: [ + Expanded( + child: TextField( + autofocus: true, + controller: controller.searchController, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 12.0, + ), + isDense: true, + border: OutlineInputBorder(), ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: controller.toggleSearching, + ), + ], + ) + : Row( + spacing: FluffyThemes.isColumnMode(context) ? 16.0 : 4.0, + mainAxisAlignment: MainAxisAlignment.center, + key: const ValueKey('filters'), + children: filters, ), - ), - ], - ), ), ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 100.0, - mainAxisExtent: 100.0, - crossAxisSpacing: 8.0, - mainAxisSpacing: 8.0, - ), - itemCount: _filteredVocab.length, - itemBuilder: (context, index) { - final vocabItem = _filteredVocab[index]; - return VocabAnalyticsListTile( - onTap: () => controller.setConstructZoom(vocabItem.id), - constructUse: vocabItem, - ); - }, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 100.0, + mainAxisExtent: 100.0, + crossAxisSpacing: 8.0, + mainAxisSpacing: 8.0, ), + itemCount: _filteredVocab.length, + itemBuilder: (context, index) { + final vocabItem = _filteredVocab[index]; + return VocabAnalyticsListTile( + onTap: () => controller.setConstructZoom(vocabItem.id), + constructUse: vocabItem, + ); + }, ), ), - ], - ), + ), + ], ); } } diff --git a/lib/pangea/analytics_misc/construct_list_model.dart b/lib/pangea/analytics_misc/construct_list_model.dart index 86a54e963..a7326e26f 100644 --- a/lib/pangea/analytics_misc/construct_list_model.dart +++ b/lib/pangea/analytics_misc/construct_list_model.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; @@ -35,7 +36,8 @@ class ConstructListModel { /// [D] is the "compression factor". It determines how quickly /// or slowly the level grows relative to XP - final double D = 1500; + + final double D = Environment.isStagingEnvironment ? 500 : 1500; List unlockedLemmas( ConstructTypeEnum type, { diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 5f8997a7d..5d861b089 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -22,6 +22,7 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart'; +import 'package:fluffychat/widgets/matrix.dart'; /// A minimized version of AnalyticsController that get the logged in user's analytics class GetAnalyticsController extends BaseController { @@ -455,10 +456,13 @@ class GetAnalyticsController extends BaseController { // int diffXP = maxXP - minXP; // if (diffXP < 0) diffXP = 0; - Future getConstructSummaryFromStateEvent() async { + ConstructSummary? getConstructSummaryFromStateEvent() { try { final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!); - if (analyticsRoom == null) return null; + if (analyticsRoom == null) { + debugPrint("Analytics room is null"); + return null; + } final state = analyticsRoom.getState(PangeaEventTypes.constructSummary, ''); if (state == null) return null; @@ -477,8 +481,8 @@ class GetAnalyticsController extends BaseController { // generate level up analytics as a construct summary ConstructSummary summary; try { - final int minXP = constructListModel.calculateXpWithLevel(upperLevel); - final int maxXP = constructListModel.calculateXpWithLevel(lowerLevel); + final int maxXP = constructListModel.calculateXpWithLevel(upperLevel); + final int minXP = constructListModel.calculateXpWithLevel(lowerLevel); int diffXP = maxXP - minXP; if (diffXP < 0) diffXP = 0; @@ -539,6 +543,10 @@ class GetAnalyticsController extends BaseController { final response = await ConstructRepo.generateConstructSummary(request); summary = response.summary; + summary.levelVocabConstructs = MatrixState + .pangeaController.getAnalytics.constructListModel.vocabLemmas; + summary.levelGrammarConstructs = MatrixState + .pangeaController.getAnalytics.constructListModel.grammarLemmas; } catch (e) { debugPrint("Error generating level up analytics: $e"); ErrorHandler.logError(e: e, data: {'e': e}); diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart deleted file mode 100644 index 6e348aa13..000000000 --- a/lib/pangea/analytics_misc/level_up.dart +++ /dev/null @@ -1,557 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:audioplayers/audioplayers.dart'; -import 'package:cached_network_image/cached_network_image.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; -import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/common/utils/overlay.dart'; -import 'package:fluffychat/pangea/constructs/construct_repo.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class LevelUpConstants { - static const String starFileName = "star.png"; - static const String dinoLevelUPFileName = "DinoBot-Congratulate.png"; -} - -class LevelUpUtil { - static Future showLevelUpDialog( - int level, - int prevLevel, - BuildContext context, - ) async { - final player = AudioPlayer(); - - final snackbarRegex = RegExp(r'_snackbar$'); - - while (MatrixState.pAnyState.activeOverlays - .any((overlayId) => snackbarRegex.hasMatch(overlayId))) { - await Future.delayed(const Duration(milliseconds: 100)); - } - - player - .play( - UrlSource( - "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", - ), - ) - .then( - (_) => Future.delayed( - const Duration(seconds: 2), - () => player.dispose(), - ), - ); - - OverlayUtil.showOverlay( - overlayKey: "level_up_notification", - context: context, - child: LevelUpBanner( - level: level, - prevLevel: prevLevel, - ), - transformTargetId: '', - position: OverlayPositionEnum.top, - backDropToDismiss: false, - closePrevOverlay: false, - canPop: false, - ); - } -} - -class LevelUpBanner extends StatefulWidget { - final int level; - final int prevLevel; - - const LevelUpBanner({ - required this.level, - required this.prevLevel, - super.key, - }); - - @override - LevelUpBannerState createState() => LevelUpBannerState(); -} - -class LevelUpBannerState extends State - with TickerProviderStateMixin { - late AnimationController _slideController; - late Animation _slideAnimation; - - late AnimationController _sizeController; - late Animation _sizeAnimation; - - bool _showDetails = false; - bool _showedDetails = false; - - ConstructSummary? _constructSummary; - String? _error; - bool _loading = true; - - @override - void initState() { - super.initState(); - _setConstructSummary(); - - _slideController = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: _slideController, - curve: Curves.easeOut, - ), - ); - - _sizeController = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _sizeAnimation = Tween( - begin: 0, - end: 1, - ).animate( - CurvedAnimation( - parent: _sizeController, - curve: Curves.easeOut, - ), - ); - - _slideController.forward(); - - Future.delayed(const Duration(seconds: 15), () async { - if (mounted && !_showedDetails) _close(); - }); - } - - @override - void dispose() { - _slideController.dispose(); - _sizeController.dispose(); - super.dispose(); - } - - Future _setConstructSummary() async { - try { - setState(() => _loading = true); - _constructSummary = await MatrixState.pangeaController.getAnalytics - .generateLevelUpAnalytics( - widget.level, - widget.prevLevel, - ); - } catch (e) { - _error = e.toString(); - } finally { - if (mounted) { - setState(() => _loading = false); - } - } - } - - Future _close() async { - await _slideController.reverse(); - MatrixState.pAnyState.closeOverlay("level_up_notification"); - } - - int _skillsPoints(LearningSkillsEnum skill) { - switch (skill) { - case LearningSkillsEnum.writing: - return _constructSummary?.writingConstructScore ?? 0; - case LearningSkillsEnum.reading: - return _constructSummary?.readingConstructScore ?? 0; - case LearningSkillsEnum.speaking: - return _constructSummary?.speakingConstructScore ?? 0; - case LearningSkillsEnum.hearing: - return _constructSummary?.hearingConstructScore ?? 0; - default: - return 0; - } - } - - Future _toggleDetails() async { - if (!Environment.isStagingEnvironment) return; - - FocusScope.of(context).unfocus(); - - if (mounted) { - setState(() { - _showDetails = !_showDetails; - if (_showDetails && !_showedDetails) { - _showedDetails = true; - } - }); - - await (_showDetails - ? _sizeController.forward() - : _sizeController.reverse()); - - if (!_showDetails) { - await Future.delayed( - const Duration(milliseconds: 300), - () async { - if (!mounted) return; - _close(); - }, - ); - } - } - } - - @override - Widget build(BuildContext context) { - final style = FluffyThemes.isColumnMode(context) - ? Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ) - : Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ); - - return SafeArea( - child: Material( - color: Colors.transparent, - child: Stack( - children: [ - SlideTransition( - position: _slideAnimation, - child: Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 600.0, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy < -10) _close(); - }, - onTap: _toggleDetails, - child: Container( - margin: const EdgeInsets.only( - top: 16, - ), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 24, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 8.0, - children: [ - Flexible( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: L10n.of(context) - .congratulationsOnReaching( - widget.level, - ), - style: style, - ), - TextSpan( - text: " ", - style: style, - ), - WidgetSpan( - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", - height: 24, - width: 24, - ), - ), - ], - ), - ), - ), - Row( - children: [ - if (Environment.isStagingEnvironment) - AnimatedSize( - duration: FluffyThemes.animationDuration, - child: _error == null - ? FluffyThemes.isColumnMode(context) - ? ElevatedButton( - style: IconButton.styleFrom( - padding: const EdgeInsets - .symmetric( - vertical: 4.0, - horizontal: 16.0, - ), - ), - onPressed: _toggleDetails, - child: Text( - L10n.of(context).details, - ), - ) - : SizedBox( - width: 32.0, - height: 32.0, - child: Center( - child: IconButton( - icon: const Icon( - Icons.info_outline, - ), - style: - IconButton.styleFrom( - padding: - const EdgeInsets - .all( - 4.0, - ), - ), - onPressed: _toggleDetails, - constraints: - const BoxConstraints(), - ), - ), - ) - : Row( - children: [ - Tooltip( - message: L10n.of(context) - .oopsSomethingWentWrong, - child: Icon( - Icons.error, - color: Theme.of(context) - .colorScheme - .error, - ), - ), - ], - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: _close, - ), - ], - ), - ], - ), - ), - ), - SizeTransition( - sizeFactor: _sizeAnimation, - child: Container( - constraints: BoxConstraints( - maxHeight: - MediaQuery.of(context).size.height * 0.75, - ), - margin: const EdgeInsets.only( - top: 4.0, - ), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.all(16), - child: _loading - ? const Center( - child: CircularProgressIndicator.adaptive(), - ) - : _error != null - ? Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.error, - color: Theme.of(context) - .colorScheme - .error, - ), - const SizedBox(width: 8.0), - Text( - L10n.of(context) - .oopsSomethingWentWrong, - ), - ], - ) - : SingleChildScrollView( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.center, - spacing: 24.0, - children: [ - Table( - columnWidths: const { - 0: IntrinsicColumnWidth(), - 1: FlexColumnWidth(), - 2: IntrinsicColumnWidth(), - }, - defaultVerticalAlignment: - TableCellVerticalAlignment - .middle, - children: [ - ...LearningSkillsEnum.values - .where( - (v) => - v.isVisible && - _skillsPoints(v) > -1, - ) - .map((skill) { - return TableRow( - children: [ - Padding( - padding: const EdgeInsets - .symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Icon( - skill.icon, - size: 25, - color: Colors.white, - ), - ), - Padding( - padding: const EdgeInsets - .symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Text( - skill.tooltip(context), - style: const TextStyle( - fontSize: 16, - fontWeight: - FontWeight.w600, - color: Colors.white, - ), - textAlign: - TextAlign.center, - ), - ), - Padding( - padding: const EdgeInsets - .symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Text( - "+ ${_skillsPoints(skill)} XP", - style: const TextStyle( - fontSize: 16, - fontWeight: - FontWeight.w600, - color: Colors.white, - ), - textAlign: - TextAlign.center, - ), - ), - ], - ); - }), - ], - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - if (_constructSummary?.textSummary != - null) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: - BorderRadius.circular(8), - ), - child: Text( - _constructSummary!.textSummary, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - textAlign: TextAlign.center, - ), - ), - const SizedBox( - height: 24, - ), - // Share button, currently no functionality - // ElevatedButton( - // onPressed: () { - // // Add share functionality - // }, - // style: ElevatedButton.styleFrom( - // backgroundColor: Colors.white, - // foregroundColor: Colors.black, - // padding: const EdgeInsets.symmetric( - // vertical: 12, - // horizontal: 24, - // ), - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(8), - // ), - // ), - // child: const Row( - // mainAxisSize: MainAxisSize - // .min, - // children: [ - // Text( - // "Share with Friends", - // style: TextStyle( - // fontSize: 16, - // fontWeight: FontWeight.bold, - // ), - // ), - // SizedBox( - // width: 8, - // ), - // Icon( - // Icons.ios_share, - // size: 20, - // ), - // ), - // ), - // ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart new file mode 100644 index 000000000..a94916231 --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -0,0 +1,270 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LevelUpConstants { + static const String starFileName = "star.png"; + static const String dinoLevelUPFileName = "DinoBot-Congratulate.png"; +} + +class LevelUpUtil { + static Future showLevelUpDialog( + int level, + int prevLevel, + BuildContext context, + ) async { + // Remove delay since GetAnalyticsController._onLevelUp is already async + final player = AudioPlayer(); + + // Wait for any existing snackbars to dismiss + await _waitForSnackbars(context); + + await player.play( + UrlSource( + "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", + ), + ); + + if (!context.mounted) return; + + await OverlayUtil.showOverlay( + overlayKey: "level_up_notification", + context: context, + child: LevelUpBanner( + level: level, + prevLevel: prevLevel, + backButtonOverride: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + MatrixState.pAnyState.closeOverlay("level_up_notification"); + }, + ), + ), + transformTargetId: '', + position: OverlayPositionEnum.top, + backDropToDismiss: false, + closePrevOverlay: false, + canPop: false, + ); + + await Future.delayed(const Duration(seconds: 2)); + player.dispose(); + } + + static Future _waitForSnackbars(BuildContext context) async { + final snackbarRegex = RegExp(r'_snackbar$'); + while (MatrixState.pAnyState.activeOverlays + .any((id) => snackbarRegex.hasMatch(id))) { + await Future.delayed(const Duration(milliseconds: 100)); + } + } +} + +class LevelUpBanner extends StatefulWidget { + final int level; + final int prevLevel; + final Widget? backButtonOverride; + + const LevelUpBanner({ + required this.level, + required this.prevLevel, + required this.backButtonOverride, + super.key, + }); + + @override + LevelUpBannerState createState() => LevelUpBannerState(); +} + +class LevelUpBannerState extends State + with TickerProviderStateMixin { + late AnimationController _slideController; + late Animation _slideAnimation; + + bool _showedDetails = false; + + @override + void initState() { + super.initState(); + + LevelUpManager.instance.preloadAnalytics( + context, + widget.level, + widget.prevLevel, + ); + + _slideController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); + + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideController, + curve: Curves.easeOut, + ), + ); + + _slideController.forward(); + + Future.delayed(const Duration(seconds: 10), () async { + if (mounted && !_showedDetails) { + _close(); + } + }); + } + + Future _close() async { + await _slideController.reverse(); + MatrixState.pAnyState.closeOverlay("level_up_notification"); + } + + @override + void dispose() { + _slideController.dispose(); + super.dispose(); + } + + Future _toggleDetails() async { + await _close(); + LevelUpManager.instance.markPopupSeen(); + _showedDetails = true; + + FocusScope.of(context).unfocus(); + + await showDialog( + context: context, + builder: (context) => const LevelUpPopup(), + ); + } + + @override + Widget build(BuildContext context) { + final isColumnMode = FluffyThemes.isColumnMode(context); + + final style = isColumnMode + ? Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppConfig.gold, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ) + : Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppConfig.gold, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ); + + return SafeArea( + child: Material( + type: MaterialType.transparency, + child: SlideTransition( + position: _slideAnimation, + child: LayoutBuilder( + builder: (context, constraints) { + return GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy < -10) _close(); + }, + onTap: _toggleDetails, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 4.0, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: AppConfig.gold.withAlpha(200), + width: 2.0, + ), + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(AppConfig.borderRadius), + bottomRight: Radius.circular(AppConfig.borderRadius), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Spacer for symmetry + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + ), + // Centered content + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isColumnMode ? 16.0 : 8.0, + ), + child: Wrap( + spacing: 16.0, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + "Level up", + style: style, + overflow: TextOverflow.ellipsis, + ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", + height: 24, + width: 24, + ), + ], + ), + ), + ), + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (Environment.isStagingEnvironment) + SizedBox( + width: 32.0, + height: 32.0, + child: Center( + child: IconButton( + icon: const Icon(Icons.arrow_drop_down), + style: IconButton.styleFrom( + padding: const EdgeInsets.all(4.0), + ), + onPressed: _toggleDetails, + constraints: const BoxConstraints(), + color: + Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart new file mode 100644 index 000000000..7716d4165 --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; +import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LevelUpManager { + // Singleton instance so analytics can be generated when level up is initiated, and be ready by the time user clicks on banner + static final LevelUpManager instance = LevelUpManager._internal(); + + LevelUpManager._internal(); + + int prevLevel = 0; + int level = 0; + + int prevGrammar = 0; + int nextGrammar = 0; + int prevVocab = 0; + int nextVocab = 0; + + String? userL2Code; + + ConstructSummary? constructSummary; + + bool hasSeenPopup = false; + bool shouldAutoPopup = false; + String? error; + + Future preloadAnalytics( + BuildContext context, + int level, + int prevLevel, + ) async { + this.level = level; + this.prevLevel = prevLevel; + + //For on route change behavior, if added in the future + shouldAutoPopup = true; + + nextGrammar = MatrixState + .pangeaController.getAnalytics.constructListModel.grammarLemmas; + nextVocab = MatrixState + .pangeaController.getAnalytics.constructListModel.vocabLemmas; + + userL2Code = MatrixState.pangeaController.languageController + .activeL2Code() + ?.toUpperCase(); + + getConstructFromLevelUp(); + + final LanguageModel? l2 = + MatrixState.pangeaController.languageController.userL2; + final Room? analyticsRoom = + MatrixState.pangeaController.matrixState.client.analyticsRoomLocal(l2!); + + if (analyticsRoom != null) { + // How to get all summary events in the timeline + final timeline = await analyticsRoom.getTimeline(); + final summaryEvents = timeline.events + .where( + (e) => e.type == PangeaEventTypes.constructSummary, + ) + .map( + (e) => ConstructSummary.fromJson(e.content), + ) + .toList(); + + //Find previous summary to get grammar constructs and vocab numbers from + final lastSummary = summaryEvents + .where((summary) => summary.upperLevel == prevLevel) + .toList() + .isNotEmpty + ? summaryEvents + .firstWhere((summary) => summary.upperLevel == prevLevel) + : null; + + //Set grammar and vocab from last level summary, if there is one. Otherwise set to placeholder data + if (lastSummary != null && + lastSummary.levelVocabConstructs != null && + lastSummary.levelGrammarConstructs != null) { + prevVocab = lastSummary.levelVocabConstructs!; + prevGrammar = lastSummary.levelGrammarConstructs!; + } else { + prevGrammar = (nextGrammar / prevLevel) as int; + prevVocab = (nextVocab / prevLevel) as int; + } + } + } + + //for testing, just fetch last level up from saved analytics + void getConstructFromButton() { + constructSummary = MatrixState.pangeaController.getAnalytics + .getConstructSummaryFromStateEvent(); + debugPrint( + "Last saved construct summary from analytics controller function: ${constructSummary?.toJson()}", + ); + } + + //for getting real level up data when leveled up + void getConstructFromLevelUp() async { + try { + constructSummary = await MatrixState.pangeaController.getAnalytics + .generateLevelUpAnalytics( + prevLevel, + level, + ); + } catch (e) { + error = e.toString(); + } + } + + void markPopupSeen() { + hasSeenPopup = true; + shouldAutoPopup = false; + } + + void reset() { + hasSeenPopup = false; + shouldAutoPopup = false; + prevLevel = 0; + level = 0; + prevGrammar = 0; + nextGrammar = 0; + prevVocab = 0; + nextVocab = 0; + constructSummary = null; + error = null; + } +} diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart new file mode 100644 index 000000000..c7aa2fb3b --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -0,0 +1,521 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:animated_flip_counter/animated_flip_counter.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:confetti/confetti.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix_api_lite/generated/model.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.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/rain_confetti.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/full_width_dialog.dart'; +import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; + +class LevelUpPopup extends StatelessWidget { + const LevelUpPopup({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return FullWidthDialog( + maxWidth: 400, + maxHeight: 800, + dialogContent: Scaffold( + appBar: AppBar( + centerTitle: true, + title: kIsWeb + ? Text( + L10n.of(context).youHaveLeveledUp, + style: const TextStyle( + color: AppConfig.gold, + fontWeight: FontWeight.w600, + ), + ) + : null, + ), + body: LevelUpPopupContent( + prevLevel: LevelUpManager.instance.prevLevel, + level: LevelUpManager.instance.level, + ), + ), + ); + } +} + +class LevelUpPopupContent extends StatefulWidget { + final int prevLevel; + final int level; + + const LevelUpPopupContent({ + super.key, + required this.prevLevel, + required this.level, + }); + + @override + State createState() => _LevelUpPopupContentState(); +} + +class _LevelUpPopupContentState extends State + with SingleTickerProviderStateMixin { + late int _endGrammar; + late int _endVocab; + final int _startGrammar = LevelUpManager.instance.prevGrammar; + final int _startVocab = LevelUpManager.instance.prevVocab; + Timer? _summaryPollTimer; + final String? _error = LevelUpManager.instance.error; + String language = LevelUpManager.instance.userL2Code ?? "N/A"; + + late final AnimationController _controller; + late final ConfettiController _confettiController; + bool _hasBlastedConfetti = false; + final Duration _animationDuration = const Duration(seconds: 5); + + Uri? avatarUrl; + late final Future profile; + int displayedLevel = -1; + late ConstructSummary? _constructSummary; + + @override + void initState() { + super.initState(); + LevelUpManager.instance.markPopupSeen(); + displayedLevel = widget.prevLevel; + _confettiController = + ConfettiController(duration: const Duration(seconds: 1)); + _endGrammar = LevelUpManager.instance.nextGrammar; + _endVocab = LevelUpManager.instance.nextVocab; + _constructSummary = LevelUpManager.instance.constructSummary; + // Poll for constructSummary if not available + if (_constructSummary == null) { + _summaryPollTimer = + Timer.periodic(const Duration(milliseconds: 300), (timer) { + final summary = LevelUpManager.instance.constructSummary; + if (summary != null) { + setState(() { + _constructSummary = summary; + }); + timer.cancel(); + } + }); + } + final client = Matrix.of(context).client; + client.fetchOwnProfile().then((profile) { + setState(() { + avatarUrl = profile.avatarUrl; + }); + }); + _controller = AnimationController( + duration: _animationDuration, + vsync: this, + ); + + // halfway through the animation, switch to the new level + _controller.addListener(() { + if (_controller.value >= 0.5 && displayedLevel == widget.prevLevel) { + setState(() { + displayedLevel = widget.level; + }); + } + }); + + _controller.addListener(() { + if (_controller.value >= 0.5 && !_hasBlastedConfetti) { + //_confettiController.play(); + _hasBlastedConfetti = true; + rainConfetti(context); + } + }); + + _controller.forward(); + } + + @override + void dispose() { + _summaryPollTimer?.cancel(); + _controller.dispose(); + _confettiController.dispose(); + LevelUpManager.instance.reset(); + stopConfetti(); + super.dispose(); + } + + int _getSkillXP(LearningSkillsEnum skill) { + if (_constructSummary == null) return 0; + return switch (skill) { + LearningSkillsEnum.writing => + _constructSummary?.writingConstructScore ?? 0, + LearningSkillsEnum.reading => + _constructSummary?.readingConstructScore ?? 0, + LearningSkillsEnum.speaking => + _constructSummary?.speakingConstructScore ?? 0, + LearningSkillsEnum.hearing => + _constructSummary?.hearingConstructScore ?? 0, + _ => 0, + }; + } + + @override + @override + Widget build(BuildContext context) { + final Animation progressAnimation = + Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.5)), + ); + + final Animation vocabAnimation = + IntTween(begin: _startVocab, end: _endVocab).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), + ), + ); + + final Animation grammarAnimation = + IntTween(begin: _startGrammar, end: _endGrammar).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), + ), + ); + + final Animation skillsOpacity = + Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeIn), + ), + ); + + final Animation shrinkMultiplier = + Tween(begin: 1.0, end: 0.3).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeInOut), + ), + ); + + final colorScheme = Theme.of(context).colorScheme; + final grammarVocabStyle = Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ); + final username = + Matrix.of(context).client.userID?.split(':').first.substring(1) ?? ''; + + return Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AnimatedBuilder( + animation: _controller, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(24.0), + child: avatarUrl == null + ? Avatar( + name: username, + showPresence: false, + size: 150 * shrinkMultiplier.value, + ) + : ClipOval( + child: MxcImage( + uri: avatarUrl, + width: 150 * shrinkMultiplier.value, + height: 150 * shrinkMultiplier.value, + ), + ), + ), + Text( + language, + style: TextStyle( + fontSize: 24 * skillsOpacity.value, + color: AppConfig.goldLight, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + // Progress bar + Level + AnimatedBuilder( + animation: _controller, + builder: (_, __) => Row( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return LevelBar( + 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), + Text( + "⭐", + style: Theme.of(context).textTheme.titleLarge, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedFlipCounter( + value: displayedLevel, + textStyle: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: AppConfig.goldLight, + ), + duration: const Duration(milliseconds: 1000), + curve: Curves.easeInOut, + ), + ), + ], + ), + ), + const SizedBox(height: 25), + + // Vocab and grammar row + AnimatedBuilder( + animation: _controller, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "+ ${_endVocab - _startVocab}", + style: const TextStyle( + color: Colors.lightGreen, + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + Icon( + Symbols.dictionary, + color: colorScheme.primary, + size: 35, + ), + const SizedBox(width: 8), + Text( + '${vocabAnimation.value}', + style: grammarVocabStyle, + ), + ], + ), + ], + ), + const SizedBox(width: 40), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "+ ${_endGrammar - _startGrammar}", + style: const TextStyle( + color: Colors.lightGreen, + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + Icon( + Symbols.toys_and_games, + color: colorScheme.primary, + size: 35, + ), + const SizedBox(width: 8), + Text( + '${grammarAnimation.value}', + style: grammarVocabStyle, + ), + ], + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + // Skills section + AnimatedBuilder( + animation: skillsOpacity, + builder: (_, __) => Opacity( + opacity: skillsOpacity.value, + child: _error == null + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildSkillsTable(context), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + _constructSummary?.textSummary ?? + L10n.of(context).loadingPleaseWait, + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), + ), + ], + ) + // if error getting construct summary + : Row( + children: [ + Tooltip( + message: L10n.of(context).oopsSomethingWentWrong, + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + ), + ], + ), + ), + ), + // Share button, currently no functionality + // ElevatedButton( + // onPressed: () { + // // Add share functionality + // }, + // style: ElevatedButton.styleFrom( + // backgroundColor: Colors.white, + // foregroundColor: Colors.black, + // padding: const EdgeInsets.symmetric( + // vertical: 12, + // horizontal: 24, + // ), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8), + // ), + // ), + // child: const Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Text( + // "Share with Friends", + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.bold, + // ), + // ), + // SizedBox( + // width: 8, + // ), + // Icon( + // Icons.ios_share, + // size: 20, + // ), + // ], + // ), + // ), + ], + ), + ), + ], + ); + } + + Widget _buildSkillsTable(BuildContext context) { + final visibleSkills = LearningSkillsEnum.values + .where((skill) => (_getSkillXP(skill) > -1) && skill.isVisible) + .toList(); + + const itemsPerRow = 4; + // chunk into rows of up to 4 + final rows = >[ + for (var i = 0; i < visibleSkills.length; i += itemsPerRow) + visibleSkills.sublist( + i, + min(i + itemsPerRow, visibleSkills.length), + ), + ]; + + return Column( + children: rows.map((row) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: row.map((skill) { + return Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + skill.tooltip(context), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Icon( + skill.icon, + size: 25, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(height: 4), + Text( + '+ ${_getSkillXP(skill)} XP', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppConfig.gold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + }).toList(), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/pangea/analytics_misc/level_up/rain_confetti.dart b/lib/pangea/analytics_misc/level_up/rain_confetti.dart new file mode 100644 index 000000000..13e43aca3 --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/rain_confetti.dart @@ -0,0 +1,121 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:confetti/confetti.dart'; + +import 'package:fluffychat/config/app_config.dart'; + +OverlayEntry? _confettiEntry; +ConfettiController? _blastController; +ConfettiController? _rainController; + +void rainConfetti(BuildContext context) { + if (_confettiEntry != null) return; // Prevent duplicates + + _blastController = ConfettiController(duration: const Duration(seconds: 1)); + _rainController = ConfettiController(duration: const Duration(seconds: 3)); + + _blastController!.play(); + _rainController!.play(); + + final screenWidth = MediaQuery.of(context).size.width; + final isSmallScreen = screenWidth < 600; + final count = isSmallScreen ? 2 : 5; + final spacing = screenWidth / (count + 1); + + _confettiEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + // Initial center blast + Positioned( + top: 0, + left: screenWidth / 2, + child: IgnorePointer( + child: ConfettiWidget( + confettiController: _blastController!, + blastDirectionality: BlastDirectionality.explosive, + shouldLoop: false, + emissionFrequency: .02, + numberOfParticles: 40, + minimumSize: const Size(20, 20), + maximumSize: const Size(25, 25), + minBlastForce: 10, + maxBlastForce: 40, + gravity: 0.07, + colors: const [AppConfig.goldLight, AppConfig.gold], + createParticlePath: drawStar, + ), + ), + ), + + // Rain confetti from the top + ...List.generate(count, (index) { + final left = spacing * (index + 1) - 10; + + return Positioned( + top: -30, // Small buffer above top edge + left: left, + child: IgnorePointer( + child: ConfettiWidget( + confettiController: _rainController!, + blastDirectionality: BlastDirectionality.directional, + blastDirection: 3 * pi / 2, + shouldLoop: true, + maxBlastForce: 5, + minBlastForce: 2, + minimumSize: const Size(20, 20), + maximumSize: const Size(25, 25), + gravity: 0.07, + emissionFrequency: 0.1, + numberOfParticles: 2, + colors: const [AppConfig.goldLight, AppConfig.gold], + createParticlePath: drawStar, + ), + ), + ); + }), + ], + ), + ); + + Overlay.of(context, rootOverlay: true).insert(_confettiEntry!); +} + +void stopConfetti() { + _confettiEntry?.remove(); + _confettiEntry = null; + + _blastController?.dispose(); + _blastController = null; + + _rainController?.dispose(); + _rainController = null; +} + +Path drawStar(Size size) { + double degToRad(double deg) => deg * (pi / 180.0); + + const numberOfPoints = 5; + final halfWidth = size.width / 2; + final externalRadius = halfWidth; + final internalRadius = halfWidth / 2.5; + final degreesPerStep = degToRad(360 / numberOfPoints); + final halfDegreesPerStep = degreesPerStep / 2; + final path = Path(); + final fullAngle = degToRad(360); + path.moveTo(size.width, halfWidth); + + for (double step = 0; step < fullAngle; step += degreesPerStep) { + path.lineTo( + halfWidth + externalRadius * cos(step), + halfWidth + externalRadius * sin(step), + ); + path.lineTo( + halfWidth + internalRadius * cos(step + halfDegreesPerStep), + halfWidth + internalRadius * sin(step + halfDegreesPerStep), + ); + } + path.close(); + return path; +} diff --git a/lib/pangea/analytics_page/analytics_page.dart b/lib/pangea/analytics_page/analytics_page.dart new file mode 100644 index 000000000..c285a1030 --- /dev/null +++ b/lib/pangea/analytics_page/analytics_page.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/analytics_page/analytics_page_view.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; + +class AnalyticsPage extends StatefulWidget { + const AnalyticsPage({super.key}); + + @override + AnalyticsPageState createState() => AnalyticsPageState(); +} + +class AnalyticsPageState extends State { + ProgressIndicatorEnum? selectedIndicator = ProgressIndicatorEnum.wordsUsed; + + void onIndicatorSelected(ProgressIndicatorEnum indicator) => setState(() { + selectedIndicator = indicator; + }); + + @override + Widget build(BuildContext context) => AnalyticsPageView(controller: this); +} diff --git a/lib/pangea/analytics_page/analytics_page_constants.dart b/lib/pangea/analytics_page/analytics_page_constants.dart new file mode 100644 index 000000000..075bde772 --- /dev/null +++ b/lib/pangea/analytics_page/analytics_page_constants.dart @@ -0,0 +1,3 @@ +class AnalyticsPageConstants { + static const String dinoBotFileName = 'Analytic_DinoBot.png'; +} diff --git a/lib/pangea/analytics_page/analytics_page_view.dart b/lib/pangea/analytics_page/analytics_page_view.dart new file mode 100644 index 000000000..4e15c90d3 --- /dev/null +++ b/lib/pangea/analytics_page/analytics_page_view.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_page/analytics_page.dart'; +import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart'; +import 'package:fluffychat/pangea/analytics_summary/level_dialog_content.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; +import 'package:fluffychat/widgets/navigation_rail.dart'; + +class AnalyticsPageView extends StatelessWidget { + final AnalyticsPageState controller; + const AnalyticsPageView({ + super.key, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + final isColumnMode = FluffyThemes.isColumnMode(context); + + return Row( + children: [ + if (!isColumnMode && AppConfig.displayNavigationRail) ...[ + SpacesNavigationRail( + activeSpaceId: null, + onGoToChats: () => context.go('/rooms'), + onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'), + ), + Container( + color: Theme.of(context).dividerColor, + width: 1, + ), + ], + Expanded( + child: Scaffold( + body: Padding( + padding: const EdgeInsetsGeometry.all(16.0), + child: Column( + spacing: 16.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LearningProgressIndicators( + selected: controller.selectedIndicator, + onIndicatorSelected: controller.onIndicatorSelected, + ), + Expanded( + child: Builder( + builder: (context) { + if (controller.selectedIndicator == + ProgressIndicatorEnum.level) { + return const LevelDialogContent(); + } else if (controller.selectedIndicator == + ProgressIndicatorEnum.morphsUsed) { + return const AnalyticsPopupWrapper( + view: ConstructTypeEnum.morph, + showAppBar: false, + ); + } else if (controller.selectedIndicator == + ProgressIndicatorEnum.wordsUsed) { + return const AnalyticsPopupWrapper( + view: ConstructTypeEnum.vocab, + showAppBar: false, + ); + } + + return const SizedBox(); + }, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pangea/analytics_summary/learning_progress_indicator_button.dart b/lib/pangea/analytics_summary/learning_progress_indicator_button.dart index 30ea42199..1e4632e0b 100644 --- a/lib/pangea/analytics_summary/learning_progress_indicator_button.dart +++ b/lib/pangea/analytics_summary/learning_progress_indicator_button.dart @@ -7,6 +7,7 @@ class HoverButton extends StatelessWidget { final Widget child; final BorderRadius? borderRadius; final double hoverOpacity; + final bool selected; const HoverButton({ super.key, @@ -14,6 +15,7 @@ class HoverButton extends StatelessWidget { required this.child, this.borderRadius, this.hoverOpacity = 0.2, + this.selected = false, }); @override @@ -26,7 +28,7 @@ class HoverButton extends StatelessWidget { onTap: onPressed, child: Container( decoration: BoxDecoration( - color: hovered + color: hovered || selected ? Theme.of(context) .colorScheme .primary diff --git a/lib/pangea/analytics_summary/learning_progress_indicators.dart b/lib/pangea/analytics_summary/learning_progress_indicators.dart index d7a6d1c98..328a3a419 100644 --- a/lib/pangea/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/analytics_summary/learning_progress_indicators.dart @@ -19,7 +19,13 @@ import 'package:fluffychat/widgets/matrix.dart'; /// messages sent, words used, and error types, which can /// be clicked to access more fine-grained analytics data. class LearningProgressIndicators extends StatefulWidget { - const LearningProgressIndicators({super.key}); + final ProgressIndicatorEnum? selected; + final Function(ProgressIndicatorEnum)? onIndicatorSelected; + const LearningProgressIndicators({ + super.key, + this.selected, + this.onIndicatorSelected, + }); @override State createState() => @@ -106,12 +112,18 @@ class LearningProgressIndicatorsState children: ConstructTypeEnum.values .map( (c) => HoverButton( + selected: widget.selected == c.indicator, onPressed: () { - showDialog( - context: context, - builder: (context) => AnalyticsPopupWrapper( - view: c, - ), + if (widget.onIndicatorSelected != null) { + widget.onIndicatorSelected?.call( + c.indicator, + ); + return; + } + + AnalyticsPopupWrapper.show( + context, + view: c, ); }, child: ProgressIndicatorBadge( @@ -168,6 +180,12 @@ class LearningProgressIndicatorsState cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { + if (widget.onIndicatorSelected != null) { + widget.onIndicatorSelected + ?.call(ProgressIndicatorEnum.level); + return; + } + showDialog( context: context, builder: (c) => const LevelBarPopup(), diff --git a/lib/pangea/analytics_summary/level_bar_popup.dart b/lib/pangea/analytics_summary/level_bar_popup.dart index f7bf790ab..2d7af94d9 100644 --- a/lib/pangea/analytics_summary/level_bar_popup.dart +++ b/lib/pangea/analytics_summary/level_bar_popup.dart @@ -1,28 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; -import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; -import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart'; -import 'package:fluffychat/pangea/analytics_summary/learning_progress_bar.dart'; -import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; -import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/pangea/analytics_summary/level_dialog_content.dart'; class LevelBarPopup extends StatelessWidget { const LevelBarPopup({ super.key, }); - GetAnalyticsController get getAnalyticsController => - MatrixState.pangeaController.getAnalytics; - int get level => getAnalyticsController.constructListModel.level; - int get totalXP => getAnalyticsController.constructListModel.totalXP; - int get maxLevelXP => getAnalyticsController.minXPForNextLevel; - List get uses => - getAnalyticsController.constructListModel.truncatedUses; - @override Widget build(BuildContext context) { return Dialog( @@ -33,143 +17,7 @@ class LevelBarPopup extends StatelessWidget { ), child: ClipRRect( borderRadius: BorderRadius.circular(20.0), - child: Scaffold( - appBar: AppBar( - titleSpacing: 0, - automaticallyImplyLeading: false, - title: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "⭐ ${L10n.of(context).levelShort(level)}", - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w900, - color: AppConfig.gold, - ), - ), - Opacity( - opacity: 0.25, - child: Text( - L10n.of(context).levelShort(level + 1), - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w900, - ), - ), - ), - ], - ), - ), - ), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LearningProgressBar( - height: 24, - level: level, - totalXP: totalXP, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - child: Text( - L10n.of(context).xpIntoLevel(totalXP, maxLevelXP), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w900, - color: AppConfig.gold, - ), - ), - ), - const Divider(), - ], - ), - ), - Expanded( - child: ListView.builder( - itemCount: uses.length, - itemBuilder: (context, index) { - final use = uses[index]; - String lemmaCopy = use.lemma; - if (use.constructType == ConstructTypeEnum.morph) { - lemmaCopy = getGrammarCopy( - category: use.category, - lemma: use.lemma, - context: context, - ) ?? - use.lemma; - } - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Container( - width: 40, - alignment: Alignment.centerLeft, - child: Icon(use.useType.icon), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Text( - "\"$lemmaCopy\" - ${use.useType.description(context)}", - style: const TextStyle(fontSize: 14), - ), - ), - Container( - alignment: Alignment.topRight, - width: 60, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${use.xp > 0 ? '+' : ''}${use.xp}", - style: TextStyle( - fontWeight: FontWeight.w900, - fontSize: 14, - height: 1, - color: use.pointValueColor(context), - ), - ), - // const SizedBox(width: 5), - // const CircleAvatar( - // radius: 8, - // child: Icon( - // size: 10, - // Icons.star, - // color: Colors.white, - // ), - // ), - ], - ), - ), - ], - ), - ), - ); - }, - ), - ), - ], - ), - ), + child: const LevelDialogContent(), ), ), ); diff --git a/lib/pangea/analytics_summary/level_dialog_content.dart b/lib/pangea/analytics_summary/level_dialog_content.dart new file mode 100644 index 000000000..ac1a71e5e --- /dev/null +++ b/lib/pangea/analytics_summary/level_dialog_content.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart'; +import 'package:fluffychat/pangea/analytics_summary/learning_progress_bar.dart'; +import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LevelDialogContent extends StatelessWidget { + const LevelDialogContent({ + super.key, + }); + + GetAnalyticsController get getAnalyticsController => + MatrixState.pangeaController.getAnalytics; + int get level => getAnalyticsController.constructListModel.level; + int get totalXP => getAnalyticsController.constructListModel.totalXP; + int get maxLevelXP => getAnalyticsController.minXPForNextLevel; + List get uses => + getAnalyticsController.constructListModel.truncatedUses; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + automaticallyImplyLeading: false, + title: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "⭐ ${L10n.of(context).levelShort(level)}", + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w900, + color: AppConfig.gold, + ), + ), + Opacity( + opacity: 0.25, + child: Text( + L10n.of(context).levelShort(level + 1), + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w900, + ), + ), + ), + ], + ), + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LearningProgressBar( + height: 24, + level: level, + totalXP: totalXP, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + child: Text( + L10n.of(context).xpIntoLevel(totalXP, maxLevelXP), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: AppConfig.gold, + ), + ), + ), + const Divider(), + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: uses.length, + itemBuilder: (context, index) { + final use = uses[index]; + String lemmaCopy = use.lemma; + if (use.constructType == ConstructTypeEnum.morph) { + lemmaCopy = getGrammarCopy( + category: use.category, + lemma: use.lemma, + context: context, + ) ?? + use.lemma; + } + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Container( + width: 40, + alignment: Alignment.centerLeft, + child: Icon(use.useType.icon), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + "\"$lemmaCopy\" - ${use.useType.description(context)}", + style: const TextStyle(fontSize: 14), + ), + ), + Container( + alignment: Alignment.topRight, + width: 60, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${use.xp > 0 ? '+' : ''}${use.xp}", + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 14, + height: 1, + color: use.pointValueColor(context), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart b/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart index 16f4649a2..93ea3006b 100644 --- a/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart +++ b/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart @@ -153,15 +153,13 @@ class ConstructNotificationOverlayState } void _showDetails() { - showDialog( - context: context, - builder: (context) => AnalyticsPopupWrapper( - constructZoom: widget.construct, - view: ConstructTypeEnum.morph, - backButtonOverride: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), + AnalyticsPopupWrapper.show( + context, + constructZoom: widget.construct, + view: ConstructTypeEnum.morph, + backButtonOverride: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), ), ); } diff --git a/lib/pangea/chat_settings/widgets/space_invite_buttons.dart b/lib/pangea/chat_settings/widgets/space_invite_buttons.dart deleted file mode 100644 index 5fd3c7347..000000000 --- a/lib/pangea/chat_settings/widgets/space_invite_buttons.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:matrix/matrix.dart'; -import 'package:universal_html/html.dart' as html; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; - -class SpaceInviteButtons extends StatefulWidget { - final Room room; - // final ScrollController scrollController; - const SpaceInviteButtons({ - super.key, - required this.room, - // required this.scrollController, - }); - - @override - SpaceInviteButtonsController createState() => SpaceInviteButtonsController(); -} - -class SpaceInviteButtonsController extends State { - // bool get isVisible { - // final context = (widget.key as GlobalKey).currentContext; - // if (context == null) return false; - - // final renderBox = context.findRenderObject() as RenderBox; - // final position = renderBox.localToGlobal(Offset.zero); - - // final size = renderBox.size; - // final screenHeight = MediaQuery.of(context).size.height; - - // debugPrint("position: $position, size: $size, screenHeight: $screenHeight"); - - // // Check if any part of the widget is within the visible range - // return position.dy + size.height > 0 && position.dy < screenHeight; - // } - - @override - void initState() { - // WidgetsBinding.instance.addPostFrameCallback( - // (_) => debugPrint("isVisible: $isVisible"), - // ); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final spaceCode = widget.room.classCode; - if (!widget.room.isSpace || spaceCode == null) { - return const SizedBox.shrink(); - } - - return SizedBox( - height: 150.0, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 16.0, - right: 16.0, - left: 16.0, - ), - child: ElevatedButton( - child: Row( - spacing: 8.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.share_outlined, - ), - Text(L10n.of(context).shareSpaceLink), - ], - ), - onPressed: () async { - final String initialUrl = - kIsWeb ? html.window.origin! : Environment.frontendURL; - final link = - "$initialUrl/#/join_with_link?${SpaceConstants.classCode}=$spaceCode"; - await Clipboard.setData( - ClipboardData( - text: link, - ), - ); - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - L10n.of(context).copiedToClipboard, - ), - ), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 16.0, - right: 16.0, - left: 16.0, - ), - child: ElevatedButton( - child: Row( - spacing: 8.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.share_outlined, - ), - Text(L10n.of(context).shareInviteCode(spaceCode)), - ], - ), - onPressed: () async { - await Clipboard.setData(ClipboardData(text: spaceCode)); - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - L10n.of(context).copiedToClipboard, - ), - ), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart b/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart index fb3d61bbe..766c6dc14 100644 --- a/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart +++ b/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart @@ -99,11 +99,9 @@ class MessageAnalyticsFeedbackState extends State } void _showAnalyticsDialog(ConstructTypeEnum? type) { - showDialog( - context: context, - builder: (context) => AnalyticsPopupWrapper( - view: type ?? ConstructTypeEnum.vocab, - ), + AnalyticsPopupWrapper.show( + context, + view: type ?? ConstructTypeEnum.vocab, ); } diff --git a/lib/pangea/find_your_people/find_your_people_side_view.dart b/lib/pangea/common/widgets/pangea_side_view.dart similarity index 70% rename from lib/pangea/find_your_people/find_your_people_side_view.dart rename to lib/pangea/common/widgets/pangea_side_view.dart index 0514455e7..46c2f0feb 100644 --- a/lib/pangea/find_your_people/find_your_people_side_view.dart +++ b/lib/pangea/common/widgets/pangea_side_view.dart @@ -5,11 +5,27 @@ import 'package:go_router/go_router.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/analytics_page/analytics_page_constants.dart'; import 'package:fluffychat/pangea/find_your_people/find_your_people_constants.dart'; import 'package:fluffychat/widgets/navigation_rail.dart'; -class FindYourPeopleSideView extends StatelessWidget { - const FindYourPeopleSideView({super.key}); +class PangeaSideView extends StatelessWidget { + final String? path; + const PangeaSideView({ + super.key, + required this.path, + }); + + String get _asset { + const defaultAsset = FindYourPeopleConstants.sideBearFileName; + if (path == null || path!.isEmpty) return defaultAsset; + + if (path!.contains('analytics')) { + return AnalyticsPageConstants.dinoBotFileName; + } + + return defaultAsset; + } @override Widget build(BuildContext context) { @@ -32,8 +48,7 @@ class FindYourPeopleSideView extends StatelessWidget { child: SizedBox( width: 250.0, child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${FindYourPeopleConstants.sideBearFileName}", + imageUrl: "${AppConfig.assetsBaseURL}/$_asset", errorWidget: (context, url, error) => const SizedBox(), placeholder: (context, url) => const Center( child: CircularProgressIndicator.adaptive(), diff --git a/lib/pangea/constructs/construct_repo.dart b/lib/pangea/constructs/construct_repo.dart index 6d97ce0af..d847f8168 100644 --- a/lib/pangea/constructs/construct_repo.dart +++ b/lib/pangea/constructs/construct_repo.dart @@ -11,6 +11,8 @@ import 'package:fluffychat/widgets/matrix.dart'; class ConstructSummary { final int upperLevel; final int lowerLevel; + int? levelVocabConstructs; + int? levelGrammarConstructs; final String language; final String textSummary; final int writingConstructScore; @@ -21,6 +23,8 @@ class ConstructSummary { ConstructSummary({ required this.upperLevel, required this.lowerLevel, + this.levelVocabConstructs, + this.levelGrammarConstructs, required this.language, required this.textSummary, required this.writingConstructScore, @@ -33,6 +37,8 @@ class ConstructSummary { return { 'upper_level': upperLevel, 'lower_level': lowerLevel, + 'level_grammar_constructs': levelGrammarConstructs, + 'level_vocab_constructs': levelVocabConstructs, 'language': language, 'text_summary': textSummary, 'writing_construct_score': writingConstructScore, @@ -46,6 +52,8 @@ class ConstructSummary { return ConstructSummary( upperLevel: json['upper_level'], lowerLevel: json['lower_level'], + levelGrammarConstructs: json['level_grammar_constructs'], + levelVocabConstructs: json['level_vocab_constructs'], language: json['language'], textSummary: json['text_summary'], writingConstructScore: json['writing_construct_score'], diff --git a/lib/pangea/events/event_wrappers/pangea_message_event.dart b/lib/pangea/events/event_wrappers/pangea_message_event.dart index d0fff2281..9df0b2e0d 100644 --- a/lib/pangea/events/event_wrappers/pangea_message_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_message_event.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:developer'; +import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -18,6 +19,7 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; import 'package:fluffychat/pangea/events/models/stt_translation_model.dart'; import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; +import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/spaces/models/space_model.dart'; @@ -266,6 +268,21 @@ class PangeaMessageEvent { final botTranscription = SpeechToTextModel.fromJson( Map.from(rawBotTranscription), ); + + _representations?.add( + RepresentationEvent( + timeline: timeline, + parentMessageEvent: _event, + content: PangeaRepresentation( + langCode: botTranscription.langCode, + text: botTranscription.transcript.text, + originalSent: false, + originalWritten: false, + speechToText: botTranscription, + ), + ), + ); + return botTranscription; } @@ -776,4 +793,9 @@ class PangeaMessageEvent { tag: tag, ); } + + TextDirection get textDirection => + PLanguageStore.rtlLanguageCodes.contains(messageDisplayLangCode) + ? TextDirection.rtl + : TextDirection.ltr; } diff --git a/lib/pangea/learning_settings/models/language_model.dart b/lib/pangea/learning_settings/models/language_model.dart index 4029b9507..285577bfc 100644 --- a/lib/pangea/learning_settings/models/language_model.dart +++ b/lib/pangea/learning_settings/models/language_model.dart @@ -1,21 +1,25 @@ import 'package:flutter/material.dart'; -import 'package:fluffychat/l10n/l10n.dart'; +import 'package:collection/collection.dart'; + import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/enums/l2_support_enum.dart'; +import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; class LanguageModel { final String langCode; final String displayName; final String script; final L2SupportEnum l2Support; + final TextDirection? _textDirection; LanguageModel({ required this.langCode, required this.displayName, this.script = LanguageKeys.unknownLanguage, this.l2Support = L2SupportEnum.na, - }); + TextDirection? textDirection, + }) : _textDirection = textDirection; factory LanguageModel.fromJson(json) { final String code = json['language_code'] ?? @@ -31,6 +35,11 @@ class LanguageModel { ? L2SupportEnum.na.fromStorageString(json['l2_support']) : L2SupportEnum.na, script: json['script'] ?? LanguageKeys.unknownLanguage, + textDirection: json['text_direction'] != null + ? TextDirection.values.firstWhereOrNull( + (e) => e.name == json['text_direction'], + ) + : null, ); } @@ -39,6 +48,7 @@ class LanguageModel { 'language_name': displayName, 'script': script, 'l2_support': l2Support.storageString, + 'text_direction': textDirection.name, }; bool get l2 => l2Support != L2SupportEnum.na; @@ -60,19 +70,22 @@ class LanguageModel { displayName: "Unknown", ); - static LanguageModel multiLingual([BuildContext? context]) => LanguageModel( - displayName: context != null - ? L10n.of(context).multiLingualSpace - : "Multilingual Space", - langCode: LanguageKeys.multiLanguage, - ); - String? getDisplayName(BuildContext context) { return displayName; } String get langCodeShort => langCode.split('-').first; + TextDirection get _defaultTextDirection { + return PLanguageStore.rtlLanguageCodes.contains(langCodeShort) + ? TextDirection.rtl + : TextDirection.ltr; + } + + TextDirection get textDirection { + return _textDirection ?? _defaultTextDirection; + } + @override bool operator ==(Object other) { if (other is LanguageModel) { diff --git a/lib/pangea/learning_settings/utils/p_language_store.dart b/lib/pangea/learning_settings/utils/p_language_store.dart index ed66d2bf9..60310f18b 100644 --- a/lib/pangea/learning_settings/utils/p_language_store.dart +++ b/lib/pangea/learning_settings/utils/p_language_store.dart @@ -42,7 +42,6 @@ class PLanguageStore { _langList = _langList.toSet().toList(); _langList.sort((a, b) => a.displayName.compareTo(b.displayName)); - _langList.insert(0, LanguageModel.multiLingual()); } catch (err, stack) { debugger(when: kDebugMode); ErrorHandler.logError( @@ -107,4 +106,19 @@ class PLanguageStore { } return null; } + + static final List rtlLanguageCodes = [ + 'ar', + 'arc', + 'dv', + 'fa', + 'ha', + 'he', + 'khw', + 'ks', + 'ku', + 'ps', + 'ur', + 'yi', + ]; } diff --git a/lib/pangea/learning_settings/widgets/p_language_dropdown.dart b/lib/pangea/learning_settings/widgets/p_language_dropdown.dart index c71fea714..d47b48cf0 100644 --- a/lib/pangea/learning_settings/widgets/p_language_dropdown.dart +++ b/lib/pangea/learning_settings/widgets/p_language_dropdown.dart @@ -14,7 +14,6 @@ class PLanguageDropdown extends StatefulWidget { final List languages; final LanguageModel? initialLanguage; final Function(LanguageModel) onChange; - final bool showMultilingual; final bool isL2List; final String? decorationText; final String? error; @@ -28,7 +27,6 @@ class PLanguageDropdown extends StatefulWidget { required this.languages, required this.onChange, required this.initialLanguage, - this.showMultilingual = false, this.decorationText, this.isL2List = false, this.error, @@ -132,15 +130,6 @@ class PLanguageDropdownState extends State { ), ), items: [ - if (widget.showMultilingual) - DropdownMenuItem( - value: LanguageModel.multiLingual(context), - enabled: widget.enabled, - child: LanguageDropDownEntry( - languageModel: LanguageModel.multiLingual(context), - isL2List: widget.isL2List, - ), - ), ...sortedLanguages.map( (languageModel) => DropdownMenuItem( value: languageModel, diff --git a/lib/pangea/message_token_text/message_token_button.dart b/lib/pangea/message_token_text/message_token_button.dart index 2b2624948..e4ac8cb1a 100644 --- a/lib/pangea/message_token_text/message_token_button.dart +++ b/lib/pangea/message_token_text/message_token_button.dart @@ -31,7 +31,6 @@ class MessageTokenButton extends StatefulWidget { final TextStyle textStyle; final double width; final bool animateIn; - final PracticeTarget? practiceTargetForToken; const MessageTokenButton({ super.key, @@ -39,7 +38,6 @@ class MessageTokenButton extends StatefulWidget { required this.token, required this.textStyle, required this.width, - required this.practiceTargetForToken, this.animateIn = false, }); @@ -124,9 +122,10 @@ class MessageTokenButtonState extends State super.dispose(); } - bool get _animate => widget.animateIn || _finishedInitialAnimation; + PracticeTarget? get _activity => + widget.overlayController?.practiceTargetForToken(widget.token); - PracticeTarget? get _activity => widget.practiceTargetForToken; + bool get _animate => widget.animateIn || _finishedInitialAnimation; bool get _isActivityCompleteOrNullForToken => _activity?.isCompleteByToken( diff --git a/lib/pangea/public_spaces/public_space_card.dart b/lib/pangea/public_spaces/public_space_card.dart deleted file mode 100644 index 29521f67b..000000000 --- a/lib/pangea/public_spaces/public_space_card.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; -import 'package:fluffychat/pangea/extensions/pangea_rooms_chunk_extension.dart'; -import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; - -class PublicSpaceCard extends StatelessWidget { - final PublicRoomsChunk space; - final double width; - final double height; - - const PublicSpaceCard({ - super.key, - required this.space, - required this.width, - required this.height, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return PressableButton( - onPressed: () => PublicRoomBottomSheet.show( - roomAlias: space.canonicalAlias ?? space.roomId, - chunk: space, - context: context, - ), - borderRadius: BorderRadius.circular(24.0), - color: theme.brightness == Brightness.dark - ? theme.colorScheme.primary - : theme.colorScheme.surfaceContainerHighest, - colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), - ), - height: height, - width: width, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(24.0), - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: height, - width: height, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: space.avatarUrl != null - ? MxcImage( - uri: space.avatarUrl!, - width: width, - height: width, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageUrl: space.defaultAvatar(), - width: width, - height: width, - fit: BoxFit.cover, - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - spacing: 4.0, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - spacing: 4.0, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Flexible( - child: Text( - space.name ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - Container( - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(24.0), - ), - padding: const EdgeInsets.symmetric( - vertical: 2.0, - horizontal: 8.0, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8.0, - children: [ - const Icon( - Icons.group_outlined, - size: 12.0, - ), - Text( - L10n.of(context).countParticipants( - space.numJoinedMembers, - ), - style: theme.textTheme.labelSmall, - ), - ], - ), - ), - ], - ), - Flexible( - child: Text( - space.topic ?? - L10n.of(context).noSpaceDescriptionYet, - style: theme.textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - maxLines: 5, - ), - ), - ], - ), - ), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/pangea/public_spaces/public_spaces_area.dart b/lib/pangea/public_spaces/public_spaces_area.dart deleted file mode 100644 index 0e8fadf6b..000000000 --- a/lib/pangea/public_spaces/public_spaces_area.dart +++ /dev/null @@ -1,215 +0,0 @@ -// shows n rows of activity suggestions vertically, where n is the number of rows -// as the user tries to scroll horizontally to the right, the client will fetch more activity suggestions - -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; -import 'package:shimmer/shimmer.dart'; - -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/public_spaces/public_space_card.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class PublicSpacesArea extends StatefulWidget { - const PublicSpacesArea({super.key}); - - @override - PublicSpacesAreaState createState() => PublicSpacesAreaState(); -} - -class PublicSpacesAreaState extends State { - @override - void initState() { - super.initState(); - _setSpaceItems(); - } - - @override - void dispose() { - _scrollController.dispose(); - _searchController.dispose(); - _coolDown?.cancel(); - super.dispose(); - } - - bool _loading = true; - bool _isSearching = false; - - final List _spaceItems = []; - - final ScrollController _scrollController = ScrollController(); - final TextEditingController _searchController = TextEditingController(); - Timer? _coolDown; - - final double cardHeight = 150.0; - final double cardWidth = 325.0; - - Future _setSpaceItems() async { - _spaceItems.clear(); - setState(() => _loading = true); - try { - final resp = await Matrix.of(context).client.queryPublicRooms( - filter: PublicRoomQueryFilter( - roomTypes: ['m.space'], - genericSearchTerm: _searchController.text, - ), - limit: 100, - ); - _spaceItems.addAll(resp.chunk); - _spaceItems.sort((a, b) { - int getPriority(item) { - final bool hasTopic = item.topic != null && item.topic!.isNotEmpty; - final bool hasAvatar = item.avatarUrl != null; - - if (hasTopic && hasAvatar) return 0; // Highest priority - if (hasAvatar) return 1; // Second priority - if (hasTopic) return 2; // Third priority - return 3; // Lowest priority - } - - return getPriority(a).compareTo(getPriority(b)); - }); - } finally { - if (mounted) setState(() => _loading = false); - } - } - - void _onSearchEnter(String text, {bool globalSearch = true}) { - if (text.isEmpty) { - _setSpaceItems(); - return; - } - - _coolDown?.cancel(); - _coolDown = Timer(const Duration(milliseconds: 500), _setSpaceItems); - } - - void _toggleSearching() { - setState(() { - _isSearching = !_isSearching; - _searchController.clear(); - _setSpaceItems(); - }); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isColumnMode = FluffyThemes.isColumnMode(context); - - final List cards = _loading && _spaceItems.isEmpty - ? List.generate(5, (i) { - return Shimmer.fromColors( - baseColor: theme.colorScheme.primary.withAlpha(20), - highlightColor: theme.colorScheme.primary.withAlpha(50), - child: Container( - height: cardHeight, - width: cardWidth, - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(24.0), - ), - ), - ); - }) - : _spaceItems - .map((space) { - return PublicSpaceCard( - space: space, - width: cardWidth, - height: cardHeight, - ); - }) - .cast() - .toList(); - - if (_loading && _spaceItems.isNotEmpty) { - cards.add( - const Padding( - padding: EdgeInsets.all(16.0), - child: CircularProgressIndicator.adaptive(), - ), - ); - } - - return Column( - spacing: 8.0, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: child, - ), - child: _isSearching - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - key: const ValueKey('search'), - children: [ - Expanded( - child: TextField( - autofocus: true, - controller: _searchController, - onChanged: _onSearchEnter, - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 12.0, - ), - isDense: true, - border: OutlineInputBorder(), - ), - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: _toggleSearching, - ), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - key: const ValueKey('title'), - children: [ - Text( - L10n.of(context).findYourPeople, - style: isColumnMode - ? theme.textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold) - : theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - IconButton( - icon: const Icon(Icons.search), - onPressed: _toggleSearching, - ), - ], - ), - ), - Container( - decoration: const BoxDecoration(), - child: Scrollbar( - thumbVisibility: true, - controller: _scrollController, - child: Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: SingleChildScrollView( - controller: _scrollController, - scrollDirection: Axis.horizontal, - child: Row( - spacing: 8.0, - children: cards, - ), - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/pangea/toolbar/widgets/message_audio_card.dart b/lib/pangea/toolbar/widgets/message_audio_card.dart index 2cc529422..3b854b5bf 100644 --- a/lib/pangea/toolbar/widgets/message_audio_card.dart +++ b/lib/pangea/toolbar/widgets/message_audio_card.dart @@ -8,7 +8,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; @@ -96,9 +95,7 @@ class MessageAudioCardState extends State { ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onPrimary, ) - : const CardErrorWidget( - error: "Null audio file in message_audio_card", - ); + : const SizedBox(); } } diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index d4b982364..fc05ad1bd 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -22,6 +22,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_activity_model.da import 'package:fluffychat/pangea/practice_activities/practice_choice.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart'; +import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; @@ -604,6 +605,13 @@ class MessageOverlayController extends State }); } + PracticeTarget? practiceTargetForToken(PangeaToken token) { + if (toolbarMode.associatedActivityType == null) return null; + return practiceSelection + ?.activities(toolbarMode.associatedActivityType!) + .firstWhereOrNull((a) => a.tokens.contains(token)); + } + /// Whether the given token is currently selected or highlighted bool isTokenSelected(PangeaToken token) { final isSelected = _selectedSpan?.offset == token.text.offset && diff --git a/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart b/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart index fef3814c1..74e3faa80 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart @@ -278,16 +278,14 @@ class MorphMeaningPopupState extends State { null) ConstructXpWidget( id: widget.cId, - onTap: () => showDialog( - context: context, - builder: (context) => AnalyticsPopupWrapper( - constructZoom: widget.cId, - view: ConstructTypeEnum.morph, - backButtonOverride: IconButton( - icon: const Icon(Icons.close), - onPressed: () => - Navigator.of(context).pop(), - ), + onTap: () => AnalyticsPopupWrapper.show( + context, + constructZoom: widget.cId, + view: ConstructTypeEnum.morph, + backButtonOverride: IconButton( + icon: const Icon(Icons.close), + onPressed: () => + Navigator.of(context).pop(), ), ), ), diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index 64d3f8f1b..a164cfd4e 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -94,12 +94,10 @@ class WordZoomWidget extends StatelessWidget { ), ConstructXpWidget( id: token.vocabConstructID, - onTap: () => showDialog( - context: context, - builder: (context) => AnalyticsPopupWrapper( - constructZoom: token.vocabConstructID, - view: ConstructTypeEnum.vocab, - ), + onTap: () => AnalyticsPopupWrapper.show( + context, + constructZoom: token.vocabConstructID, + view: ConstructTypeEnum.vocab, ), ), ], diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index bde4dd1c5..974ef75d3 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -129,6 +129,7 @@ abstract class ClientManager { PangeaEventTypes.userSetLemmaInfo, EventTypes.RoomJoinRules, PangeaEventTypes.activityPlan, + PangeaEventTypes.constructSummary, // Pangea# }, logLevel: kReleaseMode ? Level.warning : Level.verbose, diff --git a/lib/widgets/navigation_rail.dart b/lib/widgets/navigation_rail.dart index ac14abdb9..7d8a93f39 100644 --- a/lib/widgets/navigation_rail.dart +++ b/lib/widgets/navigation_rail.dart @@ -42,7 +42,7 @@ class SpacesNavigationRail extends StatelessWidget { .startsWith('/rooms/settings'); // #Pangea final path = GoRouter.of(context).routeInformationProvider.value.uri.path; - final isHomepage = path.contains('homepage'); + final isAnalytics = path.contains('analytics'); final isCommunities = path.contains('communities'); final isColumnMode = FluffyThemes.isColumnMode(context); @@ -89,10 +89,10 @@ class SpacesNavigationRail extends StatelessWidget { // #Pangea if (i == 0) { return NaviRailItem( - isSelected: isHomepage, + isSelected: isAnalytics, onTap: () { clearActiveSpace?.call(); - context.go("/rooms/homepage"); + context.go("/rooms/analytics"); }, backgroundColor: Colors.transparent, icon: FutureBuilder( @@ -125,7 +125,7 @@ class SpacesNavigationRail extends StatelessWidget { // isSelected: activeSpaceId == null && !isSettings, isSelected: activeSpaceId == null && !isSettings && - !isHomepage && + !isAnalytics && !isCommunities, // Pangea# onTap: onGoToChats, diff --git a/pubspec.lock b/pubspec.lock index 101510960..17a90ab35 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -38,6 +38,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.3.0" + animated_flip_counter: + dependency: "direct main" + description: + name: animated_flip_counter + sha256: "73f852d84c461c3e4c1ddf320bee334dde8dba89441922ab11a8013be0b2fad1" + url: "https://pub.dev" + source: hosted + version: "0.3.4" animations: dependency: "direct main" description: @@ -334,6 +342,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + confetti: + dependency: "direct main" + description: + name: confetti + sha256: "79376a99648efbc3f23582f5784ced0fe239922bd1a0fb41f582051eba750751" + url: "https://pub.dev" + source: hosted + version: "0.8.0" console: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2e67fdca8..62555872c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: 4.1.10+2 +version: 4.1.12+1 environment: sdk: ">=3.0.0 <4.0.0" @@ -21,6 +21,7 @@ dependencies: chewie: ^1.11.3 collection: ^1.18.0 cross_file: ^0.3.4+2 + confetti: ^0.8.0 cupertino_icons: any # #Pangea # desktop_drop: ^0.4.4 @@ -135,6 +136,7 @@ dependencies: text_to_speech: git: https://github.com/pangeachat/text_to_speech.git flutter_tts: ^4.2.0 + animated_flip_counter: ^0.3.4 # Pangea# dev_dependencies: diff --git a/web/index.html b/web/index.html index 1a3730e5e..cdb551c4d 100644 --- a/web/index.html +++ b/web/index.html @@ -70,6 +70,33 @@ }); }); + + + + +