mirror of
https://github.com/flutter/samples.git
synced 2026-04-07 04:02:13 +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:
39
ai_recipe_generation/lib/util/device_info.dart
Normal file
39
ai_recipe_generation/lib/util/device_info.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class DeviceInfo {
|
||||
static Future<BaseDeviceInfo> initialize(DeviceInfoPlugin plugin) async {
|
||||
if (kIsWeb) {
|
||||
return await plugin.webBrowserInfo;
|
||||
}
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return await plugin.androidInfo;
|
||||
case TargetPlatform.iOS:
|
||||
return await plugin.iosInfo;
|
||||
case TargetPlatform.macOS:
|
||||
return plugin.macOsInfo;
|
||||
case TargetPlatform.windows:
|
||||
return await plugin.windowsInfo;
|
||||
case TargetPlatform.linux:
|
||||
return await plugin.linuxInfo;
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'Device info not supported for this platform',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static bool isPhysicalDeviceWithCamera(BaseDeviceInfo deviceInfo) {
|
||||
if (deviceInfo is! IosDeviceInfo && deviceInfo is! AndroidDeviceInfo) {
|
||||
return false;
|
||||
}
|
||||
if (deviceInfo is IosDeviceInfo && deviceInfo.isPhysicalDevice) {
|
||||
return true;
|
||||
}
|
||||
if (deviceInfo is AndroidDeviceInfo && deviceInfo.isPhysicalDevice) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
13
ai_recipe_generation/lib/util/extensions.dart
Normal file
13
ai_recipe_generation/lib/util/extensions.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
extension SliverBreakpointUtils on SliverConstraints {
|
||||
bool get isTablet => crossAxisExtent > 730 && crossAxisExtent < 1000;
|
||||
bool get isDesktop => crossAxisExtent > 1000;
|
||||
bool get isMobile => crossAxisExtent < 730;
|
||||
}
|
||||
|
||||
extension BoxBreakpointUtils on BoxConstraints {
|
||||
bool get isTablet => maxWidth > 730 && maxWidth < 1000;
|
||||
bool get isDesktop => maxWidth > 1000;
|
||||
bool get isMobile => maxWidth < 730;
|
||||
}
|
||||
66
ai_recipe_generation/lib/util/filter_chip_enums.dart
Normal file
66
ai_recipe_generation/lib/util/filter_chip_enums.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
enum CuisineFilter {
|
||||
italian,
|
||||
mexican,
|
||||
american,
|
||||
french,
|
||||
japanese,
|
||||
chinese,
|
||||
indian,
|
||||
greek,
|
||||
moroccan,
|
||||
ethiopian,
|
||||
southAfrican,
|
||||
}
|
||||
|
||||
enum BasicIngredientsFilter {
|
||||
oil,
|
||||
butter,
|
||||
flour,
|
||||
salt,
|
||||
pepper,
|
||||
sugar,
|
||||
milk,
|
||||
vinegar,
|
||||
}
|
||||
|
||||
enum DietaryRestrictionsFilter {
|
||||
vegan,
|
||||
vegetarian,
|
||||
lactoseIntolerant,
|
||||
kosher,
|
||||
// keto,
|
||||
wheatAllergies,
|
||||
nutAllergies,
|
||||
fishAllergies,
|
||||
soyAllergies,
|
||||
}
|
||||
|
||||
String dietaryRestrictionReadable(DietaryRestrictionsFilter filter) {
|
||||
return switch (filter) {
|
||||
DietaryRestrictionsFilter.vegan => 'vegan',
|
||||
DietaryRestrictionsFilter.vegetarian => 'vegetarian',
|
||||
DietaryRestrictionsFilter.lactoseIntolerant => 'dairy free',
|
||||
DietaryRestrictionsFilter.kosher => 'kosher',
|
||||
// DietaryRestrictionsFilter.keto => 'low carb',
|
||||
DietaryRestrictionsFilter.wheatAllergies => 'wheat allergy',
|
||||
DietaryRestrictionsFilter.nutAllergies => 'nut allergy',
|
||||
DietaryRestrictionsFilter.fishAllergies => 'fish allergy',
|
||||
DietaryRestrictionsFilter.soyAllergies => 'soy allergy',
|
||||
};
|
||||
}
|
||||
|
||||
String cuisineReadable(CuisineFilter filter) {
|
||||
return switch (filter) {
|
||||
CuisineFilter.italian => 'Italian',
|
||||
CuisineFilter.mexican => 'Mexican',
|
||||
CuisineFilter.american => 'American',
|
||||
CuisineFilter.french => 'French',
|
||||
CuisineFilter.japanese => 'Japanese',
|
||||
CuisineFilter.chinese => 'Chinese',
|
||||
CuisineFilter.indian => 'Indian',
|
||||
CuisineFilter.ethiopian => 'Ethiopian',
|
||||
CuisineFilter.moroccan => 'Moroccan',
|
||||
CuisineFilter.greek => 'Greek',
|
||||
CuisineFilter.southAfrican => 'South African',
|
||||
};
|
||||
}
|
||||
8
ai_recipe_generation/lib/util/json_parsing.dart
Normal file
8
ai_recipe_generation/lib/util/json_parsing.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
String cleanJson(String maybeInvalidJson) {
|
||||
if (maybeInvalidJson.contains('```')) {
|
||||
final withoutLeading = maybeInvalidJson.split('```json').last;
|
||||
final withoutTrailing = withoutLeading.split('```').first;
|
||||
return withoutTrailing;
|
||||
}
|
||||
return maybeInvalidJson;
|
||||
}
|
||||
120
ai_recipe_generation/lib/util/tap_recorder.dart
Normal file
120
ai_recipe_generation/lib/util/tap_recorder.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// From: https://gist.github.com/creativecreatorormaybenot/cd42b60cb33c9962b19f629ec638d4de
|
||||
/// This is code that I (https://twitter.com/creativemaybeno) wrote for a
|
||||
/// StackOverflow answer.
|
||||
/// You can find it here: https://stackoverflow.com/a/65067655/6509751.
|
||||
/// List of the taps recorded by [TapRecorder].
|
||||
///
|
||||
/// This is only a make-shift solution of course. This will only be viable
|
||||
/// when using a single [TapRecorder] because it is saved as a top-level
|
||||
/// variable.
|
||||
@visibleForTesting
|
||||
final recordedTaps = <Offset>[];
|
||||
|
||||
/// These are the parameters for the visualization of the recorded taps.
|
||||
const _tapRadius = 15.0,
|
||||
_tapDuration = Duration(milliseconds: 420),
|
||||
_tapColor = Colors.white,
|
||||
_shadowColor = Colors.black,
|
||||
_shadowElevation = 2.0;
|
||||
|
||||
/// Widget that records any taps that hit its child.
|
||||
///
|
||||
/// It does not matter to this widget whether the child accepts the hit events.
|
||||
/// Everything hitting the rect of the child will be recorded.
|
||||
///
|
||||
/// It will both visualize them and add them to [recordedTaps].
|
||||
class TapRecorder extends SingleChildRenderObjectWidget {
|
||||
const TapRecorder({super.key, required Widget child}) : super(child: child);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderTapRecorder();
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderTapRecorder extends RenderProxyBox with _SilentTickerProvider {
|
||||
final _recordedTaps = <_RecordedTap>[];
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
for (final recordedTap in _recordedTaps) {
|
||||
(recordedTap.animation as AnimationController).dispose();
|
||||
}
|
||||
_recordedTaps.clear();
|
||||
super.detach();
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
if (!size.contains(position)) return false;
|
||||
// We always want to add a hit test entry for ourselves as we want to react
|
||||
// to each and every hit event.
|
||||
result.add(BoxHitTestEntry(this, position));
|
||||
return hitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
|
||||
// We do not want to interfere in the gesture arena, which is why we are not
|
||||
// using regular tap recognizers. Instead, we handle it ourselves and always
|
||||
// react to the hit events (ignoring the gesture arena).
|
||||
if (event is PointerDownEvent) {
|
||||
// Records the global position.
|
||||
recordedTaps.add(event.position);
|
||||
|
||||
final controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: _tapDuration,
|
||||
),
|
||||
recordedTap = _RecordedTap(event.localPosition, controller);
|
||||
_recordedTaps.add(recordedTap);
|
||||
|
||||
controller
|
||||
..addListener(markNeedsPaint)
|
||||
..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
controller.dispose();
|
||||
_recordedTaps.remove(recordedTap);
|
||||
}
|
||||
})
|
||||
..forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
context.paintChild(child!, offset);
|
||||
|
||||
final canvas = context.canvas;
|
||||
for (final tap in _recordedTaps) {
|
||||
final path = Path()
|
||||
..addOval(
|
||||
Rect.fromCircle(center: tap.localPosition, radius: _tapRadius));
|
||||
final opacity = 1 - tap.animation.value;
|
||||
|
||||
canvas.drawShadow(
|
||||
path, _shadowColor.withOpacity(opacity), _shadowElevation, true);
|
||||
canvas.drawPath(path, Paint()..color = _tapColor.withOpacity(opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordedTap {
|
||||
_RecordedTap(this.localPosition, this.animation);
|
||||
|
||||
final Offset localPosition;
|
||||
final Animation<double> animation;
|
||||
}
|
||||
|
||||
/// Ticker provider that does not perform any diagnostics.
|
||||
///
|
||||
/// We trust that the [_RenderTapRecorder] instance will dispose all tickers
|
||||
/// by disposing the animation controllers.
|
||||
mixin _SilentTickerProvider implements TickerProvider {
|
||||
@override
|
||||
Ticker createTicker(TickerCallback onTick) => Ticker(onTick);
|
||||
}
|
||||
Reference in New Issue
Block a user