mirror of
https://github.com/flutter/samples.git
synced 2026-04-06 11:41:26 +00:00
Adds ai_recipe_generation sample (#2242)
Adding the demo app from my I/O talk. Because AI. ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. --------- Co-authored-by: Brett Morgan <brett.morgan@gmail.com>
This commit is contained in:
85
ai_recipe_generation/lib/widgets/add_image_widget.dart
Normal file
85
ai_recipe_generation/lib/widgets/add_image_widget.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../theme.dart';
|
||||
|
||||
class AddImage extends StatefulWidget {
|
||||
const AddImage({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
this.height = 100,
|
||||
this.width = 100,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final double height;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
State<AddImage> createState() => _AddImageState();
|
||||
}
|
||||
|
||||
class _AddImageState extends State<AddImage> {
|
||||
bool hovered = false;
|
||||
bool tappedDown = false;
|
||||
|
||||
Color get buttonColor {
|
||||
var state = (hovered, tappedDown);
|
||||
return switch (state) {
|
||||
// tapped down state
|
||||
(_, true) => MarketplaceTheme.secondary.withOpacity(.7),
|
||||
// hovered
|
||||
(true, _) => MarketplaceTheme.secondary.withOpacity(.3),
|
||||
// base color
|
||||
(_, _) => MarketplaceTheme.secondary.withOpacity(.3),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (event) {
|
||||
setState(() {
|
||||
hovered = true;
|
||||
});
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
hovered = false;
|
||||
});
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTapDown: (details) {
|
||||
setState(() {
|
||||
tappedDown = true;
|
||||
});
|
||||
},
|
||||
onTapUp: (details) {
|
||||
setState(() {
|
||||
tappedDown = false;
|
||||
});
|
||||
widget.onTap();
|
||||
},
|
||||
child: SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: buttonColor,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Symbols.add_photo_alternate_rounded,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
ai_recipe_generation/lib/widgets/appbar_shape_border.dart
Normal file
52
ai_recipe_generation/lib/widgets/appbar_shape_border.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppBarShapeBorder extends ShapeBorder {
|
||||
final double radius;
|
||||
|
||||
const AppBarShapeBorder(this.radius);
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
||||
return Path(); // Define inner path if needed
|
||||
}
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
||||
// Define your custom shape path here
|
||||
Path path = Path();
|
||||
path.moveTo(rect.left, rect.top);
|
||||
path.lineTo(rect.left, rect.bottom - (radius * 2));
|
||||
|
||||
path.quadraticBezierTo(
|
||||
rect.left,
|
||||
rect.bottom - radius,
|
||||
rect.left + radius,
|
||||
rect.bottom - radius,
|
||||
);
|
||||
|
||||
path.lineTo(rect.right - radius, rect.bottom - radius);
|
||||
|
||||
path.quadraticBezierTo(
|
||||
rect.right, rect.bottom - radius, rect.right, rect.bottom);
|
||||
|
||||
path.lineTo(rect.right, rect.top);
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
||||
// Define your painting logic here
|
||||
Paint paint = Paint()..color = Colors.transparent;
|
||||
canvas.drawPath(getOuterPath(rect), paint);
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder scale(double t) {
|
||||
// Implement scaling if needed
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomBarShapeBorder extends ShapeBorder {
|
||||
final double radius;
|
||||
|
||||
const BottomBarShapeBorder(this.radius);
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
||||
return Path(); // Define inner path if needed
|
||||
}
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
||||
// Define your custom shape path here
|
||||
Path path = Path();
|
||||
path.moveTo(rect.left, rect.top - radius);
|
||||
path.quadraticBezierTo(
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.left + radius,
|
||||
rect.top,
|
||||
);
|
||||
|
||||
path.lineTo(rect.right - radius, rect.top);
|
||||
path.quadraticBezierTo(
|
||||
rect.right,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom,
|
||||
);
|
||||
|
||||
path.lineTo(rect.left, rect.bottom);
|
||||
path.lineTo(rect.left, rect.top + radius);
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
||||
// Define your painting logic here
|
||||
Paint paint = Paint()..color = Colors.transparent;
|
||||
canvas.drawPath(getOuterPath(rect), paint);
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder scale(double t) {
|
||||
// Implement scaling if needed
|
||||
return this;
|
||||
}
|
||||
}
|
||||
41
ai_recipe_generation/lib/widgets/cross_image_widget.dart
Normal file
41
ai_recipe_generation/lib/widgets/cross_image_widget.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class CrossImage extends StatelessWidget {
|
||||
const CrossImage({
|
||||
super.key,
|
||||
required this.file,
|
||||
this.fit = BoxFit.cover,
|
||||
this.height = 100,
|
||||
this.width = 100,
|
||||
});
|
||||
|
||||
final XFile file;
|
||||
final BoxFit fit;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (kIsWeb) {
|
||||
return Image.network(
|
||||
file.path,
|
||||
fit: fit,
|
||||
);
|
||||
} else {
|
||||
return Image.file(
|
||||
File(file.path),
|
||||
height: height,
|
||||
width: width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static DecorationImage decoration(XFile file, {BoxFit fit = BoxFit.cover}) {
|
||||
final image = kIsWeb ? NetworkImage(file.path) : FileImage(File(file.path));
|
||||
return DecorationImage(image: image as ImageProvider, fit: fit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:ai_recipe_generation/theme.dart';
|
||||
import 'package:ai_recipe_generation/util/extensions.dart';
|
||||
import 'package:ai_recipe_generation/util/filter_chip_enums.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FilterChipSelectionInput<T extends Enum> extends StatefulWidget {
|
||||
const FilterChipSelectionInput({
|
||||
super.key,
|
||||
required this.onChipSelected,
|
||||
required this.selectedValues,
|
||||
required this.allValues,
|
||||
});
|
||||
|
||||
final Null Function(Set) onChipSelected;
|
||||
final Set<T> selectedValues;
|
||||
final List<T> allValues;
|
||||
|
||||
@override
|
||||
State<FilterChipSelectionInput> createState() =>
|
||||
_CategorySelectionInputState<T>();
|
||||
}
|
||||
|
||||
class _CategorySelectionInputState<T extends Enum>
|
||||
extends State<FilterChipSelectionInput> {
|
||||
bool isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: Wrap(
|
||||
spacing: 5.0,
|
||||
runSpacing: constraints.isMobile ? 5.0 : -5.0,
|
||||
children: List<Widget>.generate(
|
||||
widget.allValues.length,
|
||||
(idx) {
|
||||
final chipData = widget.allValues[idx];
|
||||
String label(dynamic chipData) {
|
||||
if (chipData is CuisineFilter) {
|
||||
return cuisineReadable(chipData);
|
||||
} else if (chipData is DietaryRestrictionsFilter) {
|
||||
return dietaryRestrictionReadable(chipData);
|
||||
} else if (chipData is BasicIngredientsFilter) {
|
||||
return chipData.name;
|
||||
} else {
|
||||
throw "unknown enum";
|
||||
}
|
||||
}
|
||||
|
||||
return FilterChip(
|
||||
color: WidgetStateColor.resolveWith((states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return MarketplaceTheme.secondary.withOpacity(.5);
|
||||
}
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return MarketplaceTheme.secondary.withOpacity(.3);
|
||||
}
|
||||
return Theme.of(context).splashColor;
|
||||
}),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent,
|
||||
padding: const EdgeInsets.all(4),
|
||||
label: Text(
|
||||
label(chipData),
|
||||
style: MarketplaceTheme.dossierParagraph,
|
||||
),
|
||||
selected: widget.selectedValues.contains(chipData),
|
||||
onSelected: (selected) {
|
||||
setState(
|
||||
() {
|
||||
if (selected) {
|
||||
widget.selectedValues.add(chipData as T);
|
||||
} else {
|
||||
widget.selectedValues.remove(chipData);
|
||||
}
|
||||
widget.onChipSelected(widget.selectedValues);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme.dart';
|
||||
|
||||
class HighlightBorderOnHoverWidget extends StatefulWidget {
|
||||
const HighlightBorderOnHoverWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.color = MarketplaceTheme.secondary,
|
||||
required this.borderRadius,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Color color;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
@override
|
||||
State<HighlightBorderOnHoverWidget> createState() =>
|
||||
_HighlightBorderOnHoverWidgetState();
|
||||
}
|
||||
|
||||
class _HighlightBorderOnHoverWidgetState
|
||||
extends State<HighlightBorderOnHoverWidget> {
|
||||
bool hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (event) {
|
||||
setState(() {
|
||||
hovered = true;
|
||||
});
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
hovered = false;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).splashColor.withOpacity(.1),
|
||||
border: Border.all(
|
||||
color: hovered ? widget.color : MarketplaceTheme.borderColor,
|
||||
),
|
||||
borderRadius: widget.borderRadius,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
ai_recipe_generation/lib/widgets/icon_loading_indicator.dart
Normal file
98
ai_recipe_generation/lib/widgets/icon_loading_indicator.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:ai_recipe_generation/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IconLoadingAnimator extends StatefulWidget {
|
||||
IconLoadingAnimator({
|
||||
super.key,
|
||||
required this.icons,
|
||||
this.animationDuration,
|
||||
this.millisecondsBetweenAnimations,
|
||||
});
|
||||
|
||||
final List<IconData> icons;
|
||||
final Duration? animationDuration;
|
||||
final int? millisecondsBetweenAnimations;
|
||||
final List<Color> colors = [
|
||||
MarketplaceTheme.primary,
|
||||
MarketplaceTheme.secondary,
|
||||
MarketplaceTheme.tertiary,
|
||||
MarketplaceTheme.scrim,
|
||||
Colors.black87,
|
||||
];
|
||||
|
||||
@override
|
||||
State<IconLoadingAnimator> createState() => _IconLoadingAnimatorState();
|
||||
}
|
||||
|
||||
var rand = Random();
|
||||
|
||||
class _IconLoadingAnimatorState extends State<IconLoadingAnimator> {
|
||||
late List<IconData> notYetSeenIcons;
|
||||
late IconData currentIcon;
|
||||
late Color currentColor;
|
||||
late Timer timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notYetSeenIcons = widget.icons;
|
||||
|
||||
currentIcon =
|
||||
notYetSeenIcons.removeAt(rand.nextInt(notYetSeenIcons.length));
|
||||
currentColor = widget.colors[rand.nextInt(widget.colors.length)];
|
||||
|
||||
timer = Timer.periodic(
|
||||
Duration(milliseconds: widget.millisecondsBetweenAnimations ?? 1000),
|
||||
(timer) {
|
||||
nextIcon();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void nextIcon() {
|
||||
if (notYetSeenIcons.length == 1) notYetSeenIcons = widget.icons;
|
||||
setState(() {
|
||||
currentIcon =
|
||||
notYetSeenIcons.removeAt(rand.nextInt(notYetSeenIcons.length));
|
||||
currentColor = widget.colors[rand.nextInt(widget.colors.length)];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: MarketplaceTheme.focusedBorderColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: widget.animationDuration ?? const Duration(milliseconds: 200),
|
||||
transitionBuilder: (child, animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
size: 75,
|
||||
color: currentColor,
|
||||
key: Key(currentIcon.hashCode.toString()),
|
||||
currentIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme.dart';
|
||||
|
||||
class MarketplaceButton extends StatefulWidget {
|
||||
const MarketplaceButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.buttonText,
|
||||
required this.icon,
|
||||
this.iconRotateAngle,
|
||||
this.iconBackgroundColor,
|
||||
this.iconColor,
|
||||
this.buttonBackgroundColor,
|
||||
this.hoverColor,
|
||||
});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
final String buttonText;
|
||||
final IconData icon;
|
||||
final double? iconRotateAngle;
|
||||
final Color? iconBackgroundColor;
|
||||
final Color? iconColor;
|
||||
final Color? buttonBackgroundColor;
|
||||
final Color? hoverColor;
|
||||
|
||||
@override
|
||||
State<MarketplaceButton> createState() => _MarketplaceButtonState();
|
||||
}
|
||||
|
||||
class _MarketplaceButtonState extends State<MarketplaceButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton.icon(
|
||||
icon: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: widget.iconBackgroundColor ?? Colors.transparent,
|
||||
),
|
||||
child: Transform.rotate(
|
||||
angle: widget.iconRotateAngle ?? 0,
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: widget.iconColor ?? Colors.black87,
|
||||
size: 20.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
widget.buttonText,
|
||||
style: MarketplaceTheme.dossierParagraph,
|
||||
),
|
||||
onPressed: widget.onPressed,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateColor.resolveWith((states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return widget.hoverColor ??
|
||||
MarketplaceTheme.secondary.withOpacity(.3);
|
||||
}
|
||||
return widget.buttonBackgroundColor ??
|
||||
Theme.of(context).splashColor.withOpacity(.3);
|
||||
}),
|
||||
shape: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
// TODO: how can I animate between states?
|
||||
}
|
||||
return const RoundedRectangleBorder(
|
||||
side: BorderSide(color: Colors.black26),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
textStyle: WidgetStateTextStyle.resolveWith(
|
||||
(states) {
|
||||
return MarketplaceTheme.dossierParagraph.copyWith(
|
||||
color: Colors.black45,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
65
ai_recipe_generation/lib/widgets/prompt_image_widget.dart
Normal file
65
ai_recipe_generation/lib/widgets/prompt_image_widget.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:ai_recipe_generation/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'cross_image_widget.dart';
|
||||
|
||||
typedef OnTapRemoveImageCallback = void Function(XFile);
|
||||
|
||||
class PromptImage extends StatelessWidget {
|
||||
const PromptImage({
|
||||
super.key,
|
||||
required this.file,
|
||||
this.onTapIcon,
|
||||
this.width = 100,
|
||||
});
|
||||
|
||||
final XFile file;
|
||||
final VoidCallback? onTapIcon;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(
|
||||
MarketplaceTheme.defaultBorderRadius,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
foregroundDecoration: BoxDecoration(
|
||||
image: CrossImage.decoration(file),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onTapIcon != null)
|
||||
Positioned(
|
||||
right: 5,
|
||||
top: 5,
|
||||
child: GestureDetector(
|
||||
onTap: onTapIcon,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Symbols.remove,
|
||||
size: 16,
|
||||
color: Colors.red.shade400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
ai_recipe_generation/lib/widgets/star_rating.dart
Normal file
58
ai_recipe_generation/lib/widgets/star_rating.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
typedef StarRatingCallback = void Function(int);
|
||||
|
||||
class StartRating extends StatefulWidget {
|
||||
const StartRating({
|
||||
super.key,
|
||||
required this.starColor,
|
||||
required this.onTap,
|
||||
this.initialRating = -1,
|
||||
});
|
||||
|
||||
final Color starColor;
|
||||
final int initialRating;
|
||||
|
||||
/// If [onTap] is not null, the stars are interactive
|
||||
final StarRatingCallback? onTap;
|
||||
|
||||
@override
|
||||
State<StartRating> createState() => _StartRatingState();
|
||||
}
|
||||
|
||||
class _StartRatingState extends State<StartRating> {
|
||||
late int selectedIdx;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
selectedIdx = widget.initialRating - 1;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
5,
|
||||
(index) => GestureDetector(
|
||||
onTap: widget.onTap != null
|
||||
? () {
|
||||
setState(() {
|
||||
selectedIdx = index;
|
||||
});
|
||||
widget.onTap!(index);
|
||||
}
|
||||
: null,
|
||||
child: Icon(
|
||||
Symbols.kid_star,
|
||||
color: widget.starColor,
|
||||
fill: selectedIdx >= index ? 1 : 0,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user