diff --git a/lib/pangea/activity_planner/activity_plan_card.dart b/lib/pangea/activity_planner/activity_plan_card.dart index 95af173e9..557378476 100644 --- a/lib/pangea/activity_planner/activity_plan_card.dart +++ b/lib/pangea/activity_planner/activity_plan_card.dart @@ -98,29 +98,37 @@ class ActivityPlanCardState extends State { }); } - Future _addBookmark(ActivityPlanModel activity) => - BookmarkedActivitiesRepo.save(activity).catchError((e, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: e, s: stack, data: activity.toJson()); - return activity; // Return the original activity in case of error - }).whenComplete(() { - if (mounted) { - setState(() {}); - widget.onChange(); - } - }); + Future _addBookmark(ActivityPlanModel activity) async { + try { + final uniqueID = + "${activity.title.replaceAll(RegExp(r'\s+'), '-')}-${DateTime.now().millisecondsSinceEpoch}"; + return BookmarkedActivitiesRepo.save(activity, uniqueID); + } catch (e, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: stack, data: activity.toJson()); + return activity; // Return the original activity in case of error + } finally { + if (mounted) { + setState(() {}); + widget.onChange(); + } + } + } - Future _removeBookmark() => - BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId) - .catchError((e, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: e, s: stack, data: widget.activity.toJson()); - }).whenComplete(() { - if (mounted) { - setState(() {}); - widget.onChange(); - } - }); + Future _removeBookmark() async { + if (widget.activity.bookmarkId == null) return; + try { + BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId!); + } catch (e, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: stack, data: widget.activity.toJson()); + } finally { + if (mounted) { + setState(() {}); + widget.onChange(); + } + } + } void _addVocab() { setState(() { diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart index 762683d32..6a9be7755 100644 --- a/lib/pangea/activity_planner/activity_plan_model.dart +++ b/lib/pangea/activity_planner/activity_plan_model.dart @@ -10,6 +10,7 @@ class ActivityPlanModel { String instructions; List vocab; String? imageURL; + String? bookmarkId; ActivityPlanModel({ required this.req, @@ -17,6 +18,7 @@ class ActivityPlanModel { required this.learningObjective, required this.instructions, required this.vocab, + this.bookmarkId, this.imageURL, }); @@ -30,6 +32,7 @@ class ActivityPlanModel { json[ModelKey.activityPlanVocab].map((vocab) => Vocab.fromJson(vocab)), ), imageURL: json[ModelKey.activityPlanImageURL], + bookmarkId: json[ModelKey.activityPlanBookmarkId], ); } @@ -40,8 +43,8 @@ class ActivityPlanModel { ModelKey.activityPlanLearningObjective: learningObjective, ModelKey.activityPlanInstructions: instructions, ModelKey.activityPlanVocab: vocab.map((vocab) => vocab.toJson()).toList(), - ModelKey.activityPlanBookmarkId: bookmarkId, ModelKey.activityPlanImageURL: imageURL, + ModelKey.activityPlanBookmarkId: bookmarkId, }; } @@ -74,6 +77,7 @@ class ActivityPlanModel { other.learningObjective == learningObjective && other.instructions == instructions && listEquals(other.vocab, vocab) && + other.imageURL == imageURL && other.bookmarkId == bookmarkId; } @@ -83,15 +87,9 @@ class ActivityPlanModel { title.hashCode ^ learningObjective.hashCode ^ instructions.hashCode ^ - Object.hashAll(vocab); - - String get bookmarkId { - return (title.hashCode ^ - learningObjective.hashCode ^ - instructions.hashCode ^ - Object.hashAll(vocab)) - .toString(); - } + Object.hashAll(vocab) ^ + imageURL.hashCode ^ + bookmarkId.hashCode; } class Vocab { diff --git a/lib/pangea/activity_planner/bookmarked_activities_repo.dart b/lib/pangea/activity_planner/bookmarked_activities_repo.dart index cd127bab4..33df6fd13 100644 --- a/lib/pangea/activity_planner/bookmarked_activities_repo.dart +++ b/lib/pangea/activity_planner/bookmarked_activities_repo.dart @@ -9,9 +9,13 @@ class BookmarkedActivitiesRepo { /// save an activity to the list of bookmarked activities /// returns the activity with a bookmarkId - static Future save(ActivityPlanModel activity) async { + static Future save( + ActivityPlanModel activity, + String bookmarkID, + ) async { + activity.bookmarkId = bookmarkID; await _bookStorage.write( - activity.bookmarkId, + bookmarkID, activity.toJson(), ); @@ -23,7 +27,8 @@ class BookmarkedActivitiesRepo { _bookStorage.remove(bookmarkId); static bool isBookmarked(ActivityPlanModel activity) { - return _bookStorage.read(activity.bookmarkId) != null; + return activity.bookmarkId != null && + _bookStorage.read(activity.bookmarkId!) != null; } static List get() { diff --git a/lib/pangea/activity_planner/bookmarked_activity_list.dart b/lib/pangea/activity_planner/bookmarked_activity_list.dart index fee714005..318388006 100644 --- a/lib/pangea/activity_planner/bookmarked_activity_list.dart +++ b/lib/pangea/activity_planner/bookmarked_activity_list.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -9,6 +11,7 @@ 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_suggestions/activity_suggestion_card.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class BookmarkedActivitiesList extends StatefulWidget { final Room? room; @@ -36,6 +39,29 @@ class BookmarkedActivitiesListState extends State { double get cardPadding => _isColumnMode ? 8.0 : 0.0; double get cardWidth => _isColumnMode ? 225.0 : 150.0; + Future _onEdit( + ActivityPlanModel activity, + Uint8List? avatar, + String? filename, + ) async { + if (avatar != null) { + final url = await Matrix.of(context).client.uploadContent( + avatar, + filename: filename, + ); + activity.imageURL = url.toString(); + } + + final uniqueID = + "${activity.title.replaceAll(RegExp(r'\s+'), '-')}-${DateTime.now().millisecondsSinceEpoch}"; + + if (activity.bookmarkId != null) { + await BookmarkedActivitiesRepo.remove(activity.bookmarkId!); + } + await BookmarkedActivitiesRepo.save(activity, uniqueID); + if (mounted) setState(() {}); + } + @override Widget build(BuildContext context) { final l10n = L10n.of(context); @@ -70,6 +96,7 @@ class BookmarkedActivitiesListState extends State { activity: activity, buttonText: L10n.of(context).inviteAndLaunch, room: widget.room, + onEdit: _onEdit, ); }, ); diff --git a/lib/pangea/activity_suggestions/activity_suggestion_card.dart b/lib/pangea/activity_suggestions/activity_suggestion_card.dart index 8136997e7..af385a813 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_card.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_card.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart'; import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; class ActivitySuggestionCard extends StatelessWidget { final ActivityPlanModel activity; @@ -81,16 +82,25 @@ class ActivitySuggestionCard extends StatelessWidget { child: image != null ? Image.memory(image!) : activity.imageURL != null - ? CachedNetworkImage( - imageUrl: activity.imageURL!, - placeholder: (context, url) => const Center( - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => Icon( - Icons.error, - color: theme.colorScheme.error, - ), - ) + ? activity.imageURL!.startsWith("mxc") + ? MxcImage( + uri: Uri.parse(activity.imageURL!), + width: width, + height: 100, + cacheKey: activity.bookmarkId, + ) + : CachedNetworkImage( + imageUrl: activity.imageURL!, + placeholder: (context, url) => + const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => + Icon( + Icons.error, + color: theme.colorScheme.error, + ), + ) : null, ), ), @@ -173,11 +183,19 @@ class ActivitySuggestionCard extends StatelessWidget { isBookmarked ? Icons.bookmark : Icons.bookmark_border, ), onPressed: onPressed != null - ? () => isBookmarked - ? BookmarkedActivitiesRepo.remove(activity.bookmarkId) - .then((_) => onChange()) - : BookmarkedActivitiesRepo.save(activity) - .then((_) => onChange()) + ? () async { + final uniqueID = + "${activity.title.replaceAll(RegExp(r'\s+'), '-')}-${DateTime.now().millisecondsSinceEpoch}"; + await (isBookmarked + ? BookmarkedActivitiesRepo.remove( + activity.bookmarkId!, + ) + : BookmarkedActivitiesRepo.save( + activity, + uniqueID, + )); + onChange(); + } : null, iconSize: 24.0, ), diff --git a/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart b/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart index 7568d8f37..180642938 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart @@ -140,7 +140,7 @@ class ActivitySuggestionCarouselState return ActivitySuggestionDialog( activity: _currentActivity!, buttonText: L10n.of(context).selectActivity, - launch: widget.onActivitySelected, + onLaunch: widget.onActivitySelected, ); }, ); diff --git a/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart b/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart index eea250fa1..bcc31ce5c 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart @@ -3,6 +3,7 @@ import 'dart:ui'; 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:go_router/go_router.dart'; @@ -21,6 +22,7 @@ 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'; +import 'package:fluffychat/widgets/mxc_image.dart'; class ActivitySuggestionDialog extends StatefulWidget { final ActivityPlanModel activity; @@ -29,14 +31,21 @@ class ActivitySuggestionDialog extends StatefulWidget { final Function( ActivityPlanModel, - Uint8List? avatar, - String? filename, - )? launch; + Uint8List?, + String?, + )? onLaunch; + + final Future Function( + ActivityPlanModel, + Uint8List?, + String?, + )? onEdit; const ActivitySuggestionDialog({ required this.activity, required this.buttonText, - this.launch, + this.onLaunch, + this.onEdit, this.room, super.key, }); @@ -204,6 +213,26 @@ class ActivitySuggestionDialogState extends State { context.go("/rooms/$roomId/invite"); } + Future _saveEdits() async { + if (!_formKey.currentState!.validate()) return; + await _updateTextFields(); + _setEditing(false); + if (widget.onEdit != null) { + await widget.onEdit!( + widget.activity, + _avatar, + _filename, + ); + } + } + + double get width { + if (FluffyThemes.isColumnMode(context)) { + return 400.0; + } + return MediaQuery.of(context).size.width; + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -219,16 +248,32 @@ class ActivitySuggestionDialogState extends State { 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), ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: _avatar != null + ? Image.memory(_avatar!) + : widget.activity.imageURL != null + ? widget.activity.imageURL!.startsWith("mxc") + ? MxcImage( + uri: Uri.parse(widget.activity.imageURL!), + width: width, + height: 200, + cacheKey: widget.activity.bookmarkId, + ) + : CachedNetworkImage( + imageUrl: widget.activity.imageURL!, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => Icon( + Icons.error, + color: theme.colorScheme.error, + ), + ) + : null, + ), ), Flexible( child: SingleChildScrollView( @@ -469,16 +514,10 @@ class ActivitySuggestionDialogState extends State { _setEditing(false); }, ), - if (_isEditing) + if (_isEditing && widget.onEdit != null) Expanded( child: ElevatedButton( - onPressed: () async { - if (!_formKey.currentState!.validate()) { - return; - } - await _updateTextFields(); - _setEditing(false); - }, + onPressed: _saveEdits, style: ElevatedButton.styleFrom( minimumSize: Size.zero, padding: const EdgeInsets.all(6.0), @@ -494,8 +533,8 @@ class ActivitySuggestionDialogState extends State { ?.copyWith(color: theme.colorScheme.onPrimary), ), ), - ), - if (!_isEditing) + ) + else Expanded( child: ElevatedButton( onPressed: () async { @@ -505,8 +544,8 @@ class ActivitySuggestionDialogState extends State { final resp = await showFutureLoadingDialog( context: context, future: () async { - if (widget.launch != null) { - return widget.launch?.call( + if (widget.onLaunch != null) { + return widget.onLaunch?.call( widget.activity, _avatar, _filename, @@ -584,11 +623,9 @@ class ActivitySuggestionDialogState extends State { duration: FluffyThemes.animationDuration, child: ConstrainedBox( constraints: FluffyThemes.isColumnMode(context) - ? const BoxConstraints( - maxWidth: 400.0, - ) + ? BoxConstraints(maxWidth: width) : BoxConstraints( - maxWidth: MediaQuery.of(context).size.width, + maxWidth: width, maxHeight: MediaQuery.of(context).size.height, ), child: ClipRRect( diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 230f3eb09..733598764 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -124,6 +124,17 @@ class _MxcImageState extends State { WidgetsBinding.instance.addPostFrameCallback(_tryLoad); } + // #Pangea + @override + void didUpdateWidget(covariant MxcImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.uri != widget.uri || oldWidget.cacheKey != widget.cacheKey) { + _imageData = null; + WidgetsBinding.instance.addPostFrameCallback(_tryLoad); + } + } + // Pangea# + Widget placeholder(BuildContext context) => widget.placeholder?.call(context) ?? Container(