2765 direct users to add to chat with multiselect rather than create (#2824)
* chore: abstract activity editting into builder widget * feat: allow users to launch activities to existing chats instead of making new chat
This commit is contained in:
parent
454ddeb2c0
commit
8289a33c2d
16 changed files with 1868 additions and 1355 deletions
|
|
@ -4936,5 +4936,12 @@
|
|||
"permissions": "Permissions",
|
||||
"spaceChildPermission": "Who can add new chats and subspaces to this space",
|
||||
"addEnvironmentOverride": "Add environment override",
|
||||
"defaultOption": "Default"
|
||||
"defaultOption": "Default",
|
||||
"chatWithActivities": "Chat with activities",
|
||||
"findYourPeople": "Find your people",
|
||||
"launch": "Launch",
|
||||
"launchActivityToChats": "Launch activity to chats",
|
||||
"searchChats": "Search chats",
|
||||
"selectChats": "Select chats",
|
||||
"selectChatToStart": "Complete! Select a chat to start"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,12 +203,23 @@ abstract class AppRoutes {
|
|||
...newRoomRoutes,
|
||||
GoRoute(
|
||||
path: '/planner',
|
||||
redirect: loggedOutRedirect,
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const ActivityGenerator(),
|
||||
const ActivityPlannerPage(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/generator',
|
||||
redirect: loggedOutRedirect,
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const ActivityGenerator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/activity_generator/activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_card.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/suggestion_form_field.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
|
|
@ -53,13 +54,15 @@ class ActivityGeneratorView extends StatelessWidget {
|
|||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.activities!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ActivityPlanCard(
|
||||
activity: controller.activities![index],
|
||||
return ActivityPlannerBuilder(
|
||||
initialActivity: controller.activities![index],
|
||||
initialFilename: controller.filename,
|
||||
room: controller.room,
|
||||
onEdit: (updatedActivity) =>
|
||||
controller.onEdit(index, updatedActivity),
|
||||
onChange: controller.update,
|
||||
initialImageURL: controller.filename,
|
||||
builder: (c) {
|
||||
return ActivityPlanCard(
|
||||
controller: c,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,37 +6,23 @@ import 'package:flutter/material.dart';
|
|||
import 'package:cached_network_image/cached_network_image.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:material_symbols_icons/symbols.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';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_room_selection.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/pangea/common/widgets/full_width_dialog.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ActivityPlanCard extends StatefulWidget {
|
||||
final ActivityPlanModel activity;
|
||||
final Room? room;
|
||||
final VoidCallback onChange;
|
||||
final ValueChanged<ActivityPlanModel> onEdit;
|
||||
final double maxWidth;
|
||||
final String? initialImageURL;
|
||||
final ActivityPlannerBuilderState controller;
|
||||
|
||||
const ActivityPlanCard({
|
||||
super.key,
|
||||
required this.activity,
|
||||
required this.room,
|
||||
required this.onChange,
|
||||
required this.onEdit,
|
||||
this.maxWidth = 400,
|
||||
this.initialImageURL,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -44,59 +30,8 @@ class ActivityPlanCard extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ActivityPlanCardState extends State<ActivityPlanCard> {
|
||||
bool _isEditing = false;
|
||||
late ActivityPlanModel _tempActivity;
|
||||
late TextEditingController _titleController;
|
||||
late TextEditingController _learningObjectiveController;
|
||||
late TextEditingController _instructionsController;
|
||||
final TextEditingController _newVocabController = TextEditingController();
|
||||
final FocusNode _vocabFocusNode = FocusNode();
|
||||
|
||||
Uint8List? _avatar;
|
||||
String? _filename;
|
||||
String? _imageURL;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tempActivity = widget.activity;
|
||||
_titleController = TextEditingController(text: _tempActivity.title);
|
||||
_learningObjectiveController =
|
||||
TextEditingController(text: _tempActivity.learningObjective);
|
||||
_instructionsController =
|
||||
TextEditingController(text: _tempActivity.instructions);
|
||||
_filename = widget.initialImageURL?.split("/").last;
|
||||
_imageURL = widget.activity.imageURL ?? widget.initialImageURL;
|
||||
}
|
||||
|
||||
static const double itemPadding = 12;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_learningObjectiveController.dispose();
|
||||
_instructionsController.dispose();
|
||||
_newVocabController.dispose();
|
||||
_vocabFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveEdits() async {
|
||||
final updatedActivity = ActivityPlanModel(
|
||||
req: _tempActivity.req,
|
||||
title: _titleController.text,
|
||||
learningObjective: _learningObjectiveController.text,
|
||||
instructions: _instructionsController.text,
|
||||
vocab: _tempActivity.vocab,
|
||||
imageURL: widget.activity.imageURL,
|
||||
);
|
||||
|
||||
widget.onEdit(updatedActivity);
|
||||
setState(() {
|
||||
_isEditing = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<ActivityPlanModel> _addBookmark(ActivityPlanModel activity) async {
|
||||
try {
|
||||
return BookmarkedActivitiesRepo.save(activity);
|
||||
|
|
@ -107,418 +42,350 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
|
|||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
widget.onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removeBookmark() async {
|
||||
try {
|
||||
BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId);
|
||||
BookmarkedActivitiesRepo.remove(
|
||||
widget.controller.updatedActivity.bookmarkId,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: stack, data: widget.activity.toJson());
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: stack,
|
||||
data: widget.controller.updatedActivity.toJson(),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
widget.onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _addVocab() {
|
||||
setState(() {
|
||||
_tempActivity.vocab.add(Vocab(lemma: _newVocabController.text, pos: ''));
|
||||
_newVocabController.clear();
|
||||
_vocabFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
void _removeVocab(int index) {
|
||||
setState(() {
|
||||
_tempActivity.vocab.removeAt(index);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
final url = await Matrix.of(context).client.uploadContent(
|
||||
bytes,
|
||||
filename: photo.name,
|
||||
);
|
||||
|
||||
final updatedActivity = ActivityPlanModel(
|
||||
req: _tempActivity.req,
|
||||
title: _tempActivity.title,
|
||||
learningObjective: _tempActivity.learningObjective,
|
||||
instructions: _tempActivity.instructions,
|
||||
vocab: _tempActivity.vocab,
|
||||
imageURL: url.toString(),
|
||||
);
|
||||
|
||||
widget.onEdit(updatedActivity);
|
||||
}
|
||||
|
||||
Future<void> _setAvatarByImageURL() async {
|
||||
if (_avatar != null || _imageURL == null) return;
|
||||
final resp = await http
|
||||
.get(Uri.parse(_imageURL!))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
if (mounted) {
|
||||
setState(() => _avatar = resp.bodyBytes);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLaunch() async {
|
||||
await _setAvatarByImageURL();
|
||||
await showFutureLoadingDialog(
|
||||
if (widget.controller.room != null) {
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: widget.controller.launchToRoom,
|
||||
);
|
||||
if (!resp.isError) {
|
||||
context.go("/rooms/${widget.controller.room!.id}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
String? avatarUrl;
|
||||
if (_avatar != null) {
|
||||
final client = Matrix.of(context).client;
|
||||
final url = await client.uploadContent(
|
||||
_avatar!,
|
||||
filename: _filename,
|
||||
);
|
||||
avatarUrl = url.toString();
|
||||
}
|
||||
|
||||
if (widget.room != null) {
|
||||
await widget.room?.sendActivityPlan(
|
||||
widget.activity,
|
||||
avatar: _avatar,
|
||||
filename: _filename,
|
||||
);
|
||||
|
||||
context.go("/rooms/${widget.room?.id}");
|
||||
return;
|
||||
}
|
||||
|
||||
final client = Matrix.of(context).client;
|
||||
final roomId = await client.createGroupChat(
|
||||
preset: CreateRoomPreset.publicChat,
|
||||
visibility: sdk.Visibility.private,
|
||||
groupName:
|
||||
widget.activity.title.isNotEmpty ? widget.activity.title : null,
|
||||
initialState: [
|
||||
if (_avatar != null) ...[
|
||||
StateEvent(
|
||||
type: EventTypes.RoomAvatar,
|
||||
stateKey: '',
|
||||
content: {
|
||||
"url": avatarUrl,
|
||||
},
|
||||
),
|
||||
],
|
||||
StateEvent(
|
||||
type: EventTypes.RoomPowerLevels,
|
||||
stateKey: '',
|
||||
content: defaultPowerLevels(client.userID!),
|
||||
builder: (context) {
|
||||
return FullWidthDialog(
|
||||
dialogContent: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
],
|
||||
enableEncryption: false,
|
||||
child: ActivityRoomSelection(
|
||||
controller: widget.controller,
|
||||
backButton: IconButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
),
|
||||
maxWidth: 400.0,
|
||||
maxHeight: 650.0,
|
||||
);
|
||||
|
||||
Room? room = client.getRoomById(roomId);
|
||||
if (room == null) {
|
||||
await client.waitForRoomInSync(roomId);
|
||||
room = client.getRoomById(roomId);
|
||||
}
|
||||
if (room == null) return;
|
||||
|
||||
await room.sendActivityPlan(
|
||||
widget.activity,
|
||||
avatar: _avatar,
|
||||
filename: _filename,
|
||||
);
|
||||
|
||||
context.go("/rooms/$roomId/invite?filter=groups");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool get isBookmarked =>
|
||||
BookmarkedActivitiesRepo.isBookmarked(widget.activity);
|
||||
bool get _isBookmarked => BookmarkedActivitiesRepo.isBookmarked(
|
||||
widget.controller.updatedActivity,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxWidth),
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: itemPadding),
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Form(
|
||||
key: widget.controller.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
alignment: Alignment.center,
|
||||
child: widget.controller.imageURL != null ||
|
||||
widget.controller.avatar != null
|
||||
? ClipRRect(
|
||||
child: widget.controller.avatar == null
|
||||
? CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: widget.controller.imageURL!,
|
||||
placeholder: (context, url) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(28.0),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.memory(
|
||||
widget.controller.avatar!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: const Padding(
|
||||
padding: EdgeInsets.all(28.0),
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
alignment: Alignment.center,
|
||||
child: _imageURL != null || _avatar != null
|
||||
? ClipRRect(
|
||||
child: _avatar == null
|
||||
? CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: _imageURL!,
|
||||
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,
|
||||
),
|
||||
)
|
||||
: const Padding(
|
||||
padding: EdgeInsets.all(28.0),
|
||||
if (widget.controller.isEditing)
|
||||
Positioned(
|
||||
top: 10.0,
|
||||
right: 10.0,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.upload_outlined),
|
||||
onPressed: widget.controller.selectAvatar,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
),
|
||||
if (_isEditing)
|
||||
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,
|
||||
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: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller:
|
||||
widget.controller.titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).activityTitle,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity.title,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.activity.title,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
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.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(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) ...[
|
||||
if (!widget.controller.isEditing)
|
||||
IconButton(
|
||||
onPressed: _isBookmarked
|
||||
? () => _removeBookmark()
|
||||
: () => _addBookmark(
|
||||
widget.controller.updatedActivity,
|
||||
),
|
||||
icon: Icon(
|
||||
_isBookmarked
|
||||
? Icons.bookmark
|
||||
: Icons.bookmark_border,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.dictionary,
|
||||
Symbols.target,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: List<Widget>.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(),
|
||||
child: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller: widget.controller
|
||||
.learningObjectivesController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.learningObjectiveLabel,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity
|
||||
.learningObjective,
|
||||
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: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller: widget
|
||||
.controller.instructionsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.instructions,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity
|
||||
.instructions,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
if (widget.controller.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<Widget>.generate(
|
||||
widget.controller.vocab.length,
|
||||
(int index) {
|
||||
return widget.controller.isEditing
|
||||
? Chip(
|
||||
label: Text(
|
||||
widget
|
||||
.controller.vocab[index].lemma,
|
||||
),
|
||||
onDeleted: () => widget.controller
|
||||
.removeVocab(index),
|
||||
backgroundColor: Colors.transparent,
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Chip(
|
||||
label: Text(
|
||||
widget
|
||||
.controller.vocab[index].lemma,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (widget.controller.isEditing) ...[
|
||||
const SizedBox(height: itemPadding),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: itemPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: widget.controller.vocabController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.addVocabulary,
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
widget.controller.addVocab();
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: widget.controller.addVocab,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: !widget.controller.isEditing
|
||||
? l10n.edit
|
||||
: l10n.saveChanges,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
!widget.controller.isEditing
|
||||
? Icons.edit
|
||||
: Icons.save,
|
||||
),
|
||||
onPressed: () => !widget.controller.isEditing
|
||||
? setState(() {
|
||||
widget.controller.isEditing = true;
|
||||
})
|
||||
: widget.controller.saveEdits(),
|
||||
isSelected: widget.controller.isEditing,
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
Tooltip(
|
||||
message: l10n.cancel,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.cancel),
|
||||
onPressed: widget.controller.clearEdits,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed:
|
||||
!widget.controller.isEditing ? _onLaunch : null,
|
||||
icon: const Icon(Icons.send),
|
||||
label: Text(l10n.launchActivityButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
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: [
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
233
lib/pangea/activity_planner/activity_planner_builder.dart
Normal file
233
lib/pangea/activity_planner/activity_planner_builder.dart
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/client_download_content_extension.dart';
|
||||
import 'package:fluffychat/utils/file_selector.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ActivityPlannerBuilder extends StatefulWidget {
|
||||
final ActivityPlanModel initialActivity;
|
||||
final String? initialFilename;
|
||||
final Room? room;
|
||||
|
||||
final Widget Function(ActivityPlannerBuilderState) builder;
|
||||
|
||||
final Future<void> Function(
|
||||
String,
|
||||
ActivityPlanModel,
|
||||
Uint8List?,
|
||||
String?,
|
||||
)? onEdit;
|
||||
|
||||
const ActivityPlannerBuilder({
|
||||
super.key,
|
||||
required this.initialActivity,
|
||||
this.initialFilename,
|
||||
this.room,
|
||||
required this.builder,
|
||||
this.onEdit,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityPlannerBuilder> createState() => ActivityPlannerBuilderState();
|
||||
}
|
||||
|
||||
class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
||||
bool isEditing = false;
|
||||
Uint8List? avatar;
|
||||
String? imageURL;
|
||||
String? filename;
|
||||
|
||||
final TextEditingController titleController = TextEditingController();
|
||||
final TextEditingController instructionsController = TextEditingController();
|
||||
final TextEditingController vocabController = TextEditingController();
|
||||
final TextEditingController participantsController = TextEditingController();
|
||||
final TextEditingController learningObjectivesController =
|
||||
TextEditingController();
|
||||
|
||||
final List<Vocab> vocab = [];
|
||||
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_resetActivity();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
titleController.dispose();
|
||||
learningObjectivesController.dispose();
|
||||
instructionsController.dispose();
|
||||
vocabController.dispose();
|
||||
participantsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Room? get room => widget.room;
|
||||
|
||||
ActivityPlanModel get updatedActivity {
|
||||
final int participants = int.tryParse(participantsController.text.trim()) ??
|
||||
widget.initialActivity.req.numberOfParticipants;
|
||||
|
||||
final updatedReq = widget.initialActivity.req;
|
||||
updatedReq.numberOfParticipants = participants;
|
||||
|
||||
return ActivityPlanModel(
|
||||
req: updatedReq,
|
||||
title: titleController.text,
|
||||
learningObjective: learningObjectivesController.text,
|
||||
instructions: instructionsController.text,
|
||||
vocab: vocab,
|
||||
imageURL: imageURL,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _resetActivity() async {
|
||||
avatar = null;
|
||||
filename = null;
|
||||
imageURL = null;
|
||||
|
||||
titleController.text = widget.initialActivity.title;
|
||||
learningObjectivesController.text =
|
||||
widget.initialActivity.learningObjective;
|
||||
instructionsController.text = widget.initialActivity.instructions;
|
||||
participantsController.text =
|
||||
widget.initialActivity.req.numberOfParticipants.toString();
|
||||
|
||||
vocab.clear();
|
||||
vocab.addAll(widget.initialActivity.vocab);
|
||||
|
||||
imageURL = widget.initialActivity.imageURL;
|
||||
filename = widget.initialFilename;
|
||||
await _setAvatarByURL();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void setEditing(bool editting) {
|
||||
isEditing = editting;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
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(() {});
|
||||
}
|
||||
|
||||
void selectAvatar() async {
|
||||
final photo = await selectFiles(
|
||||
context,
|
||||
type: FileSelectorType.images,
|
||||
allowMultiple: false,
|
||||
);
|
||||
final bytes = await photo.singleOrNull?.readAsBytes();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
avatar = bytes;
|
||||
filename = photo.singleOrNull?.name;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setAvatarByURL() async {
|
||||
if (widget.initialActivity.imageURL == null) return;
|
||||
try {
|
||||
if (avatar == null) {
|
||||
if (widget.initialActivity.imageURL!.startsWith("mxc")) {
|
||||
final client = Matrix.of(context).client;
|
||||
final mxcUri = Uri.parse(widget.initialActivity.imageURL!);
|
||||
final data = await client.downloadMxcCached(mxcUri);
|
||||
avatar = data;
|
||||
filename = Uri.encodeComponent(
|
||||
mxcUri.pathSegments.last,
|
||||
);
|
||||
} else {
|
||||
final Response response =
|
||||
await http.get(Uri.parse(widget.initialActivity.imageURL!));
|
||||
avatar = response.bodyBytes;
|
||||
filename = Uri.encodeComponent(
|
||||
Uri.parse(widget.initialActivity.imageURL!).pathSegments.last,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {
|
||||
"imageURL": widget.initialActivity.imageURL,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateImageURL() async {
|
||||
if (avatar == null) return;
|
||||
final url = await Matrix.of(context).client.uploadContent(
|
||||
avatar!,
|
||||
filename: filename,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
imageURL = url.toString();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> saveEdits() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
await updateImageURL();
|
||||
setEditing(false);
|
||||
if (widget.onEdit != null) {
|
||||
await widget.onEdit!(
|
||||
widget.initialActivity.bookmarkId,
|
||||
updatedActivity,
|
||||
avatar,
|
||||
filename,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearEdits() async {
|
||||
_resetActivity();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> launchToRoom() async {
|
||||
return widget.room?.sendActivityPlan(
|
||||
updatedActivity,
|
||||
avatar: avatar,
|
||||
filename: filename,
|
||||
avatarURL: imageURL,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.builder(this);
|
||||
}
|
||||
|
|
@ -14,8 +14,8 @@ enum PageMode {
|
|||
}
|
||||
|
||||
class ActivityPlannerPage extends StatefulWidget {
|
||||
final String roomID;
|
||||
const ActivityPlannerPage({super.key, required this.roomID});
|
||||
final String? roomID;
|
||||
const ActivityPlannerPage({super.key, this.roomID});
|
||||
|
||||
@override
|
||||
ActivityPlannerPageState createState() => ActivityPlannerPageState();
|
||||
|
|
@ -23,7 +23,9 @@ class ActivityPlannerPage extends StatefulWidget {
|
|||
|
||||
class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
||||
PageMode pageMode = PageMode.featuredActivities;
|
||||
Room? get room => Matrix.of(context).client.getRoomById(widget.roomID);
|
||||
Room? get room => widget.roomID != null
|
||||
? Matrix.of(context).client.getRoomById(widget.roomID!)
|
||||
: null;
|
||||
|
||||
void _setPageMode(PageMode? mode) {
|
||||
if (mode == null) return;
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
|
|||
class ActivityPlannerPageAppBar extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
final PageMode pageMode;
|
||||
final String roomID;
|
||||
final String? roomID;
|
||||
|
||||
const ActivityPlannerPageAppBar({
|
||||
required this.pageMode,
|
||||
required this.roomID,
|
||||
this.roomID,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -68,7 +68,9 @@ class ActivityPlannerPageAppBar extends StatelessWidget
|
|||
alignment: Alignment.center,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => context.go('/rooms/$roomID/planner/generator'),
|
||||
onTap: () => roomID != null
|
||||
? context.go('/rooms/$roomID/planner/generator')
|
||||
: context.go("/homepage/planner/generator"),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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/activity_planner_builder.dart';
|
||||
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';
|
||||
|
|
@ -97,11 +98,16 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivitySuggestionDialog(
|
||||
return ActivityPlannerBuilder(
|
||||
initialActivity: activity,
|
||||
buttonText: L10n.of(context).inviteAndLaunch,
|
||||
room: widget.room,
|
||||
onEdit: _onEdit,
|
||||
room: widget.room,
|
||||
builder: (controller) {
|
||||
return ActivitySuggestionDialog(
|
||||
controller: controller,
|
||||
buttonText: l10n.launch,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
619
lib/pangea/activity_suggestions/activity_room_selection.dart
Normal file
619
lib/pangea/activity_suggestions/activity_room_selection.dart
Normal file
|
|
@ -0,0 +1,619 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
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';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class ActivityRoomSelection extends StatefulWidget {
|
||||
final ActivityPlannerBuilderState controller;
|
||||
final Widget backButton;
|
||||
|
||||
const ActivityRoomSelection({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.backButton,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityRoomSelection> createState() => ActivityRoomSelectionState();
|
||||
}
|
||||
|
||||
class ActivityRoomSelectionState extends State<ActivityRoomSelection> {
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
|
||||
bool _loading = false;
|
||||
bool _complete = false;
|
||||
|
||||
bool _hasBotDM = true;
|
||||
List<Room> _launchableRooms = [];
|
||||
final List<String> _selectedRooms = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_launchableRooms = Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where((room) {
|
||||
return room.canSendDefaultStates &&
|
||||
!room.isSpace &&
|
||||
!room.isAnalyticsRoom;
|
||||
})
|
||||
.toList()
|
||||
.sorted((a, b) {
|
||||
final aIsBotDM = a.directChatMatrixID == BotName.byEnvironment;
|
||||
final bIsBotDM = b.directChatMatrixID == BotName.byEnvironment;
|
||||
if (aIsBotDM && !bIsBotDM) return -1;
|
||||
if (!aIsBotDM && bIsBotDM) return 1;
|
||||
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||
});
|
||||
|
||||
_hasBotDM = Matrix.of(context).client.rooms.any((room) {
|
||||
if (room.isDirectChat &&
|
||||
room.directChatMatrixID == BotName.byEnvironment) {
|
||||
return true;
|
||||
}
|
||||
if (room.botOptions?.mode == BotMode.directChat) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Room> get _filteredRooms {
|
||||
final searchText = searchController.text.toLowerCase();
|
||||
return _launchableRooms.where((room) {
|
||||
return room.name.toLowerCase().contains(searchText);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _toggleRoomSelection(String roomId) {
|
||||
_selectedRooms.contains(roomId)
|
||||
? _selectedRooms.remove(roomId)
|
||||
: _selectedRooms.add(roomId);
|
||||
if (_selectedRooms.contains(roomId)) {
|
||||
_complete = false;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Map<String, Room> get _spaceDelegateCandidates {
|
||||
final spaces = Matrix.of(context).client.rooms.where((r) => r.isSpace);
|
||||
final candidates = <String, Room>{};
|
||||
for (final space in spaces) {
|
||||
for (final spaceChild in space.spaceChildren) {
|
||||
final roomId = spaceChild.roomId;
|
||||
if (roomId == null) continue;
|
||||
candidates[roomId] = space;
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
final Map<String, int> _launchStatus = {};
|
||||
|
||||
Future<void> _sendActivityPlan(Room room) async {
|
||||
try {
|
||||
setState(() => _launchStatus[room.id] = 0);
|
||||
await room.sendActivityPlan(
|
||||
widget.controller.updatedActivity,
|
||||
avatar: widget.controller.avatar,
|
||||
filename: widget.controller.filename,
|
||||
avatarURL: widget.controller.imageURL,
|
||||
);
|
||||
_launchStatus[room.id] = 1;
|
||||
} catch (e, s) {
|
||||
_launchStatus[room.id] = -1;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": room.id,
|
||||
"activity": widget.controller.updatedActivity.toJson(),
|
||||
"filename": widget.controller.filename,
|
||||
"avatarURL": widget.controller.imageURL,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _launchBotDM() async {
|
||||
try {
|
||||
setState(() => _launchStatus["placeholder"] = 0);
|
||||
|
||||
Uri? avatarUrl;
|
||||
final imageUrl = widget.controller.imageURL ??
|
||||
widget.controller.updatedActivity.imageURL;
|
||||
|
||||
Uint8List? avatar = widget.controller.avatar;
|
||||
if (avatar != null) {
|
||||
avatarUrl = await Matrix.of(context).client.uploadContent(
|
||||
widget.controller.avatar!,
|
||||
);
|
||||
} else if (imageUrl != null) {
|
||||
final Response response = await http.get(Uri.parse(imageUrl));
|
||||
avatar = response.bodyBytes;
|
||||
avatarUrl = await Matrix.of(context).client.uploadContent(
|
||||
avatar,
|
||||
);
|
||||
}
|
||||
|
||||
// avatar == null ? null : await client.uploadContent(avatar);
|
||||
final roomId = await Matrix.of(context).client.createRoom(
|
||||
name: widget.controller.updatedActivity.title,
|
||||
invite: [BotName.byEnvironment],
|
||||
isDirect: true,
|
||||
preset: CreateRoomPreset.trustedPrivateChat,
|
||||
initialState: [
|
||||
BotOptionsModel(mode: BotMode.directChat).toStateEvent,
|
||||
if (avatar != null && avatarUrl != null)
|
||||
StateEvent(
|
||||
type: EventTypes.RoomAvatar,
|
||||
content: {'url': avatarUrl.toString()},
|
||||
),
|
||||
],
|
||||
);
|
||||
Room? room = Matrix.of(context).client.getRoomById(roomId);
|
||||
if (room == null) {
|
||||
await Matrix.of(context).client.waitForRoomInSync(
|
||||
roomId,
|
||||
join: true,
|
||||
);
|
||||
|
||||
room = Matrix.of(context).client.getRoomById(roomId);
|
||||
if (room == null) {
|
||||
throw Exception("Room not found");
|
||||
}
|
||||
|
||||
await room.sendActivityPlan(
|
||||
widget.controller.updatedActivity,
|
||||
avatar: widget.controller.avatar,
|
||||
filename: widget.controller.filename,
|
||||
avatarURL: widget.controller.imageURL,
|
||||
);
|
||||
}
|
||||
_launchStatus["placeholder"] = 1;
|
||||
return roomId;
|
||||
} catch (e, s) {
|
||||
_launchStatus["placeholder"] = -1;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"activity": widget.controller.updatedActivity.toJson(),
|
||||
"filename": widget.controller.filename,
|
||||
"avatarURL": widget.controller.imageURL,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _launch() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final List<Future> futures = [];
|
||||
for (final roomId in _selectedRooms) {
|
||||
if (_launchStatus[roomId] == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final Room? room = _launchableRooms.firstWhereOrNull(
|
||||
(r) => r.id == roomId,
|
||||
);
|
||||
if (room == null) {
|
||||
if (roomId == 'placeholder') futures.add(_launchBotDM());
|
||||
} else {
|
||||
futures.add(_sendActivityPlan(room));
|
||||
}
|
||||
}
|
||||
|
||||
final resp = await Future.wait(futures);
|
||||
_complete = true;
|
||||
if (!mounted) return;
|
||||
if (_selectedRooms.length == 1 &&
|
||||
_launchStatus[_selectedRooms.first] == 1) {
|
||||
if (_selectedRooms.first == 'placeholder' && resp.first != null) {
|
||||
context.go("/rooms/${resp.first}");
|
||||
Navigator.of(context).pop();
|
||||
} else if (_selectedRooms.first != 'placeholder') {
|
||||
context.go('/rooms/${_selectedRooms.first}');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"activity": widget.controller.updatedActivity.toJson(),
|
||||
"filename": widget.controller.filename,
|
||||
"avatarURL": widget.controller.imageURL,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _tooltip(String roomId) {
|
||||
final status = _launchStatus[roomId];
|
||||
if (status == 0) {
|
||||
return "Sending...";
|
||||
} else if (status == 1) {
|
||||
return "Go to chat";
|
||||
} else if (status == -1) {
|
||||
return "Failed to send";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void _onTap(Room room) {
|
||||
final status = _launchStatus[room.id];
|
||||
if (status == 0) {
|
||||
return;
|
||||
} else if (status == 1) {
|
||||
context.go('/rooms/${room.id}');
|
||||
Navigator.of(context).pop();
|
||||
} else if (status == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("Toggling room selection for ${room.id}");
|
||||
_toggleRoomSelection(room.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).selectChats),
|
||||
leading: widget.backButton,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Column(
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
focusNode: searchFocusNode,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (text) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
hintText: L10n.of(context).searchChats,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context).cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
searchController.clear();
|
||||
searchFocusNode.unfocus();
|
||||
});
|
||||
},
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: () => searchFocusNode.requestFocus(),
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredRooms.length + (_hasBotDM ? 0 : 1),
|
||||
itemBuilder: (context, index) {
|
||||
if (!_hasBotDM && index == 0) {
|
||||
return ChatActivityPlaceholder(
|
||||
activity: widget.controller.updatedActivity,
|
||||
selected: _selectedRooms.contains("placeholder"),
|
||||
onTap: () {
|
||||
_toggleRoomSelection("placeholder");
|
||||
},
|
||||
tooltip: _tooltip("placeholder"),
|
||||
status: _launchStatus["placeholder"],
|
||||
avatar: widget.controller.avatar,
|
||||
);
|
||||
}
|
||||
if (!_hasBotDM) index--;
|
||||
|
||||
final room = _filteredRooms[index];
|
||||
final displayname = room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
);
|
||||
final space = _spaceDelegateCandidates[room.id];
|
||||
return Tooltip(
|
||||
message: _tooltip(room.id),
|
||||
child: ListTile(
|
||||
title: Text(displayname),
|
||||
leading: SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
height: Avatar.defaultSize,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (space != null)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Avatar(
|
||||
border: BorderSide(
|
||||
width: 2,
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 4,
|
||||
),
|
||||
mxContent: space.avatar,
|
||||
size: Avatar.defaultSize * 0.75,
|
||||
name: space.getLocalizedDisplayname(),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Avatar(
|
||||
border: space == null
|
||||
? room.isSpace
|
||||
? BorderSide(
|
||||
width: 1,
|
||||
color: theme.dividerColor,
|
||||
)
|
||||
: null
|
||||
: BorderSide(
|
||||
width: 2,
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
mxContent: room.avatar,
|
||||
size: Avatar.defaultSize * 0.75,
|
||||
name: displayname,
|
||||
presenceUserId: room.directChatMatrixID,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: Container(
|
||||
width: 30.0,
|
||||
height: 30.0,
|
||||
alignment: Alignment.center,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final status = _launchStatus[room.id];
|
||||
|
||||
if (status == 0) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
} else if (status == 1) {
|
||||
return const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: AppConfig.success,
|
||||
);
|
||||
} else if (status == -1) {
|
||||
return Icon(
|
||||
Icons.error_outline,
|
||||
color: theme.colorScheme.error,
|
||||
);
|
||||
}
|
||||
|
||||
return Checkbox(
|
||||
value: _selectedRooms.contains(room.id),
|
||||
onChanged: (_) => _onTap(room),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () => _onTap(room),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: _complete
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context).selectChatToStart),
|
||||
)
|
||||
: ElevatedButton(
|
||||
onPressed: _selectedRooms.isNotEmpty ? _launch : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size.zero,
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
disabledBackgroundColor: theme.colorScheme.primary,
|
||||
disabledForegroundColor: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_loading
|
||||
? const Expanded(
|
||||
child: SizedBox(
|
||||
height: 10,
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
L10n.of(context).launchActivityToChats,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatActivityPlaceholder extends StatelessWidget {
|
||||
final ActivityPlanModel activity;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
final String tooltip;
|
||||
final Uint8List? avatar;
|
||||
final int? status;
|
||||
|
||||
const ChatActivityPlaceholder({
|
||||
required this.activity,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
required this.tooltip,
|
||||
required this.status,
|
||||
this.avatar,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
const size = Avatar.defaultSize * 0.75;
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: ListTile(
|
||||
title: Text(activity.title),
|
||||
leading: SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
height: Avatar.defaultSize,
|
||||
child: SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: Material(
|
||||
color: theme.brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(size / 2),
|
||||
side: BorderSide.none,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: avatar != null
|
||||
? Image.memory(avatar!)
|
||||
: activity.imageURL != null
|
||||
? activity.imageURL!.startsWith('mxc')
|
||||
? MxcImage(
|
||||
uri: Uri.parse(activity.imageURL!),
|
||||
width: size,
|
||||
height: size,
|
||||
cacheKey: activity.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activity.imageURL!,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: Container(
|
||||
width: 30.0,
|
||||
height: 30.0,
|
||||
alignment: Alignment.center,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (status == 0) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
} else if (status == 1) {
|
||||
return const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: AppConfig.success,
|
||||
);
|
||||
} else if (status == -1) {
|
||||
return Icon(
|
||||
Icons.error_outline,
|
||||
color: theme.colorScheme.error,
|
||||
);
|
||||
}
|
||||
|
||||
return Checkbox(
|
||||
value: selected,
|
||||
onChanged: (_) => onTap(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import 'package:shimmer/shimmer.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.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';
|
||||
|
|
@ -44,7 +45,6 @@ class ActivitySuggestionCarousel extends StatefulWidget {
|
|||
|
||||
class ActivitySuggestionCarouselState
|
||||
extends State<ActivitySuggestionCarousel> {
|
||||
bool _isOpen = true;
|
||||
bool _loading = true;
|
||||
String? _error;
|
||||
|
||||
|
|
@ -138,7 +138,6 @@ class ActivitySuggestionCarouselState
|
|||
|
||||
void _close() {
|
||||
widget.onActivitySelected(null, null, null);
|
||||
setState(() => _isOpen = false);
|
||||
}
|
||||
|
||||
void _onClickCard() {
|
||||
|
|
@ -150,13 +149,23 @@ class ActivitySuggestionCarouselState
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivitySuggestionDialog(
|
||||
return ActivityPlannerBuilder(
|
||||
initialActivity: _currentActivity!,
|
||||
buttonText: L10n.of(context).selectActivity,
|
||||
onLaunch: widget.onActivitySelected,
|
||||
builder: (controller) {
|
||||
return ActivitySuggestionDialog(
|
||||
controller: controller,
|
||||
buttonText: L10n.of(context).selectActivity,
|
||||
onLaunch: () => widget.onActivitySelected(
|
||||
controller.updatedActivity,
|
||||
controller.avatar,
|
||||
controller.filename,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -167,164 +176,156 @@ class ActivitySuggestionCarouselState
|
|||
final theme = Theme.of(context);
|
||||
return AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: !_isOpen
|
||||
? const SizedBox.shrink()
|
||||
: AnimatedOpacity(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
opacity: widget.enabled ? 1.0 : 0.5,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0,
|
||||
horizontal: 4.0,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 16.0,
|
||||
child: AnimatedOpacity(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
opacity: widget.enabled ? 1.0 : 0.5,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0,
|
||||
horizontal: 4.0,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).newChatActivityTitle,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: widget.enabled ? _close : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).newChatActivityTitle,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(L10n.of(context).newChatActivityDesc),
|
||||
),
|
||||
Row(
|
||||
spacing: _isColumnMode ? 16.0 : 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MouseRegion(
|
||||
cursor: _canMoveLeft
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.basic,
|
||||
child: GestureDetector(
|
||||
onTap: _canMoveLeft ? _moveLeft : null,
|
||||
child: Icon(
|
||||
Icons.chevron_left_outlined,
|
||||
size: 32.0,
|
||||
color: _canMoveLeft ? null : theme.disabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints:
|
||||
BoxConstraints(maxHeight: _cardHeight + 12.0),
|
||||
child: _error != null ||
|
||||
(_currentActivity == null && !_loading)
|
||||
? const SizedBox.shrink()
|
||||
: _loading
|
||||
? Shimmer.fromColors(
|
||||
baseColor: theme.colorScheme.primary
|
||||
.withAlpha(50),
|
||||
highlightColor: theme.colorScheme.primary
|
||||
.withAlpha(150),
|
||||
child: Container(
|
||||
height: _cardHeight,
|
||||
width: _cardWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme.surfaceContainer,
|
||||
borderRadius:
|
||||
BorderRadius.circular(24.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ActivitySuggestionCard(
|
||||
selected: widget.selectedActivity ==
|
||||
_currentActivity,
|
||||
activity: _currentActivity!,
|
||||
onPressed:
|
||||
widget.enabled ? _onClickCard : null,
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
image: _currentActivity ==
|
||||
widget.selectedActivity
|
||||
? widget.selectedActivityImage
|
||||
: null,
|
||||
onChange: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: _canMoveRight
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.basic,
|
||||
child: GestureDetector(
|
||||
onTap: _canMoveRight ? _moveRight : null,
|
||||
child: Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
size: 32.0,
|
||||
color: _canMoveRight ? null : theme.disabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16.0,
|
||||
children: _activityItems.mapIndexed((i, activity) {
|
||||
final selected = activity == _currentActivity;
|
||||
return InkWell(
|
||||
enableFeedback: widget.enabled,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
onTap: widget.enabled
|
||||
? () => _setActivityByIndex(i)
|
||||
: null,
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(
|
||||
sigmaX: selected ? 0.0 : 0.5,
|
||||
sigmaY: selected ? 0.0 : 0.5,
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: selected ? 1.0 : 0.5,
|
||||
child: ClipOval(
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size.fromRadius(12.0),
|
||||
child: activity.imageURL != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: activity.imageURL!,
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
progressIndicatorBuilder:
|
||||
(context, url, progress) {
|
||||
return CircularProgressIndicator(
|
||||
value: progress.progress,
|
||||
);
|
||||
},
|
||||
)
|
||||
: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.secondary,
|
||||
radius: 12.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: widget.enabled ? _close : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(L10n.of(context).newChatActivityDesc),
|
||||
),
|
||||
Row(
|
||||
spacing: _isColumnMode ? 16.0 : 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MouseRegion(
|
||||
cursor: _canMoveLeft
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.basic,
|
||||
child: GestureDetector(
|
||||
onTap: _canMoveLeft ? _moveLeft : null,
|
||||
child: Icon(
|
||||
Icons.chevron_left_outlined,
|
||||
size: 32.0,
|
||||
color: _canMoveLeft ? null : theme.disabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxHeight: _cardHeight + 12.0),
|
||||
child: _error != null ||
|
||||
(_currentActivity == null && !_loading)
|
||||
? const SizedBox.shrink()
|
||||
: _loading
|
||||
? Shimmer.fromColors(
|
||||
baseColor:
|
||||
theme.colorScheme.primary.withAlpha(50),
|
||||
highlightColor:
|
||||
theme.colorScheme.primary.withAlpha(150),
|
||||
child: Container(
|
||||
height: _cardHeight,
|
||||
width: _cardWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ActivitySuggestionCard(
|
||||
selected:
|
||||
widget.selectedActivity == _currentActivity,
|
||||
activity: _currentActivity!,
|
||||
onPressed: widget.enabled ? _onClickCard : null,
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
image:
|
||||
_currentActivity == widget.selectedActivity
|
||||
? widget.selectedActivityImage
|
||||
: null,
|
||||
onChange: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: _canMoveRight
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.basic,
|
||||
child: GestureDetector(
|
||||
onTap: _canMoveRight ? _moveRight : null,
|
||||
child: Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
size: 32.0,
|
||||
color: _canMoveRight ? null : theme.disabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16.0,
|
||||
children: _activityItems.mapIndexed((i, activity) {
|
||||
final selected = activity == _currentActivity;
|
||||
return InkWell(
|
||||
enableFeedback: widget.enabled,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
onTap: widget.enabled ? () => _setActivityByIndex(i) : null,
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(
|
||||
sigmaX: selected ? 0.0 : 0.5,
|
||||
sigmaY: selected ? 0.0 : 0.5,
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: selected ? 1.0 : 0.5,
|
||||
child: ClipOval(
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size.fromRadius(12.0),
|
||||
child: activity.imageURL != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: activity.imageURL!,
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
progressIndicatorBuilder:
|
||||
(context, url, progress) {
|
||||
return CircularProgressIndicator(
|
||||
value: progress.progress,
|
||||
);
|
||||
},
|
||||
)
|
||||
: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.secondary,
|
||||
radius: 12.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,16 +10,14 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.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/activity_suggestions_constants.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/customized_svg.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/widgets/matrix.dart';
|
||||
|
|
@ -135,10 +133,15 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivitySuggestionDialog(
|
||||
return ActivityPlannerBuilder(
|
||||
initialActivity: activity,
|
||||
buttonText: L10n.of(context).inviteAndLaunch,
|
||||
room: widget.room,
|
||||
builder: (controller) {
|
||||
return ActivitySuggestionDialog(
|
||||
controller: controller,
|
||||
buttonText: L10n.of(context).launch,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -165,7 +168,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).startChat,
|
||||
L10n.of(context).chatWithActivities,
|
||||
style: isColumnMode
|
||||
? theme.textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold)
|
||||
|
|
@ -175,91 +178,10 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => context.go('/homepage/newgroup'),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(36.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 10.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CustomizedSvg(
|
||||
svgUrl:
|
||||
"${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.plusIconPath}",
|
||||
colorReplacements: {
|
||||
"#CDBEF9": colorToHex(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
},
|
||||
height: 16.0,
|
||||
width: 16.0,
|
||||
),
|
||||
Text(
|
||||
isColumnMode
|
||||
? L10n.of(context).createOwnChat
|
||||
: L10n.of(context).chat,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => context.go('/homepage/planner'),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(36.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 10.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CustomizedSvg(
|
||||
svgUrl:
|
||||
"${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.crayonIconPath}",
|
||||
colorReplacements: {
|
||||
"#CDBEF9": colorToHex(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
},
|
||||
height: 16.0,
|
||||
width: 16.0,
|
||||
),
|
||||
Text(
|
||||
isColumnMode
|
||||
? L10n.of(context).makeYourOwnActivity
|
||||
: L10n.of(context).createActivity,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu_outlined),
|
||||
onPressed: () => context.go('/homepage/planner'),
|
||||
tooltip: L10n.of(context).activityPlannerTitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class SuggestionsPage extends StatelessWidget {
|
|||
vertical: 16.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 24.0,
|
||||
children: [
|
||||
if (!isColumnMode) const LearningProgressIndicators(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
|
@ -16,24 +18,32 @@ class FullWidthDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = ConstrainedBox(
|
||||
constraints: FluffyThemes.isColumnMode(context)
|
||||
? BoxConstraints(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
)
|
||||
: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width,
|
||||
maxHeight: MediaQuery.of(context).size.height,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: dialogContent,
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
final content = AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: ConstrainedBox(
|
||||
constraints: isColumnMode
|
||||
? BoxConstraints(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
)
|
||||
: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width,
|
||||
maxHeight: MediaQuery.of(context).size.height,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
isColumnMode ? BorderRadius.circular(20.0) : BorderRadius.zero,
|
||||
child: dialogContent,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return FluffyThemes.isColumnMode(context)
|
||||
? Dialog(child: content)
|
||||
: Dialog.fullscreen(child: content);
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5),
|
||||
child: isColumnMode
|
||||
? Dialog(child: content)
|
||||
: Dialog.fullscreen(child: content),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,10 +274,20 @@ extension EventsRoomExtension on Room {
|
|||
}) async {
|
||||
Uint8List? bytes = avatar;
|
||||
if (avatarURL != null && bytes == null) {
|
||||
final resp = await http
|
||||
.get(Uri.parse(avatarURL))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
bytes = resp.bodyBytes;
|
||||
try {
|
||||
final resp = await http
|
||||
.get(Uri.parse(avatarURL))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
bytes = resp.bodyBytes;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"avatarURL": avatarURL,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MatrixFile? file;
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ class PublicSpacesAreaState extends State<PublicSpacesArea> {
|
|||
key: const ValueKey('title'),
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).publicSpacesTitle,
|
||||
L10n.of(context).findYourPeople,
|
||||
style: isColumnMode
|
||||
? theme.textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue