diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 448987a4c..857cdb99e 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4833,5 +4833,8 @@ "youUnlocked": "You've unlocked", "resetInstructionTooltipsTitle": "Reset instruction tooltips", "resetInstructionTooltipsDesc": "Click to show instruction tooltips like for a brand new user.", - "selectForGrammar": "Select a grammar icon for activities and details." + "selectForGrammar": "Select a grammar icon for activities and details.", + "newChatActivityTitle": "Add a fun activity", + "newChatActivityDesc": "Make every group chat an adventure with Activity Planner! Set captivating topics and objectives for the group, and bring conversations to life with stunning images. Spark imaginative discussions and keep the fun flowing effortlessly!", + "exploreMore": "Explore more" } \ No newline at end of file diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index ceba05e5c..288611718 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -10,6 +10,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/new_group/new_group_view.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; @@ -36,6 +37,7 @@ class NewGroupController extends State { TextEditingController nameController = TextEditingController(); // #Pangea + ActivityPlanModel? _selectedActivity; bool requiredCodeToJoin = false; // bool publicGroup = false; // Pangea# @@ -61,6 +63,9 @@ class NewGroupController extends State { // void setPublicGroup(bool b) => // setState(() => publicGroup = groupCanBeFound = b); void setRequireCode(bool b) => setState(() => requiredCodeToJoin = b); + + void setSelectedActivity(ActivityPlanModel? activity) => + setState(() => _selectedActivity = activity); // Pangea# void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b); @@ -113,6 +118,15 @@ class NewGroupController extends State { ); if (!mounted) return; // #Pangea + if (_selectedActivity != null) { + Room? room = Matrix.of(context).client.getRoomById(roomId); + if (room == null) { + await Matrix.of(context).client.waitForRoomInSync(roomId); + room = Matrix.of(context).client.getRoomById(roomId); + } + if (room == null) return; + await room.sendActivityPlan(_selectedActivity!); + } // if a timeout happened, don't redirect to the chat if (error != null) return; // Pangea# diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 20a164454..e2faec7ed 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_carousel.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; @@ -36,6 +37,9 @@ class NewGroupView extends StatelessWidget { ), ), body: MaxWidthBody( + // #Pangea + showBorder: false, + // Pangea# child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -171,6 +175,13 @@ class NewGroupView extends StatelessWidget { // onChanged: null, // ), // ), + if (controller.createGroupType == CreateGroupType.group) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: ActivitySuggestionCarousel( + onActivitySelected: controller.setSelectedActivity, + ), + ), // Pangea# AnimatedSize( duration: FluffyThemes.animationDuration, diff --git a/lib/pangea/activity_suggestions/activity_suggestion_card.dart b/lib/pangea/activity_suggestions/activity_suggestion_card.dart index 5dcaa03ee..66b4e113b 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_card.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_card.dart @@ -79,7 +79,7 @@ class ActivitySuggestionCard extends StatelessWidget { style: const TextStyle( fontWeight: FontWeight.bold, ), - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart b/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart new file mode 100644 index 000000000..ac595c3ab --- /dev/null +++ b/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart @@ -0,0 +1,285 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shimmer/shimmer.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart'; +import 'package:fluffychat/pangea/activity_planner/media_enum.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_plan_search_repo.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart'; +import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class ActivitySuggestionCarousel extends StatefulWidget { + final Function(ActivityPlanModel?) onActivitySelected; + const ActivitySuggestionCarousel({ + required this.onActivitySelected, + super.key, + }); + + @override + ActivitySuggestionCarouselState createState() => + ActivitySuggestionCarouselState(); +} + +class ActivitySuggestionCarouselState + extends State { + bool _isOpen = true; + bool _loading = true; + String? _error; + + double get _cardWidth => _isColumnMode ? 250.0 : 200.0; + final double _cardHeight = 275.0; + + ActivityPlanModel? _currentActivity; + final List _activityItems = []; + + @override + void initState() { + super.initState(); + _setActivityItems(); + } + + Future _setActivityItems() async { + try { + final ActivityPlanRequest request = ActivityPlanRequest( + topic: "", + mode: "", + objective: "", + media: MediaEnum.nan, + cefrLevel: LanguageLevelTypeEnum.a1, + languageOfInstructions: LanguageKeys.defaultLanguage, + targetLanguage: + MatrixState.pangeaController.languageController.userL2?.langCode ?? + LanguageKeys.defaultLanguage, + numberOfParticipants: 3, + count: 5, + ); + final resp = await ActivitySearchRepo.get(request); + _activityItems.addAll(resp.activityPlans); + } catch (e) { + _error = e.toString(); + } finally { + _loading = false; + _currentActivity = + _activityItems.isNotEmpty ? _activityItems.first : null; + if (mounted) setState(() {}); + } + } + + bool get _isColumnMode => FluffyThemes.isColumnMode(context); + + int? get _currentIndex { + if (_currentActivity == null) return null; + final index = _activityItems.indexOf(_currentActivity!); + return index == -1 ? null : index; + } + + bool get _canMoveLeft => _currentIndex != null && _currentIndex! > 0; + + bool get _canMoveRight => + _currentIndex != null && _currentIndex! < _activityItems.length - 1; + + void _moveLeft() { + if (!_canMoveLeft) return; + _setActivityByIndex(_currentIndex! - 1); + } + + void _moveRight() { + if (!_canMoveRight) return; + _setActivityByIndex(_currentIndex! + 1); + } + + void _setActivityByIndex(int index) { + if (index < 0 || index >= _activityItems.length) return; + widget.onActivitySelected(_activityItems[index]); + setState(() { + _currentActivity = _activityItems[index]; + }); + } + + void _close() { + widget.onActivitySelected(null); + setState(() => _isOpen = false); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return AnimatedSize( + duration: FluffyThemes.animationDuration, + child: !_isOpen + ? const SizedBox.shrink() + : Container( + decoration: BoxDecoration( + border: Border.all(color: theme.dividerColor), + borderRadius: BorderRadius.circular(24.0), + ), + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 16.0, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + L10n.of(context).newChatActivityTitle, + style: theme.textTheme.titleLarge, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: _close, + ), + ], + ), + Text(L10n.of(context).newChatActivityDesc), + Row( + spacing: _isColumnMode ? 16.0 : 4.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MouseRegion( + cursor: _canMoveLeft + ? SystemMouseCursors.click + : SystemMouseCursors.basic, + child: GestureDetector( + onTap: _canMoveLeft ? _moveLeft : null, + child: Icon( + Icons.chevron_left_outlined, + size: 32.0, + color: _canMoveLeft ? null : theme.disabledColor, + ), + ), + ), + Container( + constraints: + BoxConstraints(maxHeight: _cardHeight + 12.0), + child: _error != null || + (_currentActivity == null && !_loading) + ? const SizedBox.shrink() + : _loading + ? Shimmer.fromColors( + baseColor: + theme.colorScheme.primary.withAlpha(50), + highlightColor: theme.colorScheme.primary + .withAlpha(150), + child: Container( + height: _cardHeight, + width: _cardWidth, + decoration: BoxDecoration( + color: + theme.colorScheme.surfaceContainer, + borderRadius: + BorderRadius.circular(24.0), + ), + ), + ) + : ActivitySuggestionCard( + activity: _currentActivity!, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return ActivitySuggestionDialog( + activity: _currentActivity!, + ); + }, + ); + }, + width: _cardWidth, + height: _cardHeight, + padding: 0.0, + ), + ), + MouseRegion( + cursor: _canMoveRight + ? SystemMouseCursors.click + : SystemMouseCursors.basic, + child: GestureDetector( + onTap: _canMoveRight ? _moveRight : null, + child: Icon( + Icons.chevron_right_outlined, + size: 32.0, + color: _canMoveRight ? null : theme.disabledColor, + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 16.0, + children: _activityItems.mapIndexed((i, activity) { + final selected = activity == _currentActivity; + return InkWell( + borderRadius: BorderRadius.circular(12.0), + onTap: () => _setActivityByIndex(i), + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: selected ? 0.0 : 0.5, + sigmaY: selected ? 0.0 : 0.5, + ), + child: Opacity( + opacity: selected ? 1.0 : 0.5, + child: ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius(12.0), + child: activity.imageURL != null + ? CachedNetworkImage( + imageUrl: activity.imageURL!, + errorWidget: (context, url, error) { + return CircleAvatar( + backgroundColor: + theme.colorScheme.secondary, + radius: 12.0, + ); + }, + progressIndicatorBuilder: + (context, url, progress) { + return CircularProgressIndicator( + value: progress.progress, + ); + }, + ) + : CircleAvatar( + backgroundColor: + theme.colorScheme.secondary, + radius: 12.0, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + ElevatedButton( + onPressed: () => _isColumnMode + ? context.go("/rooms") + : context.go("/rooms/homepage"), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primaryContainer, + ), + child: _loading + ? const LinearProgressIndicator() + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).exploreMore), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/activity_suggestions/activity_suggestions_area.dart b/lib/pangea/activity_suggestions/activity_suggestions_area.dart index fbf82c072..eaa81b058 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_area.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_area.dart @@ -38,7 +38,7 @@ class ActivitySuggestionsAreaState extends State { final List _activityItems = []; final ScrollController _scrollController = ScrollController(); - final double cardHeight = 235.0; + final double cardHeight = 250.0; double get cardPadding => _isColumnMode ? 8.0 : 0.0; double get cardWidth => _isColumnMode ? 225.0 : 150.0; diff --git a/lib/widgets/layouts/max_width_body.dart b/lib/widgets/layouts/max_width_body.dart index 5720d94bb..8d9d2b84c 100644 --- a/lib/widgets/layouts/max_width_body.dart +++ b/lib/widgets/layouts/max_width_body.dart @@ -8,12 +8,18 @@ class MaxWidthBody extends StatelessWidget { final double maxWidth; final bool withScrolling; final EdgeInsets? innerPadding; + // #Pangea + final bool showBorder; + // Pangea# const MaxWidthBody({ required this.child, this.maxWidth = 600, this.withScrolling = true, this.innerPadding, + // #Pangea + this.showBorder = true, + // Pangea# super.key, }); @override @@ -38,7 +44,12 @@ class MaxWidthBody extends StatelessWidget { borderRadius: BorderRadius.circular(AppConfig.borderRadius), side: BorderSide( - color: theme.dividerColor, + // #Pangea + // color: theme.dividerColor, + color: showBorder + ? theme.dividerColor + : Colors.transparent, + // Pangea# ), ), clipBehavior: Clip.hardEdge,