Skip to content

Commit 25b1c63

Browse files
authored
chore: add images to activity planner page (#2169)
1 parent 24621be commit 25b1c63

6 files changed

+182
-42
lines changed

lib/pangea/activity_planner/activity_list_view.dart

+144-2
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,26 @@ import 'dart:developer';
33
import 'package:flutter/foundation.dart';
44
import 'package:flutter/material.dart';
55

6+
import 'package:cached_network_image/cached_network_image.dart';
7+
import 'package:collection/collection.dart';
68
import 'package:flutter_gen/gen_l10n/l10n.dart';
9+
import 'package:http/http.dart' as http;
710
import 'package:matrix/matrix.dart';
811

12+
import 'package:fluffychat/config/app_config.dart';
13+
import 'package:fluffychat/config/themes.dart';
914
import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.dart';
1015
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
1116
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
1217
import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart';
18+
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
1319
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
20+
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
21+
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
1422
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
1523
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
1624
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
25+
import 'package:fluffychat/utils/file_selector.dart';
1726
import 'package:fluffychat/widgets/future_loading_dialog.dart';
1827
import 'activity_plan_card.dart';
1928

@@ -23,10 +32,13 @@ class ActivityListView extends StatefulWidget {
2332
/// if null, show saved activities
2433
final ActivityPlanRequest? activityPlanRequest;
2534

35+
final ActivityPlannerPageState controller;
36+
2637
const ActivityListView({
2738
super.key,
2839
required this.room,
2940
required this.activityPlanRequest,
41+
required this.controller,
3042
});
3143

3244
@override
@@ -41,10 +53,15 @@ class ActivityListViewState extends State<ActivityListView> {
4153
bool _isLoading = true;
4254
Object? _error;
4355

56+
Uint8List? _avatar;
57+
String? _avatarURL;
58+
String? _filename;
59+
4460
@override
4561
void initState() {
4662
super.initState();
4763
_loadActivities();
64+
_setModeImageURL();
4865
}
4966

5067
Future<void> _loadActivities() async {
@@ -108,12 +125,79 @@ class ActivityListViewState extends State<ActivityListView> {
108125
return;
109126
}
110127

111-
await widget.room?.setPinnedEvents([eventId]);
128+
Uint8List? bytes = _avatar;
129+
if (_avatarURL != null && bytes == null) {
130+
final resp = await http
131+
.get(Uri.parse(_avatarURL!))
132+
.timeout(const Duration(seconds: 5));
133+
bytes = resp.bodyBytes;
134+
}
135+
136+
if (bytes != null && _filename != null) {
137+
final file = MatrixFile(
138+
bytes: bytes,
139+
name: _filename!,
140+
);
141+
142+
await widget.room?.sendFileEvent(
143+
file,
144+
shrinkImageMaxDimension: 1600,
145+
extraContent: {
146+
ModelKey.messageTags: ModelKey.messageTagActivityPlan,
147+
},
148+
);
149+
}
150+
151+
if (widget.room != null && widget.room!.canSendDefaultStates) {
152+
await widget.room?.setPinnedEvents([eventId]);
153+
}
112154

113155
Navigator.of(context).pop();
114156
},
115157
);
116158

159+
Future<ActivitySettingResponseSchema?> get _selectedMode async {
160+
final modes = await widget.controller.modeItems;
161+
return modes.firstWhereOrNull(
162+
(element) =>
163+
element.name.toLowerCase() ==
164+
widget.activityPlanRequest?.mode.toLowerCase(),
165+
);
166+
}
167+
168+
Future<void> _setModeImageURL() async {
169+
final mode = await _selectedMode;
170+
if (mode == null) return;
171+
172+
final modeName =
173+
mode.defaultName.toLowerCase().replaceAll(RegExp(r'\s+'), '');
174+
final filename =
175+
"${ActivitySuggestionsConstants.modeImageFileStart}$modeName.jpg";
176+
177+
if (!mounted) return;
178+
setState(() {
179+
_avatarURL = "${AppConfig.assetsBaseURL}/$filename";
180+
_filename = filename;
181+
});
182+
}
183+
184+
void selectPhoto() async {
185+
final resp = await selectFiles(
186+
context,
187+
type: FileSelectorType.images,
188+
allowMultiple: false,
189+
);
190+
191+
final photo = resp.singleOrNull;
192+
if (photo == null) return;
193+
final bytes = await photo.readAsBytes();
194+
195+
setState(() {
196+
_avatar = bytes;
197+
_filename = photo.name;
198+
});
199+
}
200+
117201
@override
118202
Widget build(BuildContext context) {
119203
final l10n = L10n.of(context);
@@ -152,8 +236,66 @@ class ActivityListViewState extends State<ActivityListView> {
152236
padding: const EdgeInsets.all(16),
153237
itemCount: widget.activityPlanRequest == null
154238
? _bookmarkedActivities.length
155-
: _activities!.length,
239+
: _activities!.length + 1,
156240
itemBuilder: (context, index) {
241+
if (index == 0) {
242+
return Center(
243+
child: Stack(
244+
alignment: Alignment.bottomCenter,
245+
children: [
246+
Column(
247+
children: [
248+
AnimatedSize(
249+
duration: FluffyThemes.animationDuration,
250+
child: Container(
251+
decoration: BoxDecoration(
252+
borderRadius: BorderRadius.circular(12.0),
253+
),
254+
width: 400.0,
255+
clipBehavior: Clip.hardEdge,
256+
child: _avatarURL != null || _avatar != null
257+
? ClipRRect(
258+
child: _avatar == null
259+
? CachedNetworkImage(
260+
fit: BoxFit.cover,
261+
imageUrl: _avatarURL!,
262+
placeholder: (context, url) {
263+
return const Center(
264+
child:
265+
CircularProgressIndicator(),
266+
);
267+
},
268+
errorWidget: (context, url, error) =>
269+
const SizedBox(),
270+
)
271+
: Image.memory(
272+
_avatar!,
273+
fit: BoxFit.cover,
274+
),
275+
)
276+
: const Padding(
277+
padding: EdgeInsets.all(16.0),
278+
),
279+
),
280+
),
281+
const SizedBox(height: 16.0),
282+
],
283+
),
284+
InkWell(
285+
borderRadius: BorderRadius.circular(90),
286+
onTap: _isLoading ? null : selectPhoto,
287+
child: const CircleAvatar(
288+
radius: 32.0,
289+
child: Icon(Icons.add_a_photo_outlined),
290+
),
291+
),
292+
],
293+
),
294+
);
295+
}
296+
297+
index--;
298+
157299
return ActivityPlanCard(
158300
activity: widget.activityPlanRequest == null
159301
? _bookmarkedActivities[index]

lib/pangea/activity_planner/activity_plan_message.dart

+29-24
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,13 @@ class ActivityPlanMessage extends StatelessWidget {
130130
AppConfig.borderRadius,
131131
),
132132
),
133-
padding: const EdgeInsets.symmetric(
134-
horizontal: 16,
135-
vertical: 8,
136-
),
133+
padding:
134+
event.messageType == MessageTypes.Image
135+
? EdgeInsets.zero
136+
: const EdgeInsets.symmetric(
137+
horizontal: 16,
138+
vertical: 8,
139+
),
137140
constraints: const BoxConstraints(
138141
maxWidth: FluffyThemes.columnWidth * 1.5,
139142
),
@@ -218,32 +221,34 @@ class ActivityPlanMessage extends StatelessWidget {
218221
mainAxisSize: MainAxisSize.min,
219222
crossAxisAlignment: CrossAxisAlignment.start,
220223
children: <Widget>[
221-
Padding(
222-
padding: const EdgeInsets.symmetric(vertical: 8.0),
223-
child: Center(
224-
child: Padding(
225-
padding: const EdgeInsets.only(top: 4.0),
226-
child: Material(
227-
borderRadius: BorderRadius.circular(AppConfig.borderRadius * 2),
228-
color: theme.colorScheme.surface.withAlpha(128),
229-
child: Padding(
230-
padding: const EdgeInsets.symmetric(
231-
horizontal: 8.0,
232-
vertical: 2.0,
233-
),
234-
child: Text(
235-
event.originServerTs.localizedTime(context),
236-
style: TextStyle(
237-
fontSize: 12 * AppConfig.fontSizeFactor,
238-
fontWeight: FontWeight.bold,
239-
color: theme.colorScheme.secondary,
224+
if (event.messageType == MessageTypes.Text)
225+
Padding(
226+
padding: const EdgeInsets.symmetric(vertical: 8.0),
227+
child: Center(
228+
child: Padding(
229+
padding: const EdgeInsets.only(top: 4.0),
230+
child: Material(
231+
borderRadius:
232+
BorderRadius.circular(AppConfig.borderRadius * 2),
233+
color: theme.colorScheme.surface.withAlpha(128),
234+
child: Padding(
235+
padding: const EdgeInsets.symmetric(
236+
horizontal: 8.0,
237+
vertical: 2.0,
238+
),
239+
child: Text(
240+
event.originServerTs.localizedTime(context),
241+
style: TextStyle(
242+
fontSize: 12 * AppConfig.fontSizeFactor,
243+
fontWeight: FontWeight.bold,
244+
color: theme.colorScheme.secondary,
245+
),
240246
),
241247
),
242248
),
243249
),
244250
),
245251
),
246-
),
247252
row,
248253
],
249254
);

lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class ActivityPlanPageLaunchIconButton extends StatelessWidget {
1616

1717
@override
1818
Widget build(BuildContext context) {
19+
if (!controller.room.canSendDefaultStates) {
20+
return const SizedBox();
21+
}
22+
1923
return FutureBuilder<bool>(
2024
future: controller.room.isBotDM,
2125
builder: (BuildContext context, snapshot) {

lib/pangea/activity_planner/activity_planner_page.dart

+4-3
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
9090
Future<List<ActivitySettingResponseSchema>> get _topicItems =>
9191
TopicListRepo.get(req);
9292

93-
Future<List<ActivitySettingResponseSchema>> get _modeItems =>
93+
Future<List<ActivitySettingResponseSchema>> get modeItems =>
9494
ActivityModeListRepo.get(req);
9595

9696
Future<List<ActivitySettingResponseSchema>> get _objectiveItems =>
@@ -112,7 +112,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
112112
}
113113

114114
Future<String> _randomMode() async {
115-
final modes = await _modeItems;
115+
final modes = await modeItems;
116116
return (modes..shuffle()).first.name;
117117
}
118118

@@ -197,6 +197,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
197197
cefrLevel: _selectedCefrLevel!,
198198
numberOfParticipants: _selectedNumberOfParticipants!,
199199
),
200+
controller: this,
200201
)
201202
: Center(
202203
child: ConstrainedBox(
@@ -233,7 +234,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
233234
),
234235
const SizedBox(height: 24),
235236
SuggestionFormField(
236-
suggestions: _modeItems,
237+
suggestions: modeItems,
237238
validator: _validateNotNull,
238239
label: l10n.modeLabel,
239240
placeholder: l10n.modePlaceholder,
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
class ActivitySuggestionsConstants {
22
static const String plusIconPath = "add_icon.svg";
3+
static const String modeImageFileStart = "activityplanner_mode_";
34
}

lib/pangea/analytics_summary/progress_indicators_enum.dart

-13
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import 'package:flutter/material.dart';
33
import 'package:flutter_gen/gen_l10n/l10n.dart';
44
import 'package:material_symbols_icons/symbols.dart';
55

6-
import 'package:fluffychat/config/app_config.dart';
7-
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
86
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
97

108
enum ProgressIndicatorEnum {
@@ -25,17 +23,6 @@ extension ProgressIndicatorsExtension on ProgressIndicatorEnum {
2523
}
2624
}
2725

28-
String? get iconURL {
29-
switch (this) {
30-
case ProgressIndicatorEnum.wordsUsed:
31-
return '${AppConfig.assetsBaseURL}/${AnalyticsConstants.vocabIconFileName}';
32-
case ProgressIndicatorEnum.morphsUsed:
33-
return '${AppConfig.assetsBaseURL}/${AnalyticsConstants.morphIconFileName}';
34-
case ProgressIndicatorEnum.level:
35-
return null;
36-
}
37-
}
38-
3926
static bool isDarkMode(BuildContext context) =>
4027
Theme.of(context).brightness == Brightness.dark;
4128

0 commit comments

Comments
 (0)