chore: add images to activity planner page (#2169)

This commit is contained in:
ggurdin 2025-03-19 09:52:35 -04:00 committed by GitHub
parent 24621be6b2
commit 25b1c63df4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 182 additions and 42 deletions

View file

@ -3,17 +3,26 @@ import 'dart:developer';
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:http/http.dart' as http;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.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_plan_response.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_planner/list_request_schema.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.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/widgets/future_loading_dialog.dart';
import 'activity_plan_card.dart';
@ -23,10 +32,13 @@ class ActivityListView extends StatefulWidget {
/// if null, show saved activities
final ActivityPlanRequest? activityPlanRequest;
final ActivityPlannerPageState controller;
const ActivityListView({
super.key,
required this.room,
required this.activityPlanRequest,
required this.controller,
});
@override
@ -41,10 +53,15 @@ class ActivityListViewState extends State<ActivityListView> {
bool _isLoading = true;
Object? _error;
Uint8List? _avatar;
String? _avatarURL;
String? _filename;
@override
void initState() {
super.initState();
_loadActivities();
_setModeImageURL();
}
Future<void> _loadActivities() async {
@ -108,12 +125,79 @@ class ActivityListViewState extends State<ActivityListView> {
return;
}
await widget.room?.setPinnedEvents([eventId]);
Uint8List? bytes = _avatar;
if (_avatarURL != null && bytes == null) {
final resp = await http
.get(Uri.parse(_avatarURL!))
.timeout(const Duration(seconds: 5));
bytes = resp.bodyBytes;
}
if (bytes != null && _filename != null) {
final file = MatrixFile(
bytes: bytes,
name: _filename!,
);
await widget.room?.sendFileEvent(
file,
shrinkImageMaxDimension: 1600,
extraContent: {
ModelKey.messageTags: ModelKey.messageTagActivityPlan,
},
);
}
if (widget.room != null && widget.room!.canSendDefaultStates) {
await widget.room?.setPinnedEvents([eventId]);
}
Navigator.of(context).pop();
},
);
Future<ActivitySettingResponseSchema?> get _selectedMode async {
final modes = await widget.controller.modeItems;
return modes.firstWhereOrNull(
(element) =>
element.name.toLowerCase() ==
widget.activityPlanRequest?.mode.toLowerCase(),
);
}
Future<void> _setModeImageURL() async {
final mode = await _selectedMode;
if (mode == null) return;
final modeName =
mode.defaultName.toLowerCase().replaceAll(RegExp(r'\s+'), '');
final filename =
"${ActivitySuggestionsConstants.modeImageFileStart}$modeName.jpg";
if (!mounted) return;
setState(() {
_avatarURL = "${AppConfig.assetsBaseURL}/$filename";
_filename = filename;
});
}
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;
});
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
@ -152,8 +236,66 @@ class ActivityListViewState extends State<ActivityListView> {
padding: const EdgeInsets.all(16),
itemCount: widget.activityPlanRequest == null
? _bookmarkedActivities.length
: _activities!.length,
: _activities!.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return Center(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Column(
children: [
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
),
width: 400.0,
clipBehavior: Clip.hardEdge,
child: _avatarURL != null || _avatar != null
? ClipRRect(
child: _avatar == null
? CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: _avatarURL!,
placeholder: (context, url) {
return const Center(
child:
CircularProgressIndicator(),
);
},
errorWidget: (context, url, error) =>
const SizedBox(),
)
: Image.memory(
_avatar!,
fit: BoxFit.cover,
),
)
: const Padding(
padding: EdgeInsets.all(16.0),
),
),
),
const SizedBox(height: 16.0),
],
),
InkWell(
borderRadius: BorderRadius.circular(90),
onTap: _isLoading ? null : selectPhoto,
child: const CircleAvatar(
radius: 32.0,
child: Icon(Icons.add_a_photo_outlined),
),
),
],
),
);
}
index--;
return ActivityPlanCard(
activity: widget.activityPlanRequest == null
? _bookmarkedActivities[index]

View file

@ -130,10 +130,13 @@ class ActivityPlanMessage extends StatelessWidget {
AppConfig.borderRadius,
),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
padding:
event.messageType == MessageTypes.Image
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
@ -218,32 +221,34 @@ class ActivityPlanMessage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius * 2),
color: theme.colorScheme.surface.withAlpha(128),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 2.0,
),
child: Text(
event.originServerTs.localizedTime(context),
style: TextStyle(
fontSize: 12 * AppConfig.fontSizeFactor,
fontWeight: FontWeight.bold,
color: theme.colorScheme.secondary,
if (event.messageType == MessageTypes.Text)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius * 2),
color: theme.colorScheme.surface.withAlpha(128),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 2.0,
),
child: Text(
event.originServerTs.localizedTime(context),
style: TextStyle(
fontSize: 12 * AppConfig.fontSizeFactor,
fontWeight: FontWeight.bold,
color: theme.colorScheme.secondary,
),
),
),
),
),
),
),
),
row,
],
);

View file

@ -16,6 +16,10 @@ class ActivityPlanPageLaunchIconButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!controller.room.canSendDefaultStates) {
return const SizedBox();
}
return FutureBuilder<bool>(
future: controller.room.isBotDM,
builder: (BuildContext context, snapshot) {

View file

@ -90,7 +90,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
Future<List<ActivitySettingResponseSchema>> get _topicItems =>
TopicListRepo.get(req);
Future<List<ActivitySettingResponseSchema>> get _modeItems =>
Future<List<ActivitySettingResponseSchema>> get modeItems =>
ActivityModeListRepo.get(req);
Future<List<ActivitySettingResponseSchema>> get _objectiveItems =>
@ -112,7 +112,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
}
Future<String> _randomMode() async {
final modes = await _modeItems;
final modes = await modeItems;
return (modes..shuffle()).first.name;
}
@ -197,6 +197,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
cefrLevel: _selectedCefrLevel!,
numberOfParticipants: _selectedNumberOfParticipants!,
),
controller: this,
)
: Center(
child: ConstrainedBox(
@ -233,7 +234,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
),
const SizedBox(height: 24),
SuggestionFormField(
suggestions: _modeItems,
suggestions: modeItems,
validator: _validateNotNull,
label: l10n.modeLabel,
placeholder: l10n.modePlaceholder,

View file

@ -1,3 +1,4 @@
class ActivitySuggestionsConstants {
static const String plusIconPath = "add_icon.svg";
static const String modeImageFileStart = "activityplanner_mode_";
}

View file

@ -3,8 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
enum ProgressIndicatorEnum {
@ -25,17 +23,6 @@ extension ProgressIndicatorsExtension on ProgressIndicatorEnum {
}
}
String? get iconURL {
switch (this) {
case ProgressIndicatorEnum.wordsUsed:
return '${AppConfig.assetsBaseURL}/${AnalyticsConstants.vocabIconFileName}';
case ProgressIndicatorEnum.morphsUsed:
return '${AppConfig.assetsBaseURL}/${AnalyticsConstants.morphIconFileName}';
case ProgressIndicatorEnum.level:
return null;
}
}
static bool isDarkMode(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark;