From 25b1c63df4152aeb044395c57449c0bb45469e21 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Wed, 19 Mar 2025 09:52:35 -0400 Subject: [PATCH] chore: add images to activity planner page (#2169) --- .../activity_planner/activity_list_view.dart | 146 +++++++++++++++++- .../activity_plan_message.dart | 53 ++++--- ...activity_plan_page_launch_icon_button.dart | 4 + .../activity_planner_page.dart | 7 +- .../activity_suggestions_constants.dart | 1 + .../progress_indicators_enum.dart | 13 -- 6 files changed, 182 insertions(+), 42 deletions(-) diff --git a/lib/pangea/activity_planner/activity_list_view.dart b/lib/pangea/activity_planner/activity_list_view.dart index 02709817c..eb137f7f6 100644 --- a/lib/pangea/activity_planner/activity_list_view.dart +++ b/lib/pangea/activity_planner/activity_list_view.dart @@ -3,17 +3,26 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; 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:http/http.dart' as http; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.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/activity_plan_response.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart'; import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart'; +import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'activity_plan_card.dart'; @@ -23,10 +32,13 @@ class ActivityListView extends StatefulWidget { /// if null, show saved activities final ActivityPlanRequest? activityPlanRequest; + final ActivityPlannerPageState controller; + const ActivityListView({ super.key, required this.room, required this.activityPlanRequest, + required this.controller, }); @override @@ -41,10 +53,15 @@ class ActivityListViewState extends State { bool _isLoading = true; Object? _error; + Uint8List? _avatar; + String? _avatarURL; + String? _filename; + @override void initState() { super.initState(); _loadActivities(); + _setModeImageURL(); } Future _loadActivities() async { @@ -108,12 +125,79 @@ class ActivityListViewState extends State { return; } - await widget.room?.setPinnedEvents([eventId]); + Uint8List? bytes = _avatar; + if (_avatarURL != null && bytes == null) { + final resp = await http + .get(Uri.parse(_avatarURL!)) + .timeout(const Duration(seconds: 5)); + bytes = resp.bodyBytes; + } + + if (bytes != null && _filename != null) { + final file = MatrixFile( + bytes: bytes, + name: _filename!, + ); + + await widget.room?.sendFileEvent( + file, + shrinkImageMaxDimension: 1600, + extraContent: { + ModelKey.messageTags: ModelKey.messageTagActivityPlan, + }, + ); + } + + if (widget.room != null && widget.room!.canSendDefaultStates) { + await widget.room?.setPinnedEvents([eventId]); + } Navigator.of(context).pop(); }, ); + Future get _selectedMode async { + final modes = await widget.controller.modeItems; + return modes.firstWhereOrNull( + (element) => + element.name.toLowerCase() == + widget.activityPlanRequest?.mode.toLowerCase(), + ); + } + + Future _setModeImageURL() async { + final mode = await _selectedMode; + if (mode == null) return; + + final modeName = + mode.defaultName.toLowerCase().replaceAll(RegExp(r'\s+'), ''); + final filename = + "${ActivitySuggestionsConstants.modeImageFileStart}$modeName.jpg"; + + if (!mounted) return; + setState(() { + _avatarURL = "${AppConfig.assetsBaseURL}/$filename"; + _filename = filename; + }); + } + + void selectPhoto() async { + final resp = await selectFiles( + context, + type: FileSelectorType.images, + allowMultiple: false, + ); + + final photo = resp.singleOrNull; + if (photo == null) return; + final bytes = await photo.readAsBytes(); + + setState(() { + _avatar = bytes; + _filename = photo.name; + }); + } + @override Widget build(BuildContext context) { final l10n = L10n.of(context); @@ -152,8 +236,66 @@ class ActivityListViewState extends State { padding: const EdgeInsets.all(16), itemCount: widget.activityPlanRequest == null ? _bookmarkedActivities.length - : _activities!.length, + : _activities!.length + 1, itemBuilder: (context, index) { + if (index == 0) { + return Center( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Column( + children: [ + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + ), + width: 400.0, + clipBehavior: Clip.hardEdge, + child: _avatarURL != null || _avatar != null + ? ClipRRect( + child: _avatar == null + ? CachedNetworkImage( + fit: BoxFit.cover, + imageUrl: _avatarURL!, + placeholder: (context, url) { + return const Center( + child: + CircularProgressIndicator(), + ); + }, + errorWidget: (context, url, error) => + const SizedBox(), + ) + : Image.memory( + _avatar!, + fit: BoxFit.cover, + ), + ) + : const Padding( + padding: EdgeInsets.all(16.0), + ), + ), + ), + const SizedBox(height: 16.0), + ], + ), + InkWell( + borderRadius: BorderRadius.circular(90), + onTap: _isLoading ? null : selectPhoto, + child: const CircleAvatar( + radius: 32.0, + child: Icon(Icons.add_a_photo_outlined), + ), + ), + ], + ), + ); + } + + index--; + return ActivityPlanCard( activity: widget.activityPlanRequest == null ? _bookmarkedActivities[index] diff --git a/lib/pangea/activity_planner/activity_plan_message.dart b/lib/pangea/activity_planner/activity_plan_message.dart index 744c1871b..d4331c4eb 100644 --- a/lib/pangea/activity_planner/activity_plan_message.dart +++ b/lib/pangea/activity_planner/activity_plan_message.dart @@ -130,10 +130,13 @@ class ActivityPlanMessage extends StatelessWidget { AppConfig.borderRadius, ), ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: + event.messageType == MessageTypes.Image + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), constraints: const BoxConstraints( maxWidth: FluffyThemes.columnWidth * 1.5, ), @@ -218,32 +221,34 @@ class ActivityPlanMessage extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Center( - child: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius * 2), - color: theme.colorScheme.surface.withAlpha(128), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 2.0, - ), - child: Text( - event.originServerTs.localizedTime(context), - style: TextStyle( - fontSize: 12 * AppConfig.fontSizeFactor, - fontWeight: FontWeight.bold, - color: theme.colorScheme.secondary, + if (event.messageType == MessageTypes.Text) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Center( + child: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius * 2), + color: theme.colorScheme.surface.withAlpha(128), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 2.0, + ), + child: Text( + event.originServerTs.localizedTime(context), + style: TextStyle( + fontSize: 12 * AppConfig.fontSizeFactor, + fontWeight: FontWeight.bold, + color: theme.colorScheme.secondary, + ), ), ), ), ), ), ), - ), row, ], ); diff --git a/lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart b/lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart index f78df0ee5..8a498c99d 100644 --- a/lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart +++ b/lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart @@ -16,6 +16,10 @@ class ActivityPlanPageLaunchIconButton extends StatelessWidget { @override Widget build(BuildContext context) { + if (!controller.room.canSendDefaultStates) { + return const SizedBox(); + } + return FutureBuilder( future: controller.room.isBotDM, builder: (BuildContext context, snapshot) { diff --git a/lib/pangea/activity_planner/activity_planner_page.dart b/lib/pangea/activity_planner/activity_planner_page.dart index b03f5869e..a136156c8 100644 --- a/lib/pangea/activity_planner/activity_planner_page.dart +++ b/lib/pangea/activity_planner/activity_planner_page.dart @@ -90,7 +90,7 @@ class ActivityPlannerPageState extends State { Future> get _topicItems => TopicListRepo.get(req); - Future> get _modeItems => + Future> get modeItems => ActivityModeListRepo.get(req); Future> get _objectiveItems => @@ -112,7 +112,7 @@ class ActivityPlannerPageState extends State { } Future _randomMode() async { - final modes = await _modeItems; + final modes = await modeItems; return (modes..shuffle()).first.name; } @@ -197,6 +197,7 @@ class ActivityPlannerPageState extends State { cefrLevel: _selectedCefrLevel!, numberOfParticipants: _selectedNumberOfParticipants!, ), + controller: this, ) : Center( child: ConstrainedBox( @@ -233,7 +234,7 @@ class ActivityPlannerPageState extends State { ), const SizedBox(height: 24), SuggestionFormField( - suggestions: _modeItems, + suggestions: modeItems, validator: _validateNotNull, label: l10n.modeLabel, placeholder: l10n.modePlaceholder, diff --git a/lib/pangea/activity_suggestions/activity_suggestions_constants.dart b/lib/pangea/activity_suggestions/activity_suggestions_constants.dart index bb3d9f18a..ecb4c8c90 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_constants.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_constants.dart @@ -1,3 +1,4 @@ class ActivitySuggestionsConstants { static const String plusIconPath = "add_icon.svg"; + static const String modeImageFileStart = "activityplanner_mode_"; } diff --git a/lib/pangea/analytics_summary/progress_indicators_enum.dart b/lib/pangea/analytics_summary/progress_indicators_enum.dart index 28a53e399..74b66d118 100644 --- a/lib/pangea/analytics_summary/progress_indicators_enum.dart +++ b/lib/pangea/analytics_summary/progress_indicators_enum.dart @@ -3,8 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; enum ProgressIndicatorEnum { @@ -25,17 +23,6 @@ extension ProgressIndicatorsExtension on ProgressIndicatorEnum { } } - String? get iconURL { - switch (this) { - case ProgressIndicatorEnum.wordsUsed: - return '${AppConfig.assetsBaseURL}/${AnalyticsConstants.vocabIconFileName}'; - case ProgressIndicatorEnum.morphsUsed: - return '${AppConfig.assetsBaseURL}/${AnalyticsConstants.morphIconFileName}'; - case ProgressIndicatorEnum.level: - return null; - } - } - static bool isDarkMode(BuildContext context) => Theme.of(context).brightness == Brightness.dark;