1
0
mirror of https://github.com/flutter/samples.git synced 2026-04-07 12:14:27 +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,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;
}
}

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

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

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

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