From c204f484c9de073c8d90cc8a52e285137cc9a194 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:23:38 -0400 Subject: [PATCH] refactor: make activity card into a dialog when launching / editing, adjust sizing to fit two-per-row on small screens (#2123) --- .../activity_suggestion_card.dart | 113 ++-- .../activity_suggestion_card_content.dart | 172 ------ .../activity_suggestion_dialog.dart | 581 ++++++++++++++++++ .../activity_suggestion_edit_card.dart | 278 --------- .../activity_suggestions_area.dart | 173 +----- .../create_chat_card.dart | 18 +- .../common/widgets/full_width_dialog.dart | 9 +- 7 files changed, 692 insertions(+), 652 deletions(-) delete mode 100644 lib/pangea/activity_suggestions/activity_suggestion_card_content.dart create mode 100644 lib/pangea/activity_suggestions/activity_suggestion_dialog.dart delete mode 100644 lib/pangea/activity_suggestions/activity_suggestion_edit_card.dart diff --git a/lib/pangea/activity_suggestions/activity_suggestion_card.dart b/lib/pangea/activity_suggestions/activity_suggestion_card.dart index 15054811b..5dcaa03ee 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_card.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_card.dart @@ -1,47 +1,39 @@ import 'package:flutter/material.dart'; -import 'package:fluffychat/config/themes.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_content.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_edit_card.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart'; import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; class ActivitySuggestionCard extends StatelessWidget { final ActivityPlanModel activity; - final ActivitySuggestionsAreaState controller; final VoidCallback onPressed; final double width; final double height; + final double padding; const ActivitySuggestionCard({ super.key, required this.activity, - required this.controller, required this.onPressed, required this.width, required this.height, + required this.padding, }); - bool get _isSelected => controller.selectedActivity == activity; - @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( - padding: const EdgeInsets.all(8.0), + padding: EdgeInsets.all(padding), child: PressableButton( onPressed: onPressed, borderRadius: BorderRadius.circular(24.0), color: theme.colorScheme.primary, - child: AnimatedContainer( - duration: FluffyThemes.animationDuration, - height: controller.isEditing && _isSelected - ? 675 - : _isSelected - ? 400 - : height, + child: SizedBox( + height: height, width: width, child: Stack( alignment: Alignment.topCenter, @@ -62,10 +54,7 @@ class ActivitySuggestionCard extends StatelessWidget { decoration: BoxDecoration( image: activity.imageURL != null ? DecorationImage( - image: controller.avatar == null || !_isSelected - ? NetworkImage(activity.imageURL!) - : MemoryImage(controller.avatar!) - as ImageProvider, + image: NetworkImage(activity.imageURL!), ) : null, borderRadius: BorderRadius.circular(24.0), @@ -74,40 +63,74 @@ class ActivitySuggestionCard extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.only( - top: 16.0, + top: 12.0, left: 12.0, right: 12.0, bottom: 12.0, ), - child: controller.isEditing && _isSelected - ? ActivitySuggestionEditCard( - activity: activity, - controller: controller, - ) - : ActivitySuggestionCardContent( - activity: activity, - isSelected: _isSelected, - controller: controller, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ActivitySuggestionCardRow( + icon: Icons.event_note_outlined, + child: Text( + activity.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 54.0), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Align( + alignment: Alignment.topLeft, + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: activity.vocab + .map( + (vocab) => Container( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primary + .withAlpha(50), + borderRadius: + BorderRadius.circular(24.0), + ), + child: Text( + vocab.lemma, + style: theme.textTheme.bodySmall, + ), + ), + ) + .toList(), + ), + ), + ), + ), + ActivitySuggestionCardRow( + icon: Icons.group_outlined, + child: Text( + L10n.of(context).countParticipants( + activity.req.numberOfParticipants, + ), + style: theme.textTheme.bodySmall, + ), + ), + ], + ), ), ), ], ), - if (controller.isEditing && _isSelected) - Positioned( - top: 75.0, - child: InkWell( - borderRadius: BorderRadius.circular(90), - onTap: controller.selectPhoto, - child: const CircleAvatar( - radius: 16.0, - child: Icon( - Icons.add_a_photo_outlined, - size: 16.0, - ), - ), - ), - ), ], ), ), diff --git a/lib/pangea/activity_suggestions/activity_suggestion_card_content.dart b/lib/pangea/activity_suggestions/activity_suggestion_card_content.dart deleted file mode 100644 index 3e7957665..000000000 --- a/lib/pangea/activity_suggestions/activity_suggestion_card_content.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart'; - -class ActivitySuggestionCardContent extends StatelessWidget { - final ActivityPlanModel activity; - final ActivitySuggestionsAreaState controller; - final bool isSelected; - - const ActivitySuggestionCardContent({ - super.key, - required this.activity, - required this.controller, - required this.isSelected, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ActivitySuggestionCardRow( - icon: Icons.event_note_outlined, - child: Text( - activity.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - if (isSelected) - ActivitySuggestionCardRow( - icon: Symbols.target, - child: Text( - activity.learningObjective, - style: theme.textTheme.bodySmall, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - if (isSelected) - ActivitySuggestionCardRow( - icon: Symbols.target, - child: Text( - activity.instructions, - style: theme.textTheme.bodySmall, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - if (isSelected) - ActivitySuggestionCardRow( - icon: Symbols.dictionary, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 54.0), - child: SingleChildScrollView( - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: activity.vocab - .map( - (vocab) => Container( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - decoration: BoxDecoration( - color: theme.colorScheme.primary.withAlpha(50), - borderRadius: BorderRadius.circular(24.0), - ), - child: Text( - vocab.lemma, - style: theme.textTheme.bodySmall, - ), - ), - ) - .toList(), - ), - ), - ), - ), - if (!isSelected) - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 54.0), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Align( - alignment: Alignment.topLeft, - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: activity.vocab - .map( - (vocab) => Container( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - decoration: BoxDecoration( - color: theme.colorScheme.primary.withAlpha(50), - borderRadius: BorderRadius.circular(24.0), - ), - child: Text( - vocab.lemma, - style: theme.textTheme.bodySmall, - ), - ), - ) - .toList(), - ), - ), - ), - ), - ActivitySuggestionCardRow( - icon: Icons.group_outlined, - child: Text( - L10n.of(context).countParticipants( - activity.req.numberOfParticipants, - ), - style: theme.textTheme.bodySmall, - ), - ), - if (isSelected) - Row( - spacing: 6.0, - children: [ - Expanded( - child: ElevatedButton( - onPressed: () => controller.onLaunch(activity), - style: ElevatedButton.styleFrom( - minimumSize: Size.zero, - padding: const EdgeInsets.all(4.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - ), - child: Text( - L10n.of(context).inviteAndLaunch, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onPrimary, - ), - ), - ), - ), - IconButton.filled( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - ), - padding: const EdgeInsets.all(6.0), - constraints: - const BoxConstraints(), // override default min size of 48px - iconSize: 16.0, - icon: const Icon(Icons.edit_outlined), - onPressed: () => controller.setEditting(true), - ), - ], - ), - ], - ); - } -} diff --git a/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart b/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart new file mode 100644 index 000000000..f3edd2048 --- /dev/null +++ b/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart @@ -0,0 +1,581 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:http/http.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix.dart'; +import 'package:matrix/matrix_api_lite/generated/model.dart' as sdk; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart'; +import 'package:fluffychat/pangea/chat/constants/default_power_level.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 'package:fluffychat/widgets/matrix.dart'; + +class ActivitySuggestionDialog extends StatefulWidget { + final ActivityPlanModel activity; + const ActivitySuggestionDialog({ + required this.activity, + super.key, + }); + + @override + ActivitySuggestionDialogState createState() => + ActivitySuggestionDialogState(); +} + +class ActivitySuggestionDialogState extends State { + bool _isEditing = false; + + Uint8List? _avatar; + String? _avatarURL; + + final TextEditingController _titleController = TextEditingController(); + final TextEditingController _instructionsController = TextEditingController(); + final TextEditingController _vocabController = TextEditingController(); + final TextEditingController _participantsController = TextEditingController(); + final TextEditingController _learningObjectivesController = + TextEditingController(); + + // storing this separately so that we can dismiss edits, + // rather than directly modifying the activity with each change + final List _vocab = []; + + final GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _titleController.text = widget.activity.title; + _learningObjectivesController.text = widget.activity.learningObjective; + _instructionsController.text = widget.activity.instructions; + _participantsController.text = + widget.activity.req.numberOfParticipants.toString(); + _vocab.addAll(widget.activity.vocab); + } + + @override + void dispose() { + _titleController.dispose(); + _learningObjectivesController.dispose(); + _instructionsController.dispose(); + _vocabController.dispose(); + _participantsController.dispose(); + super.dispose(); + } + + void _setEditing(bool editting) { + _isEditing = editting; + if (mounted) setState(() {}); + } + + void _setAvatar() async { + final photo = await selectFiles( + context, + type: FileSelectorType.images, + allowMultiple: false, + ); + final bytes = await photo.singleOrNull?.readAsBytes(); + if (mounted) setState(() => _avatar = bytes); + } + + Future _setAvatarURL() async { + if (widget.activity.imageURL == null && _avatar == null) return; + try { + if (_avatar == null) { + final Response response = + await http.get(Uri.parse(widget.activity.imageURL!)); + _avatar = response.bodyBytes; + } + final resp = await Matrix.of(context).client.uploadContent(_avatar!); + if (mounted) setState(() => _avatarURL = resp.toString()); + widget.activity.imageURL = _avatarURL; + } catch (err, s) { + ErrorHandler.logError( + e: err, + s: s, + data: { + "imageURL": widget.activity.imageURL, + }, + ); + } + } + + void _clearEdits() { + _avatar = null; + _avatarURL = null; + _vocab.clear(); + _vocab.addAll(widget.activity.vocab); + if (mounted) setState(() {}); + } + + Future _updateTextFields() async { + widget.activity.title = _titleController.text; + widget.activity.learningObjective = _learningObjectivesController.text; + widget.activity.instructions = _instructionsController.text; + widget.activity.req.numberOfParticipants = + int.tryParse(_participantsController.text) ?? 3; + widget.activity.vocab = _vocab; + } + + void _addVocab() { + _vocab.insert( + 0, + Vocab( + lemma: _vocabController.text.trim(), + pos: "", + ), + ); + _vocabController.clear(); + if (mounted) setState(() {}); + } + + void _removeVocab(int index) { + _vocab.removeAt(index); + if (mounted) setState(() {}); + } + + Future _launch() async { + final client = Matrix.of(context).client; + + final resp = await showFutureLoadingDialog( + context: context, + future: () async { + await _setAvatarURL(); + final roomId = await client.createGroupChat( + preset: CreateRoomPreset.publicChat, + visibility: sdk.Visibility.private, + groupName: widget.activity.title, + initialState: [ + if (_avatarURL != null) + StateEvent( + type: EventTypes.RoomAvatar, + content: {'url': _avatarURL.toString()}, + ), + StateEvent( + type: EventTypes.RoomPowerLevels, + stateKey: '', + content: defaultPowerLevels(client.userID!), + ), + ], + enableEncryption: false, + ); + + Room? room = Matrix.of(context).client.getRoomById(roomId); + if (room == null) { + await client.waitForRoomInSync(roomId); + room = Matrix.of(context).client.getRoomById(roomId); + if (room == null) return; + } + + final eventId = await room.pangeaSendTextEvent( + widget.activity.markdown, + messageTag: ModelKey.messageTagActivityPlan, + ); + + if (eventId == null) { + debugger(when: kDebugMode); + return; + } + + await room.setPinnedEvents([eventId]); + context.go("/rooms/$roomId/invite"); + }, + ); + + if (!resp.isError) { + Navigator.of(context).pop(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final body = Stack( + alignment: Alignment.topCenter, + children: [ + Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 200, + decoration: BoxDecoration( + image: widget.activity.imageURL != null || _avatar != null + ? DecorationImage( + image: _avatar != null + ? MemoryImage(_avatar!) + : NetworkImage(widget.activity.imageURL!) + as ImageProvider, + ) + : null, + borderRadius: BorderRadius.circular(24.0), + ), + ), + Flexible( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + if (_isEditing) + ActivitySuggestionCardRow( + icon: Icons.event_note_outlined, + child: TextFormField( + controller: _titleController, + decoration: InputDecoration( + labelText: L10n.of(context).activityTitle, + ), + style: theme.textTheme.bodySmall, + maxLines: 2, + minLines: 1, + ), + ) + else + ActivitySuggestionCardRow( + icon: Icons.event_note_outlined, + child: Text( + widget.activity.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (_isEditing) + ActivitySuggestionCardRow( + icon: Symbols.target, + child: TextFormField( + style: theme.textTheme.bodySmall, + controller: _learningObjectivesController, + decoration: InputDecoration( + labelText: + L10n.of(context).learningObjectiveLabel, + ), + maxLines: 4, + minLines: 1, + ), + ) + else + ActivitySuggestionCardRow( + icon: Symbols.target, + child: Text( + widget.activity.learningObjective, + style: theme.textTheme.bodySmall, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + if (_isEditing) + ActivitySuggestionCardRow( + icon: Symbols.target, + child: TextFormField( + style: theme.textTheme.bodySmall, + controller: _instructionsController, + decoration: InputDecoration( + labelText: L10n.of(context).instructions, + ), + maxLines: 8, + minLines: 1, + ), + ) + else + ActivitySuggestionCardRow( + icon: Symbols.target, + child: Text( + widget.activity.instructions, + style: theme.textTheme.bodySmall, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + if (_isEditing) + ActivitySuggestionCardRow( + icon: Icons.group_outlined, + child: TextFormField( + controller: _participantsController, + style: theme.textTheme.bodySmall, + decoration: InputDecoration( + labelText: L10n.of(context).classRoster, + ), + maxLines: 1, + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return null; + } + + try { + final val = int.parse(value); + if (val <= 0) { + return L10n.of(context).pleaseEnterInt; + } + } catch (e) { + return L10n.of(context).pleaseEnterANumber; + } + return null; + }, + ), + ) + else + ActivitySuggestionCardRow( + icon: Icons.group_outlined, + child: Text( + L10n.of(context).countParticipants( + widget.activity.req.numberOfParticipants, + ), + style: theme.textTheme.bodySmall, + ), + ), + if (_isEditing) + ActivitySuggestionCardRow( + icon: Symbols.dictionary, + child: ConstrainedBox( + constraints: + const BoxConstraints(maxHeight: 54.0), + child: SingleChildScrollView( + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: _vocab + .mapIndexed( + (i, vocab) => Container( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primary + .withAlpha(50), + borderRadius: + BorderRadius.circular(24.0), + ), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => _removeVocab(i), + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + vocab.lemma, + style: theme + .textTheme.bodySmall, + ), + const Icon( + Icons.close, + size: 12.0, + ), + ], + ), + ), + ), + ), + ) + .toList(), + ), + ), + ), + ) + else + ActivitySuggestionCardRow( + icon: Symbols.dictionary, + child: ConstrainedBox( + constraints: + const BoxConstraints(maxHeight: 54.0), + child: SingleChildScrollView( + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: _vocab + .map( + (vocab) => Container( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primary + .withAlpha(50), + borderRadius: + BorderRadius.circular(24.0), + ), + child: Text( + vocab.lemma, + style: theme.textTheme.bodySmall, + ), + ), + ) + .toList(), + ), + ), + ), + ), + if (_isEditing) + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + spacing: 4.0, + children: [ + Expanded( + child: TextFormField( + controller: _vocabController, + style: theme.textTheme.bodySmall, + decoration: InputDecoration( + hintText: L10n.of(context).addVocabulary, + ), + maxLines: 1, + onFieldSubmitted: (_) => _addVocab(), + ), + ), + IconButton( + padding: const EdgeInsets.all(0.0), + constraints: + const BoxConstraints(), // override default min size of 48px + iconSize: 16.0, + icon: const Icon(Icons.add_outlined), + onPressed: _addVocab, + ), + ], + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + spacing: 6.0, + children: [ + if (_isEditing) + GestureDetector( + child: const Icon(Icons.save_outlined, size: 16.0), + onTap: () { + if (!_formKey.currentState!.validate()) { + return; + } + _updateTextFields(); + _setEditing(false); + }, + ), + if (_isEditing) + GestureDetector( + child: const Icon(Icons.close_outlined, size: 16.0), + onTap: () { + _clearEdits(); + _setEditing(false); + }, + ), + Expanded( + child: ElevatedButton( + onPressed: () { + if (!_formKey.currentState!.validate()) { + return; + } + _updateTextFields(); + _launch(); + }, + style: ElevatedButton.styleFrom( + minimumSize: Size.zero, + padding: const EdgeInsets.all(4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), + child: Text( + L10n.of(context).inviteAndLaunch, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), + ), + ), + ), + if (!_isEditing) + IconButton.filled( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + ), + padding: const EdgeInsets.all(6.0), + constraints: + const BoxConstraints(), // override default min size of 48px + iconSize: 16.0, + icon: const Icon(Icons.edit_outlined), + onPressed: () => _setEditing(true), + ), + ], + ), + ), + ], + ), + ), + Positioned( + top: 4.0, + left: 4.0, + child: IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: Navigator.of(context).pop, + tooltip: L10n.of(context).close, + ), + ), + if (_isEditing) + Positioned( + top: 160.0, + child: InkWell( + borderRadius: BorderRadius.circular(90), + onTap: _setAvatar, + child: const CircleAvatar( + radius: 24.0, + child: Icon( + Icons.add_a_photo_outlined, + size: 24.0, + ), + ), + ), + ), + ], + ); + + final content = AnimatedSize( + duration: FluffyThemes.animationDuration, + child: ConstrainedBox( + constraints: FluffyThemes.isColumnMode(context) + ? const BoxConstraints( + maxWidth: 400.0, + ) + : BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, + maxHeight: MediaQuery.of(context).size.height, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: body, + ), + ), + ); + + return FluffyThemes.isColumnMode(context) + ? Dialog(child: content) + : Dialog.fullscreen(child: content); + } +} diff --git a/lib/pangea/activity_suggestions/activity_suggestion_edit_card.dart b/lib/pangea/activity_suggestions/activity_suggestion_edit_card.dart deleted file mode 100644 index a4e578c0c..000000000 --- a/lib/pangea/activity_suggestions/activity_suggestion_edit_card.dart +++ /dev/null @@ -1,278 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:collection/collection.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart'; - -class ActivitySuggestionEditCard extends StatefulWidget { - final ActivityPlanModel activity; - final ActivitySuggestionsAreaState controller; - - const ActivitySuggestionEditCard({ - super.key, - required this.activity, - required this.controller, - }); - - @override - ActivitySuggestionEditCardState createState() => - ActivitySuggestionEditCardState(); -} - -class ActivitySuggestionEditCardState - extends State { - final TextEditingController _titleController = TextEditingController(); - final TextEditingController _instructionsController = TextEditingController(); - final TextEditingController _vocabController = TextEditingController(); - final TextEditingController _participantsController = TextEditingController(); - final TextEditingController _learningObjectivesController = - TextEditingController(); - - final GlobalKey _formKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _titleController.text = widget.activity.title; - _learningObjectivesController.text = widget.activity.learningObjective; - _instructionsController.text = widget.activity.instructions; - _participantsController.text = - widget.activity.req.numberOfParticipants.toString(); - } - - @override - void dispose() { - _titleController.dispose(); - _learningObjectivesController.dispose(); - _instructionsController.dispose(); - _vocabController.dispose(); - _participantsController.dispose(); - super.dispose(); - } - - void _updateActivity() { - widget.controller.updateActivity((activity) { - activity.title = _titleController.text; - activity.learningObjective = _learningObjectivesController.text; - activity.instructions = _instructionsController.text; - activity.req.numberOfParticipants = - int.tryParse(_participantsController.text) ?? 3; - return activity; - }); - } - - void _addVocab() { - widget.controller.updateActivity((activity) { - activity.vocab.insert( - 0, - Vocab( - lemma: _vocabController.text.trim(), - pos: "", - ), - ); - return activity; - }); - _vocabController.clear(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ActivitySuggestionCardRow( - icon: Icons.event_note_outlined, - child: TextFormField( - controller: _titleController, - decoration: InputDecoration( - labelText: L10n.of(context).activityTitle, - ), - style: theme.textTheme.bodySmall, - maxLines: 2, - minLines: 1, - ), - ), - ActivitySuggestionCardRow( - icon: Symbols.target, - child: TextFormField( - style: theme.textTheme.bodySmall, - controller: _learningObjectivesController, - decoration: InputDecoration( - labelText: L10n.of(context).learningObjectiveLabel, - ), - maxLines: 4, - minLines: 1, - ), - ), - ActivitySuggestionCardRow( - icon: Symbols.target, - child: TextFormField( - style: theme.textTheme.bodySmall, - controller: _instructionsController, - decoration: InputDecoration( - labelText: L10n.of(context).instructions, - ), - maxLines: 8, - minLines: 1, - ), - ), - ActivitySuggestionCardRow( - icon: Icons.group_outlined, - child: TextFormField( - controller: _participantsController, - style: theme.textTheme.bodySmall, - decoration: InputDecoration( - labelText: L10n.of(context).classRoster, - ), - maxLines: 1, - keyboardType: TextInputType.number, - validator: (value) { - if (value == null || value.isEmpty) { - return null; - } - - try { - final val = int.parse(value); - if (val <= 0) { - return L10n.of(context).pleaseEnterInt; - } - } catch (e) { - return L10n.of(context).pleaseEnterANumber; - } - return null; - }, - ), - ), - ActivitySuggestionCardRow( - icon: Symbols.dictionary, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 54.0), - child: SingleChildScrollView( - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: widget.activity.vocab - .mapIndexed( - (i, vocab) => Container( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - decoration: BoxDecoration( - color: theme.colorScheme.primary.withAlpha(50), - borderRadius: BorderRadius.circular(24.0), - ), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - widget.controller.updateActivity((activity) { - activity.vocab.removeAt(i); - return activity; - }); - }, - child: Row( - spacing: 4.0, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - vocab.lemma, - style: theme.textTheme.bodySmall, - ), - const Icon(Icons.close, size: 12.0), - ], - ), - ), - ), - ), - ) - .toList(), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - spacing: 4.0, - children: [ - Expanded( - child: TextFormField( - controller: _vocabController, - style: theme.textTheme.bodySmall, - decoration: InputDecoration( - hintText: L10n.of(context).addVocabulary, - ), - maxLines: 1, - onFieldSubmitted: (_) => _addVocab(), - ), - ), - IconButton( - padding: const EdgeInsets.all(0.0), - constraints: - const BoxConstraints(), // override default min size of 48px - iconSize: 16.0, - icon: const Icon(Icons.add_outlined), - onPressed: _addVocab, - ), - ], - ), - ), - Row( - spacing: 6.0, - children: [ - GestureDetector( - child: const Icon(Icons.save_outlined, size: 16.0), - onTap: () { - if (!_formKey.currentState!.validate()) { - return; - } - _updateActivity(); - widget.controller.setEditting(false); - }, - ), - GestureDetector( - child: const Icon(Icons.close_outlined, size: 16.0), - onTap: () => widget.controller.setEditting(false), - ), - Expanded( - child: ElevatedButton( - onPressed: () { - if (!_formKey.currentState!.validate()) { - return; - } - _updateActivity(); - widget.controller.onLaunch(widget.activity); - }, - style: ElevatedButton.styleFrom( - minimumSize: Size.zero, - padding: const EdgeInsets.all(4.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - ), - child: Text( - L10n.of(context).inviteAndLaunch, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onPrimary, - ), - ), - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/pangea/activity_suggestions/activity_suggestions_area.dart b/lib/pangea/activity_suggestions/activity_suggestions_area.dart index 0b0c72917..9cefb2d02 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_area.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_area.dart @@ -1,17 +1,9 @@ // 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:developer'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:go_router/go_router.dart'; -import 'package:http/http.dart' as http; -import 'package:http/http.dart'; -import 'package:matrix/matrix.dart' as sdk; -import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; @@ -19,15 +11,10 @@ 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/activity_suggestions/create_chat_card.dart'; -import 'package:fluffychat/pangea/chat/constants/default_power_level.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/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; -import 'package:fluffychat/utils/file_selector.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class ActivitySuggestionsArea extends StatefulWidget { @@ -50,14 +37,12 @@ class ActivitySuggestionsAreaState extends State { super.dispose(); } - ActivityPlanModel? selectedActivity; - bool isEditing = false; - Uint8List? avatar; final List _activityItems = []; final ScrollController _scrollController = ScrollController(); - final double cardHeight = 275.0; - final double cardWidth = 250.0; + final double cardHeight = 235.0; + final double cardPadding = 8.0; + double get cardWidth => FluffyThemes.isColumnMode(context) ? 225.0 : 160.0; void _scrollToItem(int index) { final viewportDimension = _scrollController.position.viewportDimension; @@ -98,131 +83,26 @@ class ActivitySuggestionsAreaState extends State { setState(() {}); } - void setSelectedActivity(ActivityPlanModel? activity) { - selectedActivity = activity; - isEditing = false; - if (mounted) setState(() {}); - } - - void setEditting(bool editting) { - if (selectedActivity == null) return; - isEditing = editting; - if (mounted) setState(() {}); - } - - void selectPhoto() async { - final photo = await selectFiles( - context, - type: FileSelectorType.images, - allowMultiple: false, - ); - final bytes = await photo.singleOrNull?.readAsBytes(); - - setState(() { - avatar = bytes; - }); - } - - void updateActivity( - ActivityPlanModel Function(ActivityPlanModel) update, - ) { - if (selectedActivity == null) return; - update(selectedActivity!); - if (mounted) setState(() {}); - } - - Future _getAvatarURL(ActivityPlanModel activity) async { - if (activity.imageURL == null && avatar == null) return null; - try { - if (avatar == null) { - final Response response = await http.get(Uri.parse(activity.imageURL!)); - avatar = response.bodyBytes; - } - return (await Matrix.of(context).client.uploadContent(avatar!)) - .toString(); - } catch (err, s) { - ErrorHandler.logError( - e: err, - s: s, - data: { - "imageURL": activity.imageURL, - }, - ); - } - return null; - } - - Future onLaunch(ActivityPlanModel activity) async { - final client = Matrix.of(context).client; - - await showFutureLoadingDialog( - context: context, - future: () async { - final avatarURL = await _getAvatarURL(activity); - final roomId = await client.createGroupChat( - preset: CreateRoomPreset.publicChat, - visibility: sdk.Visibility.private, - groupName: activity.title, - initialState: [ - if (avatarURL != null) - StateEvent( - type: EventTypes.RoomAvatar, - content: {'url': avatarURL.toString()}, - ), - StateEvent( - type: EventTypes.RoomPowerLevels, - stateKey: '', - content: defaultPowerLevels(client.userID!), - ), - ], - enableEncryption: false, - ); - - Room? room = Matrix.of(context).client.getRoomById(roomId); - if (room == null) { - await client.waitForRoomInSync(roomId); - room = Matrix.of(context).client.getRoomById(roomId); - if (room == null) return; - } - - final eventId = await room.pangeaSendTextEvent( - activity.markdown, - messageTag: ModelKey.messageTagActivityPlan, - ); - - if (eventId == null) { - debugger(when: kDebugMode); - return; - } - - await room.setPinnedEvents([eventId]); - context.go("/rooms/$roomId/invite"); - }, - ); - } - @override Widget build(BuildContext context) { final List cards = _activityItems .mapIndexed((i, activity) { return ActivitySuggestionCard( activity: activity, - controller: this, onPressed: () { - if (isEditing && selectedActivity == activity) { - setEditting(false); - } else if (selectedActivity == activity) { - setSelectedActivity(null); - } else { - setSelectedActivity(activity); - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToItem(i); - }); + _scrollToItem(i); + showDialog( + context: context, + builder: (context) { + return ActivitySuggestionDialog( + activity: activity, + ); + }, + ); }, width: cardWidth, height: cardHeight, + padding: cardPadding, ); }) .cast() @@ -233,25 +113,22 @@ class ActivitySuggestionsAreaState extends State { CreateChatCard( width: cardWidth, height: cardHeight, + padding: cardPadding, ), ); return Container( alignment: Alignment.topCenter, - padding: const EdgeInsets.all(16.0), - child: FluffyThemes.isColumnMode(context) - ? ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: cards.length, - itemBuilder: (context, index) => cards[index], - controller: _scrollController, - ) - : SingleChildScrollView( - controller: _scrollController, - child: Wrap( - children: cards, - ), - ), + padding: EdgeInsets.symmetric( + vertical: 16.0, + horizontal: FluffyThemes.isColumnMode(context) ? 16.0 : 0.0, + ), + child: SingleChildScrollView( + controller: _scrollController, + child: Wrap( + children: cards, + ), + ), ); } } diff --git a/lib/pangea/activity_suggestions/create_chat_card.dart b/lib/pangea/activity_suggestions/create_chat_card.dart index d19580bd5..3e304d069 100644 --- a/lib/pangea/activity_suggestions/create_chat_card.dart +++ b/lib/pangea/activity_suggestions/create_chat_card.dart @@ -11,10 +11,12 @@ import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; class CreateChatCard extends StatelessWidget { final double width; final double height; + final double padding; const CreateChatCard({ required this.width, required this.height, + required this.padding, super.key, }); @@ -22,7 +24,7 @@ class CreateChatCard extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( - padding: const EdgeInsets.all(8.0), + padding: EdgeInsets.all(padding), child: PressableButton( onPressed: () => context.go('/rooms/newgroup'), borderRadius: BorderRadius.circular(24.0), @@ -46,11 +48,15 @@ class CreateChatCard extends StatelessWidget { width: 80, ), ), - const SizedBox(height: 24.0), - Text( - L10n.of(context).createOwnChat, - style: theme.textTheme.bodyLarge - ?.copyWith(color: theme.colorScheme.secondary), + const SizedBox(height: 16.0), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + L10n.of(context).createOwnChat, + style: theme.textTheme.bodyLarge + ?.copyWith(color: theme.colorScheme.secondary), + textAlign: TextAlign.center, + ), ), ], ), diff --git a/lib/pangea/common/widgets/full_width_dialog.dart b/lib/pangea/common/widgets/full_width_dialog.dart index a13a5b752..e3f93ffc6 100644 --- a/lib/pangea/common/widgets/full_width_dialog.dart +++ b/lib/pangea/common/widgets/full_width_dialog.dart @@ -1,6 +1,7 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:fluffychat/config/themes.dart'; + class FullWidthDialog extends StatelessWidget { final Widget dialogContent; final double maxWidth; @@ -16,7 +17,7 @@ class FullWidthDialog extends StatelessWidget { @override Widget build(BuildContext context) { final content = ConstrainedBox( - constraints: kIsWeb + constraints: FluffyThemes.isColumnMode(context) ? BoxConstraints( maxWidth: maxWidth, maxHeight: maxHeight, @@ -31,6 +32,8 @@ class FullWidthDialog extends StatelessWidget { ), ); - return kIsWeb ? Dialog(child: content) : Dialog.fullscreen(child: content); + return FluffyThemes.isColumnMode(context) + ? Dialog(child: content) + : Dialog.fullscreen(child: content); } }