chore: improvements to activity bookmarking (#2275)
This commit is contained in:
parent
b6e27d739a
commit
6f5f0b5825
8 changed files with 183 additions and 79 deletions
|
|
@ -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(() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class ActivitySuggestionCarouselState
|
|||
return ActivitySuggestionDialog(
|
||||
activity: _currentActivity!,
|
||||
buttonText: L10n.of(context).selectActivity,
|
||||
launch: widget.onActivitySelected,
|
||||
onLaunch: widget.onActivitySelected,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue