feat: public spaces on homepage (#2318)
This commit is contained in:
parent
f1106e0aa8
commit
10c41c7112
9 changed files with 668 additions and 318 deletions
|
|
@ -4855,5 +4855,6 @@
|
|||
"goodJobTranslation": "Good work on this translation.",
|
||||
"makingProgress": "You're making progress!",
|
||||
"keepPracticing": "Keep practicing!",
|
||||
"niceJob": "Nice job!"
|
||||
"niceJob": "Nice job!",
|
||||
"publicSpacesTitle": "Learning communities"
|
||||
}
|
||||
|
|
@ -45,7 +45,6 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
|||
child: SingleChildScrollView(
|
||||
child: ActivitySuggestionsArea(
|
||||
scrollDirection: Axis.vertical,
|
||||
showCreateChatCard: false,
|
||||
room: room,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
|
|||
bool get _isColumnMode => FluffyThemes.isColumnMode(context);
|
||||
|
||||
double get cardHeight => _isColumnMode ? 315.0 : 240.0;
|
||||
double get cardPadding => _isColumnMode ? 8.0 : 0.0;
|
||||
double get cardWidth => _isColumnMode ? 225.0 : 150.0;
|
||||
|
||||
Future<void> _onEdit(
|
||||
|
|
@ -98,7 +97,6 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
|
|||
},
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
padding: cardPadding,
|
||||
onChange: () => setState(() {}),
|
||||
);
|
||||
}).toList(),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ class ActivitySuggestionCard extends StatelessWidget {
|
|||
|
||||
final double width;
|
||||
final double height;
|
||||
final double padding;
|
||||
final bool selected;
|
||||
|
||||
final VoidCallback onChange;
|
||||
|
|
@ -27,7 +26,6 @@ class ActivitySuggestionCard extends StatelessWidget {
|
|||
required this.onPressed,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.padding,
|
||||
required this.onChange,
|
||||
this.selected = false,
|
||||
this.image,
|
||||
|
|
@ -38,175 +36,171 @@ class ActivitySuggestionCard extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
final isBookmarked = BookmarkedActivitiesRepo.isBookmarked(activity);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(padding),
|
||||
child: PressableButton(
|
||||
depressed: selected || onPressed == null,
|
||||
onPressed: onPressed,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
color: theme.brightness == Brightness.dark
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.surfaceContainerHighest,
|
||||
colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: selected
|
||||
? Border.all(
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
height: height,
|
||||
width: width,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
return PressableButton(
|
||||
depressed: selected || onPressed == null,
|
||||
onPressed: onPressed,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
color: theme.brightness == Brightness.dark
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.surfaceContainerHighest,
|
||||
colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: selected
|
||||
? Border.all(
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
height: height,
|
||||
width: width,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: width - 16.0,
|
||||
width: width - 16.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 8.0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
child: image != null
|
||||
? Image.memory(image!)
|
||||
: activity.imageURL != null
|
||||
? activity.imageURL!.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(activity.imageURL!),
|
||||
width: width - 16.0,
|
||||
height: width - 16.0,
|
||||
cacheKey: activity.bookmarkId,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activity.imageURL!,
|
||||
placeholder: (context, url) =>
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: width - 16.0,
|
||||
width: width - 16.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
margin: const EdgeInsets.only(top: 8.0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
child: image != null
|
||||
? Image.memory(image!)
|
||||
: activity.imageURL != null
|
||||
? activity.imageURL!.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(activity.imageURL!),
|
||||
width: width - 16.0,
|
||||
height: width - 16.0,
|
||||
cacheKey: activity.bookmarkId,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activity.imageURL!,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
activity.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 12.0,
|
||||
),
|
||||
Text(
|
||||
"${activity.req.numberOfParticipants}",
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (activity.req.mode.isNotEmpty)
|
||||
Flexible(
|
||||
child: Text(
|
||||
activity.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 16.0,
|
||||
),
|
||||
Text(
|
||||
"${activity.req.numberOfParticipants}",
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (activity.req.mode.isNotEmpty)
|
||||
Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
activity.req.mode,
|
||||
style: theme.textTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
activity.req.mode,
|
||||
style: theme.textTheme.labelSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 4.0,
|
||||
right: 4.0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isBookmarked ? Icons.bookmark : Icons.bookmark_border,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: onPressed != null
|
||||
? () async {
|
||||
await (isBookmarked
|
||||
? BookmarkedActivitiesRepo.remove(
|
||||
activity.bookmarkId,
|
||||
)
|
||||
: BookmarkedActivitiesRepo.save(activity));
|
||||
onChange();
|
||||
}
|
||||
: null,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withAlpha(180),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 4.0,
|
||||
right: 4.0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isBookmarked ? Icons.bookmark : Icons.bookmark_border,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: onPressed != null
|
||||
? () async {
|
||||
await (isBookmarked
|
||||
? BookmarkedActivitiesRepo.remove(
|
||||
activity.bookmarkId,
|
||||
)
|
||||
: BookmarkedActivitiesRepo.save(activity));
|
||||
onChange();
|
||||
}
|
||||
: null,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withAlpha(180),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -236,7 +236,6 @@ class ActivitySuggestionCarouselState
|
|||
widget.enabled ? _onClickCard : null,
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
padding: 0.0,
|
||||
image: _currentActivity ==
|
||||
widget.selectedActivity
|
||||
? widget.selectedActivityImage
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
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';
|
||||
|
|
@ -14,22 +16,22 @@ 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';
|
||||
|
||||
class ActivitySuggestionsArea extends StatefulWidget {
|
||||
final Axis? scrollDirection;
|
||||
final bool showCreateChatCard;
|
||||
final bool showMakeActivityCard;
|
||||
final bool showTitle;
|
||||
|
||||
final Room? room;
|
||||
|
||||
const ActivitySuggestionsArea({
|
||||
super.key,
|
||||
this.scrollDirection,
|
||||
this.showCreateChatCard = true,
|
||||
this.showMakeActivityCard = true,
|
||||
this.showTitle = false,
|
||||
this.room,
|
||||
});
|
||||
@override
|
||||
|
|
@ -55,7 +57,6 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
final List<ActivityPlanModel> _activityItems = [];
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
double get cardHeight => _isColumnMode ? 315.0 : 240.0;
|
||||
double get cardPadding => _isColumnMode ? 8.0 : 0.0;
|
||||
double get cardWidth => _isColumnMode ? 225.0 : 150.0;
|
||||
|
||||
Future<void> _setActivityItems() async {
|
||||
|
|
@ -83,6 +84,8 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
final List<Widget> cards = _loading
|
||||
? List.generate(5, (i) {
|
||||
return Shimmer.fromColors(
|
||||
|
|
@ -91,7 +94,6 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
child: Container(
|
||||
height: cardHeight,
|
||||
width: cardWidth,
|
||||
margin: EdgeInsets.all(cardPadding),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
|
|
@ -117,7 +119,6 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
},
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
padding: cardPadding,
|
||||
onChange: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
|
|
@ -129,28 +130,141 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
final scrollDirection = widget.scrollDirection ??
|
||||
(_isColumnMode ? Axis.horizontal : Axis.vertical);
|
||||
|
||||
return scrollDirection == Axis.horizontal
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: cardHeight + 36.0),
|
||||
child: Scrollbar(
|
||||
controller: _scrollController,
|
||||
thumbVisibility: true,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
children: cards,
|
||||
return Column(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
if (widget.showTitle)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).startChat,
|
||||
style: isColumnMode
|
||||
? theme.textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold)
|
||||
: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceEvenly,
|
||||
runSpacing: 16.0,
|
||||
spacing: 4.0,
|
||||
children: cards,
|
||||
),
|
||||
);
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => context.go('/rooms/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('/rooms/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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
decoration: const BoxDecoration(),
|
||||
child: scrollDirection == Axis.horizontal
|
||||
? Expanded(
|
||||
child: Scrollbar(
|
||||
thumbVisibility: true,
|
||||
controller: _scrollController,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
children: cards,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceEvenly,
|
||||
runSpacing: 16.0,
|
||||
spacing: 4.0,
|
||||
children: cards,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,147 +1,33 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
|
||||
import 'package:fluffychat/pangea/public_spaces/public_spaces_area.dart';
|
||||
|
||||
class SuggestionsPage extends StatelessWidget {
|
||||
const SuggestionsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isColumnMode ? 36.0 : 4.0,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 24.0,
|
||||
children: [
|
||||
if (!isColumnMode)
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: isColumnMode ? 0 : 12.0),
|
||||
child: const LearningProgressIndicators(),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: isColumnMode ? 0.0 : 4.0,
|
||||
right: isColumnMode ? 0.0 : 4.0,
|
||||
top: 16.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).startChat,
|
||||
style: isColumnMode
|
||||
? theme.textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold)
|
||||
: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => context.go('/rooms/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('/rooms/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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!isColumnMode) const LearningProgressIndicators(),
|
||||
const ActivitySuggestionsArea(
|
||||
showTitle: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
const ActivitySuggestionsArea(),
|
||||
const PublicSpacesArea(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
142
lib/pangea/public_spaces/public_space_card.dart
Normal file
142
lib/pangea/public_spaces/public_space_card.dart
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class PublicSpaceCard extends StatelessWidget {
|
||||
final PublicRoomsChunk space;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const PublicSpaceCard({
|
||||
super.key,
|
||||
required this.space,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return PressableButton(
|
||||
onPressed: () {},
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
color: theme.brightness == Brightness.dark
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.surfaceContainerHighest,
|
||||
colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
height: height,
|
||||
width: width,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: height - 16.0,
|
||||
width: height - 16.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
child: space.avatarUrl != null
|
||||
? MxcImage(
|
||||
uri: space.avatarUrl!,
|
||||
width: width - 16.0,
|
||||
height: width - 16.0,
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
spacing: 4.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
space.name ?? '',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 12.0,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).countParticipants(
|
||||
space.numJoinedMembers,
|
||||
),
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (space.topic != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
space.topic!,
|
||||
style: theme.textTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
217
lib/pangea/public_spaces/public_spaces_area.dart
Normal file
217
lib/pangea/public_spaces/public_spaces_area.dart
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
// shows n rows of activity suggestions vertically, where n is the number of rows
|
||||
// as the user tries to scroll horizontally to the right, the client will fetch more activity suggestions
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/public_spaces/public_space_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PublicSpacesArea extends StatefulWidget {
|
||||
const PublicSpacesArea({super.key});
|
||||
|
||||
@override
|
||||
PublicSpacesAreaState createState() => PublicSpacesAreaState();
|
||||
}
|
||||
|
||||
class PublicSpacesAreaState extends State<PublicSpacesArea> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setSpaceItems();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
_coolDown?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _loading = true;
|
||||
bool _isSearching = false;
|
||||
|
||||
final List<PublicRoomsChunk> _spaceItems = [];
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
Timer? _coolDown;
|
||||
|
||||
final double cardHeight = 150.0;
|
||||
final double cardWidth = 450.0;
|
||||
|
||||
Future<void> _setSpaceItems() async {
|
||||
_spaceItems.clear();
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final resp = await Matrix.of(context).client.queryPublicRooms(
|
||||
filter: PublicRoomQueryFilter(
|
||||
roomTypes: ['m.space'],
|
||||
genericSearchTerm: _searchController.text,
|
||||
),
|
||||
limit: 100,
|
||||
);
|
||||
_spaceItems.addAll(resp.chunk);
|
||||
_spaceItems.sort((a, b) {
|
||||
int getPriority(item) {
|
||||
final bool hasTopic = item.topic != null && item.topic!.isNotEmpty;
|
||||
final bool hasAvatar = item.avatarUrl != null;
|
||||
|
||||
if (hasTopic && hasAvatar) return 0; // Highest priority
|
||||
if (hasAvatar) return 1; // Second priority
|
||||
if (hasTopic) return 2; // Third priority
|
||||
return 3; // Lowest priority
|
||||
}
|
||||
|
||||
return getPriority(a).compareTo(getPriority(b));
|
||||
});
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearchEnter(String text, {bool globalSearch = true}) {
|
||||
if (text.isEmpty) {
|
||||
_setSpaceItems();
|
||||
return;
|
||||
}
|
||||
|
||||
_coolDown?.cancel();
|
||||
_coolDown = Timer(const Duration(milliseconds: 500), _setSpaceItems);
|
||||
}
|
||||
|
||||
void _toggleSearching() {
|
||||
setState(() {
|
||||
_isSearching = !_isSearching;
|
||||
_searchController.clear();
|
||||
_setSpaceItems();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
final List<Widget> cards = _loading && _spaceItems.isEmpty
|
||||
? List.generate(5, (i) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: theme.colorScheme.primary.withAlpha(20),
|
||||
highlightColor: theme.colorScheme.primary.withAlpha(50),
|
||||
child: Container(
|
||||
height: cardHeight,
|
||||
width: cardWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
: _spaceItems
|
||||
.map((space) {
|
||||
return PublicSpaceCard(
|
||||
space: space,
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
);
|
||||
})
|
||||
.cast<Widget>()
|
||||
.toList();
|
||||
|
||||
if (_loading && _spaceItems.isNotEmpty) {
|
||||
cards.add(
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child: _isSearching
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
key: const ValueKey('search'),
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _searchController,
|
||||
onChanged: _onSearchEnter,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: _toggleSearching,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
key: const ValueKey('title'),
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).publicSpacesTitle,
|
||||
style: isColumnMode
|
||||
? theme.textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold)
|
||||
: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: _toggleSearching,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: const BoxDecoration(),
|
||||
child: Expanded(
|
||||
child: Scrollbar(
|
||||
thumbVisibility: true,
|
||||
controller: _scrollController,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
children: cards,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue