diff --git a/lib/pangea/activity_planner/activity_list_view.dart b/lib/pangea/activity_planner/activity_list_view.dart index f851037ab..42574bf34 100644 --- a/lib/pangea/activity_planner/activity_list_view.dart +++ b/lib/pangea/activity_planner/activity_list_view.dart @@ -1,13 +1,10 @@ -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: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'; @@ -17,9 +14,6 @@ import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.da 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/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'; class ActivityListView extends StatefulWidget { @@ -49,7 +43,6 @@ class ActivityListViewState extends State { bool _isLoading = true; Object? _error; - Uint8List? _avatar; String? _avatarURL; String? _filename; @@ -103,22 +96,6 @@ class ActivityListViewState extends State { setState(() {}); } - Future _onLaunch(int index) => showFutureLoadingDialog( - context: context, - future: () async { - final activity = _activities![index]; - - await widget.room?.sendActivityPlan( - activity, - avatar: _avatar, - avatarURL: _avatarURL, - filename: _filename, - ); - - Navigator.of(context).pop(); - }, - ); - Future get _selectedMode async { final modes = await widget.controller.modeItems; return modes.firstWhereOrNull( @@ -144,23 +121,6 @@ class ActivityListViewState extends State { }); } - 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); @@ -199,72 +159,16 @@ class ActivityListViewState extends State { padding: const EdgeInsets.all(16), itemCount: widget.activityPlanRequest == null ? _bookmarkedActivities.length - : _activities!.length + 1, + : _activities!.length, 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] : _activities![index], - onLaunch: () => _onLaunch(index), + room: widget.room, onEdit: (updatedActivity) => _onEdit(index, updatedActivity), + avatarURL: _avatarURL, + initialFilename: _filename, ); }, ); diff --git a/lib/pangea/activity_planner/activity_plan_card.dart b/lib/pangea/activity_planner/activity_plan_card.dart index d7c2f8c78..24571d1f8 100644 --- a/lib/pangea/activity_planner/activity_plan_card.dart +++ b/lib/pangea/activity_planner/activity_plan_card.dart @@ -3,25 +3,35 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.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'; class ActivityPlanCard extends StatefulWidget { final ActivityPlanModel activity; - final VoidCallback onLaunch; + final Room? room; final ValueChanged onEdit; final double maxWidth; + final String? avatarURL; + final String? initialFilename; const ActivityPlanCard({ super.key, required this.activity, - required this.onLaunch, + required this.room, required this.onEdit, this.maxWidth = 400, + this.avatarURL, + this.initialFilename, }); @override @@ -37,6 +47,9 @@ class ActivityPlanCardState extends State { final TextEditingController _newVocabController = TextEditingController(); final FocusNode _vocabFocusNode = FocusNode(); + Uint8List? _avatar; + String? _filename; + @override void initState() { super.initState(); @@ -46,6 +59,7 @@ class ActivityPlanCardState extends State { TextEditingController(text: _tempActivity.learningObjective); _instructionsController = TextEditingController(text: _tempActivity.instructions); + _filename = widget.initialFilename; } static const double itemPadding = 12; @@ -111,6 +125,37 @@ class ActivityPlanCardState extends State { }); } + 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; + }); + } + + Future _onLaunch() => showFutureLoadingDialog( + context: context, + future: () async { + await widget.room?.sendActivityPlan( + widget.activity, + avatar: _avatar, + avatarURL: widget.avatarURL, + filename: _filename, + ); + + Navigator.of(context).pop(); + }, + ); + bool get isBookmarked => BookmarkedActivitiesRepo.isBookmarked(widget.activity); @@ -122,199 +167,260 @@ class ActivityPlanCardState extends State { constraints: BoxConstraints(maxWidth: widget.maxWidth), child: Card( margin: const EdgeInsets.symmetric(vertical: itemPadding), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, + child: Column( + children: [ + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Stack( children: [ - const Icon(Icons.event_note_outlined), - const SizedBox(width: itemPadding), - Expanded( - child: _isEditing - ? TextField( - controller: _titleController, - decoration: InputDecoration( - labelText: L10n.of(context).activityTitle, - ), - maxLines: null, + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + ), + clipBehavior: Clip.hardEdge, + alignment: Alignment.center, + child: widget.avatarURL != null || _avatar != null + ? ClipRRect( + child: _avatar == null + ? CachedNetworkImage( + fit: BoxFit.cover, + imageUrl: widget.avatarURL!, + placeholder: (context, url) { + return const Center( + child: CircularProgressIndicator(), + ); + }, + errorWidget: (context, url, error) { + return const Padding( + padding: EdgeInsets.all(28.0), + ); + }, + ) + : Image.memory( + _avatar!, + fit: BoxFit.cover, + ), ) - : Text( - widget.activity.title, - style: Theme.of(context).textTheme.bodyLarge, + : const Padding( + padding: EdgeInsets.all(28.0), ), ), - if (!_isEditing) - IconButton( - onPressed: isBookmarked - ? () => _removeBookmark() - : () => _addBookmark(widget.activity), - icon: Icon( - isBookmarked ? Icons.bookmark : Icons.bookmark_border, + Positioned( + top: 10.0, + right: 10.0, + child: IconButton( + icon: const Icon(Icons.upload_outlined), + onPressed: selectPhoto, + style: + IconButton.styleFrom(backgroundColor: Colors.black), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon(Icons.event_note_outlined), + const SizedBox(width: itemPadding), + Expanded( + child: _isEditing + ? TextField( + controller: _titleController, + decoration: InputDecoration( + labelText: L10n.of(context).activityTitle, + ), + maxLines: null, + ) + : Text( + widget.activity.title, + style: Theme.of(context).textTheme.bodyLarge, + ), ), - ), - ], - ), - const SizedBox(height: itemPadding), - Row( - children: [ - Icon( - Symbols.target, - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: itemPadding), - Expanded( - child: _isEditing - ? TextField( - controller: _learningObjectiveController, - decoration: InputDecoration( - labelText: l10n.learningObjectiveLabel, - ), - maxLines: null, - ) - : Text( - widget.activity.learningObjective, - style: Theme.of(context).textTheme.bodyMedium, + if (!_isEditing) + IconButton( + onPressed: isBookmarked + ? () => _removeBookmark() + : () => _addBookmark(widget.activity), + icon: Icon( + isBookmarked + ? Icons.bookmark + : Icons.bookmark_border, ), + ), + ], ), - ], - ), - const SizedBox(height: itemPadding), - Row( - children: [ - Icon( - Symbols.steps_rounded, - color: Theme.of(context).colorScheme.secondary, + const SizedBox(height: itemPadding), + Row( + children: [ + Icon( + Symbols.target, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: itemPadding), + Expanded( + child: _isEditing + ? TextField( + controller: _learningObjectiveController, + decoration: InputDecoration( + labelText: l10n.learningObjectiveLabel, + ), + maxLines: null, + ) + : Text( + widget.activity.learningObjective, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], ), - const SizedBox(width: itemPadding), - Expanded( - child: _isEditing - ? TextField( - controller: _instructionsController, - decoration: InputDecoration( - labelText: l10n.instructions, - ), - maxLines: null, - ) - : Text( - widget.activity.instructions, - style: Theme.of(context).textTheme.bodyMedium, + const SizedBox(height: itemPadding), + Row( + children: [ + Icon( + Symbols.steps_rounded, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: itemPadding), + Expanded( + child: _isEditing + ? TextField( + controller: _instructionsController, + decoration: InputDecoration( + labelText: l10n.instructions, + ), + maxLines: null, + ) + : Text( + widget.activity.instructions, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + const SizedBox(height: itemPadding), + if (widget.activity.vocab.isNotEmpty) ...[ + Row( + children: [ + Icon( + Symbols.dictionary, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: itemPadding), + Expanded( + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: List.generate( + _tempActivity.vocab.length, (int index) { + return _isEditing + ? Chip( + label: Text( + _tempActivity.vocab[index].lemma, + ), + onDeleted: () => _removeVocab(index), + backgroundColor: Colors.transparent, + visualDensity: VisualDensity.compact, + shape: const StadiumBorder( + side: BorderSide( + color: Colors.transparent, + ), + ), + ) + : Chip( + label: Text( + _tempActivity.vocab[index].lemma, + ), + backgroundColor: Colors.transparent, + visualDensity: VisualDensity.compact, + shape: const StadiumBorder( + side: BorderSide( + color: Colors.transparent, + ), + ), + ); + }).toList(), ), - ), - ], - ), - const SizedBox(height: itemPadding), - if (widget.activity.vocab.isNotEmpty) ...[ - Row( - children: [ - Icon( - Symbols.dictionary, - color: Theme.of(context).colorScheme.secondary, + ), + ], ), - const SizedBox(width: itemPadding), - Expanded( - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: List.generate( - _tempActivity.vocab.length, (int index) { - return _isEditing - ? Chip( - label: - Text(_tempActivity.vocab[index].lemma), - onDeleted: () => _removeVocab(index), - backgroundColor: Colors.transparent, - visualDensity: VisualDensity.compact, - shape: const StadiumBorder( - side: - BorderSide(color: Colors.transparent), - ), - ) - : Chip( - label: - Text(_tempActivity.vocab[index].lemma), - backgroundColor: Colors.transparent, - visualDensity: VisualDensity.compact, - shape: const StadiumBorder( - side: - BorderSide(color: Colors.transparent), - ), - ); - }).toList(), + ], + if (_isEditing) ...[ + const SizedBox(height: itemPadding), + Padding( + padding: const EdgeInsets.only(top: itemPadding), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _newVocabController, + focusNode: _vocabFocusNode, + decoration: InputDecoration( + labelText: l10n.addVocabulary, + ), + onSubmitted: (value) { + _addVocab(); + }, + ), + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: _addVocab, + ), + ], ), ), ], - ), - ], - if (_isEditing) ...[ - const SizedBox(height: itemPadding), - Padding( - padding: const EdgeInsets.only(top: itemPadding), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _newVocabController, - focusNode: _vocabFocusNode, - decoration: InputDecoration( - labelText: l10n.addVocabulary, - ), - onSubmitted: (value) { - _addVocab(); - }, - ), - ), - IconButton( - icon: const Icon(Icons.add), - onPressed: _addVocab, - ), - ], - ), - ), - ], - const SizedBox(height: itemPadding), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + const SizedBox(height: itemPadding), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Tooltip( - message: !_isEditing ? l10n.edit : l10n.saveChanges, - child: IconButton( - icon: Icon(!_isEditing ? Icons.edit : Icons.save), - onPressed: () => !_isEditing - ? setState(() { - _isEditing = true; - }) - : _saveEdits(), - isSelected: _isEditing, - ), - ), - if (_isEditing) - Tooltip( - message: l10n.cancel, - child: IconButton( - icon: const Icon(Icons.cancel), - onPressed: () { - setState(() { - _isEditing = false; - }); - }, + Row( + children: [ + Tooltip( + message: + !_isEditing ? l10n.edit : l10n.saveChanges, + child: IconButton( + icon: + Icon(!_isEditing ? Icons.edit : Icons.save), + onPressed: () => !_isEditing + ? setState(() { + _isEditing = true; + }) + : _saveEdits(), + isSelected: _isEditing, + ), ), - ), + if (_isEditing) + Tooltip( + message: l10n.cancel, + child: IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + setState(() { + _isEditing = false; + }); + }, + ), + ), + ], + ), + ElevatedButton.icon( + onPressed: !_isEditing ? _onLaunch : null, + icon: const Icon(Icons.send), + label: Text(l10n.launchActivityButton), + ), ], ), - ElevatedButton.icon( - onPressed: !_isEditing ? widget.onLaunch : null, - icon: const Icon(Icons.send), - label: Text(l10n.launchActivityButton), - ), ], ), - ], - ), + ), + ], ), ), ),