chore: improvements to activity bookmarking (#2275)

This commit is contained in:
ggurdin 2025-03-31 12:42:42 -04:00 committed by GitHub
parent b6e27d739a
commit 6f5f0b5825
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 183 additions and 79 deletions

View file

@ -98,29 +98,37 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
});
}
Future<ActivityPlanModel> _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<ActivityPlanModel> _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<void> _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<void> _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(() {

View file

@ -10,6 +10,7 @@ class ActivityPlanModel {
String instructions;
List<Vocab> 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 {

View file

@ -9,9 +9,13 @@ class BookmarkedActivitiesRepo {
/// save an activity to the list of bookmarked activities
/// returns the activity with a bookmarkId
static Future<ActivityPlanModel> save(ActivityPlanModel activity) async {
static Future<ActivityPlanModel> 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<ActivityPlanModel> get() {

View file

@ -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<BookmarkedActivitiesList> {
double get cardPadding => _isColumnMode ? 8.0 : 0.0;
double get cardWidth => _isColumnMode ? 225.0 : 150.0;
Future<void> _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<BookmarkedActivitiesList> {
activity: activity,
buttonText: L10n.of(context).inviteAndLaunch,
room: widget.room,
onEdit: _onEdit,
);
},
);

View file

@ -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,
),

View file

@ -140,7 +140,7 @@ class ActivitySuggestionCarouselState
return ActivitySuggestionDialog(
activity: _currentActivity!,
buttonText: L10n.of(context).selectActivity,
launch: widget.onActivitySelected,
onLaunch: widget.onActivitySelected,
);
},
);

View file

@ -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<void> 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<ActivitySuggestionDialog> {
context.go("/rooms/$roomId/invite");
}
Future<void> _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<ActivitySuggestionDialog> {
Container(
height: 200,
decoration: BoxDecoration(
image: widget.activity.imageURL != null || _avatar != null
? DecorationImage(
image: _avatar != null
? MemoryImage(_avatar!)
: NetworkImage(widget.activity.imageURL!)
as ImageProvider<Object>,
)
: 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<ActivitySuggestionDialog> {
_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<ActivitySuggestionDialog> {
?.copyWith(color: theme.colorScheme.onPrimary),
),
),
),
if (!_isEditing)
)
else
Expanded(
child: ElevatedButton(
onPressed: () async {
@ -505,8 +544,8 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
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<ActivitySuggestionDialog> {
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(

View file

@ -124,6 +124,17 @@ class _MxcImageState extends State<MxcImage> {
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(