import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../i18n/strings.g.dart'; import '../state/peer_rating_cubit.dart'; import 'theme.dart'; /// A compact "★ 4,5 (12)" pill, plus how many ratings come from people you /// know — the signal that makes strangers' reviews easy to weigh. class RatingStars extends StatelessWidget { const RatingStars({ required this.average, required this.count, this.circleCount = 0, super.key, }); final double average; final int count; final int circleCount; @override Widget build(BuildContext context) { final t = context.t; final locale = Localizations.localeOf(context).toString(); final avg = NumberFormat('0.#', locale).format(average); return Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 6, children: [ const Icon(Icons.star_rounded, size: 18, color: seedGreen), Text( '$avg ($count)', style: const TextStyle( color: seedOnSurface, fontSize: 13, fontWeight: FontWeight.w600, ), ), if (circleCount > 0) Text( t.ratings.fromYourCircle(n: circleCount), style: const TextStyle(color: seedGreen, fontSize: 13), ), ], ); } } /// Opens the bottom sheet to leave, edit or take back your rating of the /// peer behind [cubit]. Pre-filled when you already rated. Future showRatingSheet(BuildContext context, PeerRatingCubit cubit) { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: _RatingForm(cubit: cubit), ), ); } class _RatingForm extends StatefulWidget { const _RatingForm({required this.cubit}); final PeerRatingCubit cubit; @override State<_RatingForm> createState() => _RatingFormState(); } class _RatingFormState extends State<_RatingForm> { late int _stars; late final TextEditingController _comment; bool _saving = false; @override void initState() { super.initState(); final state = widget.cubit.state; _stars = state.myStars ?? 0; _comment = TextEditingController(text: state.myComment); } @override void dispose() { _comment.dispose(); super.dispose(); } Future _save() async { final messenger = ScaffoldMessenger.of(context); final navigator = Navigator.of(context); final saved = context.t.ratings.saved; setState(() => _saving = true); await widget.cubit.rate(_stars, comment: _comment.text.trim()); navigator.pop(); messenger.showSnackBar(SnackBar(content: Text(saved))); } Future _retract() async { final navigator = Navigator.of(context); setState(() => _saving = true); await widget.cubit.retract(); navigator.pop(); } @override Widget build(BuildContext context) { final t = context.t; final iRated = widget.cubit.state.iRated; return Padding( padding: const EdgeInsets.fromLTRB(24, 20, 24, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( iRated ? t.ratings.edit : t.ratings.rate, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: seedTitle, ), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (var s = 1; s <= 5; s++) IconButton( key: Key('rating.star.$s'), iconSize: 36, onPressed: _saving ? null : () => setState(() => _stars = s), icon: Icon( s <= _stars ? Icons.star_rounded : Icons.star_outline_rounded, color: s <= _stars ? seedGreen : seedMuted, ), ), ], ), const SizedBox(height: 8), TextField( key: const Key('rating.comment'), controller: _comment, enabled: !_saving, maxLength: 280, maxLines: 2, decoration: InputDecoration( hintText: t.ratings.commentHint, border: const OutlineInputBorder(), ), ), const SizedBox(height: 8), Row( children: [ if (iRated) TextButton( key: const Key('rating.retract'), onPressed: _saving ? null : _retract, child: Text(t.ratings.retract), ), const Spacer(), FilledButton( key: const Key('rating.save'), onPressed: _saving || _stars == 0 ? null : _save, child: Text(t.common.save), ), ], ), ], ), ); } }