1
0
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:
Eric Windmill
2024-05-14 11:41:20 -04:00
committed by GitHub
parent 8575261d37
commit be52906894
171 changed files with 8626 additions and 0 deletions

View 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,
),
),
),
),
),
),
);
}
}

View 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;
}
}

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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(),
),
);
});
}
}

View File

@@ -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,
),
);
}
}

View 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,
),
),
);
}
}

View File

@@ -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,
);
},
),
),
);
}
}

View 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,
),
),
),
),
],
),
);
}
}

View 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,
),
),
)
],
);
}
}