pull in main

gemini-cli-actions
Eric Windmill 2 weeks ago
parent 4e3afd58e2
commit d3deaf4928

@ -1,86 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'local_veggie_provider.dart';
import 'veggie.dart';
class AppState extends ChangeNotifier {
final List<Veggie> _veggies;
AppState() : _veggies = LocalVeggieProvider.veggies;
List<Veggie> get allVeggies => List<Veggie>.from(_veggies);
List<Veggie> get availableVeggies {
var currentSeason = _getSeasonForDate(DateTime.now());
return _veggies
.where((v) => v.seasons.contains(currentSeason))
.toList();
}
List<Veggie> get favoriteVeggies =>
_veggies.where((v) => v.isFavorite).toList();
List<Veggie> get unavailableVeggies {
var currentSeason = _getSeasonForDate(DateTime.now());
return _veggies
.where((v) => !v.seasons.contains(currentSeason))
.toList();
}
Veggie getVeggie(int? id) => _veggies.singleWhere((v) => v.id == id);
List<Veggie> searchVeggies(String? terms) => _veggies
.where((v) => v.name.toLowerCase().contains(terms!.toLowerCase()))
.toList();
void setFavorite(int? id, bool isFavorite) {
var veggie = getVeggie(id);
veggie.isFavorite = isFavorite;
notifyListeners();
}
/// Used in tests to set the season independent of the current date.
static Season? debugCurrentSeason;
static Season? _getSeasonForDate(DateTime date) {
if (debugCurrentSeason != null) {
return debugCurrentSeason;
}
// Technically the start and end dates of seasons can vary by a day or so,
// but this is close enough for produce.
switch (date.month) {
case 1:
return Season.winter;
case 2:
return Season.winter;
case 3:
return date.day < 21 ? Season.winter : Season.spring;
case 4:
return Season.spring;
case 5:
return Season.spring;
case 6:
return date.day < 21 ? Season.spring : Season.summer;
case 7:
return Season.summer;
case 8:
return Season.summer;
case 9:
return date.day < 22 ? Season.autumn : Season.winter;
case 10:
return Season.autumn;
case 11:
return Season.autumn;
case 12:
return date.day < 22 ? Season.autumn : Season.winter;
default:
throw ArgumentError(
'Can\'t return a season for month #${date.month}.',
);
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,243 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' show Platform;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart'
show DeviceOrientation, SystemChrome;
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:window_size/window_size.dart';
import 'data/app_state.dart';
import 'data/preferences.dart';
import 'screens/details.dart';
import 'screens/favorites.dart';
import 'screens/home.dart';
import 'screens/list.dart';
import 'screens/search.dart';
import 'screens/settings.dart';
import 'styles.dart';
import 'widgets/veggie_seasons_page.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
setupWindow();
runApp(
const RootRestorationScope(restorationId: 'root', child: VeggieApp()),
);
}
const double windowWidth = 480;
const double windowHeight = 854;
void setupWindow() {
if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
setWindowTitle('Veggie Seasons');
setWindowMinSize(const Size(windowWidth, windowHeight));
setWindowMaxSize(const Size(windowWidth, windowHeight));
getCurrentScreen().then((screen) {
setWindowFrame(
Rect.fromCenter(
center: screen!.frame.center,
width: windowWidth,
height: windowHeight,
),
);
});
}
}
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
class VeggieApp extends StatefulWidget {
const VeggieApp({super.key});
@override
State<StatefulWidget> createState() => _VeggieAppState();
}
class _VeggieAppState extends State<VeggieApp> with RestorationMixin {
final _RestorableAppState _appState = _RestorableAppState();
@override
String get restorationId => 'wrapper';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_appState, 'state');
}
@override
void dispose() {
_appState.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: _appState.value),
ChangeNotifierProvider(create: (_) => Preferences()..load()),
],
child: CupertinoApp.router(
theme: Styles.veggieThemeData,
debugShowCheckedModeBanner: false,
restorationScopeId: 'app',
routerConfig: GoRouter(
navigatorKey: _rootNavigatorKey,
restorationScopeId: 'router',
initialLocation: '/list',
redirect: (context, state) {
if (state.path == '/') {
return '/list';
}
return null;
},
debugLogDiagnostics: true,
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
pageBuilder: (context, state, child) {
return CupertinoPage(
restorationId: 'router.shell',
child: HomeScreen(
restorationId: 'home',
child: child,
onTap: (index) {
if (index == 0) {
context.go('/list');
} else if (index == 1) {
context.go('/favorites');
} else if (index == 2) {
context.go('/search');
} else {
context.go('/settings');
}
},
),
);
},
routes: [
GoRoute(
path: '/list',
pageBuilder: (context, state) {
return VeggieSeasonsPage(
key: state.pageKey,
restorationId: 'route.list',
child: const ListScreen(restorationId: 'list'),
);
},
routes: [_buildDetailsRoute()],
),
GoRoute(
path: '/favorites',
pageBuilder: (context, state) {
return VeggieSeasonsPage(
key: state.pageKey,
restorationId: 'route.favorites',
child: const FavoritesScreen(
restorationId: 'favorites',
),
);
},
routes: [_buildDetailsRoute()],
),
GoRoute(
path: '/search',
pageBuilder: (context, state) {
return VeggieSeasonsPage(
key: state.pageKey,
restorationId: 'route.search',
child: const SearchScreen(restorationId: 'search'),
);
},
routes: [_buildDetailsRoute()],
),
GoRoute(
path: '/settings',
pageBuilder: (context, state) {
return VeggieSeasonsPage(
key: state.pageKey,
restorationId: 'route.settings',
child: const SettingsScreen(
restorationId: 'settings',
),
);
},
routes: [
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: 'categories',
pageBuilder: (context, state) {
return VeggieCategorySettingsScreen.pageBuilder(
context,
);
},
),
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: 'calories',
pageBuilder: (context, state) {
return CalorieSettingsScreen.pageBuilder(context);
},
),
],
),
],
),
],
),
),
);
}
// GoRouter does not support relative routes,
// see https://github.com/flutter/flutter/issues/108177
GoRoute _buildDetailsRoute() {
return GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: 'details/:id',
pageBuilder: (context, state) {
final veggieId = int.parse(state.pathParameters['id']!);
return CupertinoPage(
restorationId: 'route.details',
child: DetailsScreen(id: veggieId, restorationId: 'details'),
);
},
);
}
}
class _RestorableAppState extends RestorableListenable<AppState> {
@override
AppState createDefaultValue() {
return AppState();
}
@override
AppState fromPrimitives(Object? data) {
final appState = AppState();
final favorites = (data as List<dynamic>).cast<int>();
for (var id in favorites) {
appState.setFavorite(id, true);
}
return appState;
}
@override
Object toPrimitives() {
return value.favoriteVeggies.map((veggie) => veggie.id).toList();
}
}

@ -1,294 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../data/app_state.dart';
import '../data/preferences.dart';
import '../data/veggie.dart';
import '../styles.dart';
import '../widgets/detail_buttons.dart';
class ServingInfoChart extends StatelessWidget {
const ServingInfoChart(this.veggie, this.prefs, {super.key});
final Veggie veggie;
final Preferences prefs;
// Creates a [Text] widget to display a veggie's "percentage of your daily
// value of this vitamin" data adjusted for the user's preferred calorie
// target.
Widget _buildVitaminText(
int standardPercentage,
Future<int> targetCalories,
) {
return FutureBuilder<int>(
future: targetCalories,
builder: (context, snapshot) {
final target = snapshot.data ?? 2000;
final percent = standardPercentage * 2000 ~/ target;
return Text(
'$percent% DV',
style: CupertinoTheme.of(context).textTheme.textStyle,
textAlign: TextAlign.end,
);
},
);
}
@override
Widget build(BuildContext context) {
final themeData = CupertinoTheme.of(context);
return Column(
children: [
const SizedBox(height: 32),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
'Serving size',
style: Styles.detailsServingLabelText(themeData),
),
const Spacer(),
Text(
veggie.servingSize,
textAlign: TextAlign.end,
style: CupertinoTheme.of(context).textTheme.textStyle,
),
],
),
const SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
'Calories',
style: Styles.detailsServingLabelText(themeData),
),
const Spacer(),
Text(
'${veggie.caloriesPerServing} kCal',
style: CupertinoTheme.of(context).textTheme.textStyle,
textAlign: TextAlign.end,
),
],
),
const SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
'Vitamin A',
style: Styles.detailsServingLabelText(themeData),
),
const Spacer(),
_buildVitaminText(
veggie.vitaminAPercentage,
prefs.desiredCalories,
),
],
),
const SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
'Vitamin C',
style: Styles.detailsServingLabelText(themeData),
),
const Spacer(),
_buildVitaminText(
veggie.vitaminCPercentage,
prefs.desiredCalories,
),
],
),
Padding(
padding: const EdgeInsets.only(top: 32),
child: FutureBuilder(
future: prefs.desiredCalories,
builder: (context, snapshot) {
return Text(
'Percent daily values based on a diet of '
'${snapshot.data ?? '2,000'} calories.',
style: Styles.detailsServingNoteText(themeData),
);
},
),
),
],
);
}
}
class InfoView extends StatelessWidget {
final int? id;
const InfoView(this.id, {super.key});
@override
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);
final prefs = Provider.of<Preferences>(context);
final veggie = appState.getVeggie(id);
final themeData = CupertinoTheme.of(context);
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(veggie.name, style: Styles.detailsTitleText(themeData)),
const SizedBox(height: 8),
Text(
veggie.shortDescription,
style: CupertinoTheme.of(context).textTheme.textStyle,
),
const SizedBox(height: 16),
Text(
'Seasons',
style: Styles.detailsServingLabelText(themeData),
),
const SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
for (var season in Season.values) ...[
const Spacer(),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Styles.seasonIconData[season],
color: veggie.seasons.contains(season)
? Styles.seasonColors[season]
: const Color.fromRGBO(128, 128, 128, 1),
size: 24,
),
const SizedBox(height: 4),
Text(
season.name.characters.first.toUpperCase() +
season.name.characters.skip(1).string,
style: Styles.minorText(
CupertinoTheme.of(context),
).copyWith(fontSize: 11),
),
],
),
const Spacer(),
],
],
),
ServingInfoChart(veggie, prefs),
],
),
);
}
}
class DetailsScreen extends StatelessWidget {
final int? id;
final String? restorationId;
const DetailsScreen({this.id, this.restorationId, super.key});
Widget _buildHeader(BuildContext context, AppState model) {
final veggie = model.getVeggie(id);
return SizedBox(
height: 240,
child: Stack(
children: [
Positioned(
right: 0,
left: 0,
child: Image.asset(
veggie.imageAssetPath,
fit: BoxFit.cover,
semanticLabel: 'A background image of ${veggie.name}',
),
),
Positioned(
top: 16,
left: 16,
child: SafeArea(
child: CloseButton(() {
context.pop();
}),
),
),
Positioned(
top: 16,
right: 16,
child: SafeArea(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ShareButton(() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
return CupertinoActionSheet(
title: Text('Share ${veggie.name}'),
message: Text(veggie.shortDescription),
actions: [
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(context);
},
child: const Text('OK'),
),
],
);
},
);
}),
const SizedBox(width: 8),
Builder(
builder: (context) {
final appState = Provider.of<AppState>(context);
final veggie = appState.getVeggie(id);
return FavoriteButton(
() => appState.setFavorite(id, !veggie.isFavorite),
veggie.isFavorite,
);
},
),
],
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);
return CupertinoPageScaffold(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
restorationId: 'list',
children: [
_buildHeader(context, appState),
const SizedBox(height: 20),
InfoView(id),
],
),
),
],
),
);
}
}

@ -1,59 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import '../data/app_state.dart';
import '../data/veggie.dart';
import '../widgets/veggie_headline.dart';
class FavoritesScreen extends StatelessWidget {
const FavoritesScreen({this.restorationId, super.key});
final String? restorationId;
@override
Widget build(BuildContext context) {
return CupertinoTabView(
restorationScopeId: restorationId,
builder: (context) {
final model = Provider.of<AppState>(context);
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('My Garden'),
),
child: Center(
child: model.favoriteVeggies.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
'You haven\'t added any favorite veggies to your garden yet.',
style: CupertinoTheme.of(
context,
).textTheme.textStyle,
),
)
: ListView(
restorationId: 'list',
children: [
const SizedBox(height: 24),
for (Veggie veggie in model.favoriteVeggies)
Padding(
padding: const EdgeInsets.fromLTRB(
16,
0,
16,
24,
),
child: VeggieHeadline(veggie),
),
],
),
),
);
},
);
}
}

@ -1,81 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
const _bottomNavigationBarItemIconPadding = EdgeInsets.only(top: 4.0);
class HomeScreen extends StatelessWidget {
const HomeScreen({
super.key,
this.restorationId,
required this.child,
required this.onTap,
});
final String? restorationId;
final Widget child;
final void Function(int) onTap;
@override
Widget build(BuildContext context) {
final String location = GoRouter.of(
context,
).routerDelegate.currentConfiguration.uri.toString();
final index = _getSelectedIndex(location);
return RestorationScope(
restorationId: restorationId,
child: CupertinoPageScaffold(
child: Column(
children: [
Expanded(child: child),
CupertinoTabBar(
currentIndex: index,
items: const [
BottomNavigationBarItem(
icon: Padding(
padding: _bottomNavigationBarItemIconPadding,
child: Icon(CupertinoIcons.home),
),
label: 'Home',
),
BottomNavigationBarItem(
icon: Padding(
padding: _bottomNavigationBarItemIconPadding,
child: Icon(CupertinoIcons.book),
),
label: 'My Garden',
),
BottomNavigationBarItem(
icon: Padding(
padding: _bottomNavigationBarItemIconPadding,
child: Icon(CupertinoIcons.search),
),
label: 'Search',
),
BottomNavigationBarItem(
icon: Padding(
padding: _bottomNavigationBarItemIconPadding,
child: Icon(CupertinoIcons.settings),
),
label: 'Settings',
),
],
onTap: onTap,
),
],
),
),
);
}
int _getSelectedIndex(String location) {
if (location.startsWith('/list')) return 0;
if (location.startsWith('/favorites')) return 1;
if (location.startsWith('/search')) return 2;
if (location.startsWith('/settings')) return 3;
return 0;
}
}

@ -1,95 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../data/app_state.dart';
import '../data/preferences.dart';
import '../data/veggie.dart';
import '../styles.dart';
import '../widgets/veggie_card.dart';
class ListScreen extends StatelessWidget {
const ListScreen({this.restorationId, super.key});
final String? restorationId;
Widget _generateVeggieCard(
Veggie veggie,
Preferences prefs, {
bool inSeason = true,
}) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24),
child: FutureBuilder<Set<VeggieCategory>>(
future: prefs.preferredCategories,
builder: (context, snapshot) {
final data = snapshot.data ?? <VeggieCategory>{};
return VeggieCard(
veggie,
inSeason,
data.contains(veggie.category),
);
},
),
);
}
@override
Widget build(BuildContext context) {
return CupertinoTabView(
restorationScopeId: restorationId,
builder: (context) {
final appState = Provider.of<AppState>(context);
final prefs = Provider.of<Preferences>(context);
final themeData = CupertinoTheme.of(context);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarBrightness: MediaQuery.platformBrightnessOf(context),
),
child: SafeArea(
bottom: false,
child: ListView.builder(
restorationId: 'list',
itemCount: appState.allVeggies.length + 2,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
child: Text(
'In season today',
style: Styles.headlineText(themeData),
),
);
} else if (index <= appState.availableVeggies.length) {
return _generateVeggieCard(
appState.availableVeggies[index - 1],
prefs,
);
} else if (index <= appState.availableVeggies.length + 1) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
child: Text(
'Not in season',
style: Styles.headlineText(themeData),
),
);
} else {
var relativeIndex =
index - (appState.availableVeggies.length + 2);
return _generateVeggieCard(
appState.unavailableVeggies[relativeIndex],
prefs,
inSeason: false,
);
}
},
),
),
);
},
);
}
}

@ -1,132 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../data/app_state.dart';
import '../data/veggie.dart';
import '../widgets/veggie_headline.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({this.restorationId, super.key});
final String? restorationId;
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen>
with RestorationMixin {
final controller = RestorableTextEditingController();
final focusNode = FocusNode();
String? terms;
@override
String? get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(controller, 'text');
controller.addListener(_onTextChanged);
terms = controller.value.text;
}
@override
void dispose() {
focusNode.dispose();
controller.dispose();
super.dispose();
}
void _onTextChanged() {
setState(() => terms = controller.value.text);
}
Widget _createSearchBox({bool focus = true}) {
return Padding(
padding: const EdgeInsets.all(8),
child: CupertinoSearchTextField(
controller: controller.value,
focusNode: focus ? focusNode : null,
),
);
}
Widget _buildSearchResults(List<Veggie> veggies) {
if (veggies.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
'No veggies matching your search terms were found.',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
),
);
}
return ListView.builder(
restorationId: 'list',
itemCount: veggies.length + 1,
itemBuilder: (context, i) {
if (i == 0) {
return Visibility(
// This invisible and otherwise unnecessary search box is used to
// pad the list entries downward, so none will be underneath the
// real search box when the list is at its top scroll position.
visible: false,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
// This invisible and otherwise unnecessary search box is used to
// pad the list entries downward, so none will be underneath the
// real search box when the list is at its top scroll position.
child: _createSearchBox(focus: false),
);
} else {
return Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 24,
),
child: VeggieHeadline(veggies[i - 1]),
);
}
},
);
}
@override
Widget build(BuildContext context) {
final model = Provider.of<AppState>(context);
return UnmanagedRestorationScope(
bucket: bucket,
child: CupertinoTabView(
restorationScopeId: 'tabview',
builder: (context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarBrightness: MediaQuery.platformBrightnessOf(
context,
),
),
child: SafeArea(
bottom: false,
child: Stack(
children: [
_buildSearchResults(model.searchVeggies(terms)),
_createSearchBox(),
],
),
),
);
},
),
);
}
}

@ -1,311 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../data/preferences.dart';
import '../data/veggie.dart';
import '../styles.dart';
class VeggieCategorySettingsScreen extends StatelessWidget {
const VeggieCategorySettingsScreen({super.key, this.restorationId});
final String? restorationId;
static Page<void> pageBuilder(BuildContext context) {
return const CupertinoPage(
restorationId: 'router.categories',
child: VeggieCategorySettingsScreen(restorationId: 'category'),
title: 'Preferred Categories',
);
}
@override
Widget build(BuildContext context) {
final model = Provider.of<Preferences>(context);
final currentPrefs = model.preferredCategories;
var brightness = CupertinoTheme.brightnessOf(context);
return RestorationScope(
restorationId: restorationId,
child: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Preferred Categories'),
previousPageTitle: 'Settings',
),
backgroundColor: Styles.scaffoldBackground(brightness),
child: FutureBuilder<Set<VeggieCategory>>(
future: currentPrefs,
builder: (context, snapshot) {
final tiles = <CupertinoListTile>[];
for (final category in VeggieCategory.values) {
CupertinoSwitch toggle;
// It's possible that category data hasn't loaded from shared prefs
// yet, so display it if possible and fall back to disabled switches
// otherwise.
if (snapshot.hasData) {
toggle = CupertinoSwitch(
value: snapshot.data!.contains(category),
onChanged: (value) {
if (value) {
model.addPreferredCategory(category);
} else {
model.removePreferredCategory(category);
}
},
);
} else {
toggle = const CupertinoSwitch(
value: false,
onChanged: null,
);
}
tiles.add(
CupertinoListTile.notched(
title: Text(veggieCategoryNames[category]!),
trailing: toggle,
),
);
}
return ListView(
restorationId: 'list',
children: [
CupertinoListSection.insetGrouped(
hasLeading: false,
children: tiles,
),
],
);
},
),
),
);
}
}
class CalorieSettingsScreen extends StatelessWidget {
const CalorieSettingsScreen({super.key, this.restorationId});
final String? restorationId;
static const max = 1000;
static const min = 2600;
static const step = 200;
static Page<void> pageBuilder(BuildContext context) {
return const CupertinoPage<void>(
restorationId: 'router.calorie',
child: CalorieSettingsScreen(restorationId: 'calorie'),
title: 'Calorie Target',
);
}
@override
Widget build(BuildContext context) {
final model = Provider.of<Preferences>(context);
var brightness = CupertinoTheme.brightnessOf(context);
return RestorationScope(
restorationId: restorationId,
child: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
previousPageTitle: 'Settings',
),
backgroundColor: Styles.scaffoldBackground(brightness),
child: ListView(
restorationId: 'list',
children: [
FutureBuilder<int>(
future: model.desiredCalories,
builder: (context, snapshot) {
final tiles = <CupertinoListTile>[];
for (var cals = max; cals < min; cals += step) {
tiles.add(
CupertinoListTile.notched(
title: Text('$cals calories'),
trailing: SettingsIcon(
icon: CupertinoIcons.check_mark,
foregroundColor:
snapshot.hasData && snapshot.data == cals
? CupertinoColors.activeBlue
: Styles.transparentColor,
backgroundColor: Styles.transparentColor,
),
onTap: snapshot.hasData
? () => model.setDesiredCalories(cals)
: null,
),
);
}
return CupertinoListSection.insetGrouped(
header: Text(
'Available calorie levels'.toUpperCase(),
style: Styles.settingsGroupHeaderText(
CupertinoTheme.of(context),
),
),
footer: Text(
'These are used for serving calculations',
style: Styles.settingsGroupFooterText(
CupertinoTheme.of(context),
),
),
children: tiles,
);
},
),
],
),
),
);
}
}
class SettingsScreen extends StatefulWidget {
const SettingsScreen({this.restorationId, super.key});
final String? restorationId;
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
CupertinoListTile _buildCaloriesTile(
BuildContext context,
Preferences prefs,
) {
return CupertinoListTile.notched(
leading: const SettingsIcon(
backgroundColor: CupertinoColors.systemBlue,
icon: Styles.calorieIcon,
),
title: const Text('Calorie Target'),
additionalInfo: FutureBuilder<int>(
future: prefs.desiredCalories,
builder: (context, snapshot) {
return Text(
snapshot.data?.toString() ?? '',
style: CupertinoTheme.of(context).textTheme.textStyle,
);
},
),
trailing: const CupertinoListTileChevron(),
onTap: () => context.go('/settings/calories'),
);
}
CupertinoListTile _buildCategoriesTile(
BuildContext context,
Preferences prefs,
) {
return CupertinoListTile.notched(
leading: const SettingsIcon(
backgroundColor: CupertinoColors.systemOrange,
icon: Styles.preferenceIcon,
),
title: const Text('Preferred Categories'),
trailing: const CupertinoListTileChevron(),
onTap: () => context.go('/settings/categories'),
);
}
CupertinoListTile _buildRestoreDefaultsTile(
BuildContext context,
Preferences prefs,
) {
return CupertinoListTile.notched(
leading: const SettingsIcon(
backgroundColor: CupertinoColors.systemRed,
icon: Styles.resetIcon,
),
title: const Text('Restore Defaults'),
onTap: () {
showCupertinoDialog<void>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text('Are you sure?'),
content: const Text(
'Are you sure you want to reset the current settings?',
),
actions: [
CupertinoDialogAction(
isDestructiveAction: true,
child: const Text('Yes'),
onPressed: () async {
await prefs.restoreDefaults();
if (!context.mounted) return;
context.pop();
},
),
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('No'),
onPressed: () => context.pop(),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final prefs = Provider.of<Preferences>(context);
return CupertinoPageScaffold(
backgroundColor: Styles.scaffoldBackground(
CupertinoTheme.brightnessOf(context),
),
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(largeTitle: Text('Settings')),
SliverList(
delegate: SliverChildListDelegate([
CupertinoListSection.insetGrouped(
children: [
_buildCaloriesTile(context, prefs),
_buildCategoriesTile(context, prefs),
],
),
CupertinoListSection.insetGrouped(
children: [_buildRestoreDefaultsTile(context, prefs)],
),
]),
),
],
),
);
}
}
class SettingsIcon extends StatelessWidget {
const SettingsIcon({
required this.icon,
this.foregroundColor = CupertinoColors.white,
this.backgroundColor = CupertinoColors.black,
super.key,
});
final Color backgroundColor;
final Color foregroundColor;
final IconData icon;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: backgroundColor,
),
child: Center(child: Icon(icon, color: foregroundColor, size: 20)),
);
}
}

@ -1,221 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'data/veggie.dart';
abstract class Styles {
static CupertinoThemeData veggieThemeData = const CupertinoThemeData(
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(
color: CupertinoColors.label,
fontSize: 16,
fontWeight: FontWeight.normal,
fontStyle: FontStyle.normal,
fontFamily: 'CupertinoSystemText',
letterSpacing: -0.41,
decoration: TextDecoration.none,
),
),
);
static TextStyle headlineText(CupertinoThemeData themeData) => themeData
.textTheme
.textStyle
.copyWith(fontSize: 32, fontWeight: FontWeight.bold);
static TextStyle minorText(CupertinoThemeData themeData) => themeData
.textTheme
.textStyle
.copyWith(color: const Color.fromRGBO(128, 128, 128, 1));
static TextStyle headlineName(CupertinoThemeData themeData) => themeData
.textTheme
.textStyle
.copyWith(fontSize: 24, fontWeight: FontWeight.bold);
static TextStyle cardTitleText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(
color: const Color.fromRGBO(0, 0, 0, 0.9),
fontSize: 32,
fontWeight: FontWeight.bold,
);
static TextStyle cardCategoryText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(
color: const Color.fromRGBO(255, 255, 255, 0.9),
);
static TextStyle cardDescriptionText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(
color: const Color.fromRGBO(0, 0, 0, 0.9),
);
static TextStyle detailsTitleText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(
fontSize: 30,
fontWeight: FontWeight.bold,
);
static TextStyle detailsPreferredCategoryText(
CupertinoThemeData themeData,
) => themeData.textTheme.textStyle.copyWith(fontWeight: FontWeight.bold);
static TextStyle detailsBoldDescriptionText(
CupertinoThemeData themeData,
) => themeData.textTheme.textStyle.copyWith(
color: const Color.fromRGBO(0, 0, 0, 0.9),
fontWeight: FontWeight.bold,
);
static TextStyle detailsServingHeaderText(
CupertinoThemeData themeData,
) => themeData.textTheme.textStyle.copyWith(
color: const Color.fromRGBO(176, 176, 176, 1),
fontWeight: FontWeight.bold,
);
static TextStyle detailsServingLabelText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(fontWeight: FontWeight.bold);
static TextStyle detailsServingNoteText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(fontStyle: FontStyle.italic);
static TextStyle triviaFinishedTitleText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(fontSize: 32);
static TextStyle triviaFinishedBigText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(fontSize: 48);
static TextStyle settingsGroupHeaderText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(
color: CupertinoColors.inactiveGray,
fontSize: 13.5,
letterSpacing: -0.5,
);
static TextStyle settingsGroupFooterText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle.copyWith(
color: const Color(0xff777777),
fontSize: 13,
letterSpacing: -0.08,
);
static const appBackground = Color(0xffd0d0d0);
static Color? scaffoldBackground(Brightness brightness) =>
brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: null;
static const frostedBackground = Color(0xccf8f8f8);
static const closeButtonUnpressed = Color(0xff101010);
static const closeButtonPressed = Color(0xff808080);
static TextStyle settingsItemSubtitleText(
CupertinoThemeData themeData,
) => themeData.textTheme.textStyle.copyWith(
fontSize: 12,
letterSpacing: -0.2,
);
static const Color searchCursorColor = Color.fromRGBO(0, 122, 255, 1);
static const Color searchIconColor = Color.fromRGBO(128, 128, 128, 1);
static const seasonColors = <Season, Color>{
Season.winter: Color(0xff336dcc),
Season.spring: Color(0xff2fa02b),
Season.summer: Color(0xff287213),
Season.autumn: Color(0xff724913),
};
// While handy, some of the Font Awesome icons sometimes bleed over their
// allotted bounds. This padding is used to adjust for that.
static const seasonIconPadding = {
Season.winter: EdgeInsets.only(right: 0),
Season.spring: EdgeInsets.only(right: 4),
Season.summer: EdgeInsets.only(right: 6),
Season.autumn: EdgeInsets.only(right: 0),
};
static const seasonIconData = {
Season.winter: FontAwesomeIcons.snowflake,
Season.spring: FontAwesomeIcons.leaf,
Season.summer: FontAwesomeIcons.umbrellaBeach,
Season.autumn: FontAwesomeIcons.canadianMapleLeaf,
};
static const seasonBorder = Border(
top: BorderSide(color: Color(0xff606060)),
left: BorderSide(color: Color(0xff606060)),
bottom: BorderSide(color: Color(0xff606060)),
right: BorderSide(color: Color(0xff606060)),
);
static const uncheckedIcon = IconData(
0xf372,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const checkedIcon = IconData(
0xf373,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const transparentColor = Color(0x00000000);
static const shadowColor = Color(0xa0000000);
static const shadowGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [transparentColor, shadowColor],
);
static const Color settingsMediumGray = Color(0xffc7c7c7);
static const Color settingsItemPressed = Color(0xffd9d9d9);
static Color settingsItemColor(Brightness brightness) =>
brightness == Brightness.light
? CupertinoColors.tertiarySystemBackground
: CupertinoColors.darkBackgroundGray;
static Color settingsLineation(Brightness brightness) =>
brightness == Brightness.light
? const Color(0xffbcbbc1)
: const Color(0xff4c4b4b);
static const Color settingsBackground = Color(0xffefeff4);
static const preferenceIcon = IconData(
0xf443,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const resetIcon = IconData(
0xf4c4,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const calorieIcon = IconData(
0xf3bb,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const servingInfoBorderColor = Color(0xffb0b0b0);
static const ColorFilter desaturatedColorFilter =
// 222222 is a random color that has low color saturation.
ColorFilter.mode(Color(0xff222222), BlendMode.saturation);
}

@ -1,148 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import '../styles.dart';
/// Partially overlays and then blurs its child's background.
class FrostedBox extends StatelessWidget {
const FrostedBox({this.child, super.key});
final Widget? child;
@override
Widget build(BuildContext context) {
return BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: DecoratedBox(
decoration: const BoxDecoration(color: Styles.frostedBackground),
child: child,
),
);
}
}
/// An Icon that implicitly animates changes to its color.
class ColorChangingIcon extends ImplicitlyAnimatedWidget {
const ColorChangingIcon(
this.icon, {
this.color = CupertinoColors.black,
this.size,
required super.duration,
super.key,
});
final Color color;
final IconData icon;
final double? size;
@override
AnimatedWidgetBaseState<ColorChangingIcon> createState() =>
_ColorChangingIconState();
}
class _ColorChangingIconState
extends AnimatedWidgetBaseState<ColorChangingIcon> {
ColorTween? _colorTween;
@override
Widget build(BuildContext context) {
return Icon(
widget.icon,
semanticLabel: 'Close button',
size: widget.size,
color: _colorTween?.evaluate(animation),
);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_colorTween =
visitor(
_colorTween,
widget.color,
(dynamic value) => ColorTween(begin: value as Color?),
)
as ColorTween?;
}
}
/// A close button that invokes a callback when pressed.
class CloseButton extends _DetailPageButton {
const CloseButton(VoidCallback onPressed, {super.key})
: super(onPressed, CupertinoIcons.chevron_back);
}
/// A share button that invokes a callback when pressed.
class ShareButton extends _DetailPageButton {
const ShareButton(VoidCallback onPressed, {super.key})
: super(onPressed, CupertinoIcons.share);
}
/// A favorite button that invokes a callback when pressed.
class FavoriteButton extends _DetailPageButton {
const FavoriteButton(
VoidCallback onPressed,
bool isFavorite, {
super.key,
}) : super(
onPressed,
isFavorite ? CupertinoIcons.heart_fill : CupertinoIcons.heart,
);
}
class _DetailPageButton extends StatefulWidget {
const _DetailPageButton(this.onPressed, this.icon, {super.key});
final VoidCallback onPressed;
final IconData icon;
@override
State<_DetailPageButton> createState() => _DetailPageButtonState();
}
class _DetailPageButtonState extends State<_DetailPageButton> {
bool tapInProgress = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (details) {
setState(() => tapInProgress = true);
},
onTapUp: (details) {
setState(() => tapInProgress = false);
widget.onPressed();
},
onTapCancel: () {
setState(() => tapInProgress = false);
},
child: ClipOval(
child: FrostedBox(
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: ColorChangingIcon(
widget.icon,
duration: const Duration(milliseconds: 300),
color: tapInProgress
? Styles.closeButtonPressed
: Styles.closeButtonUnpressed,
size: 20,
),
),
),
),
),
);
}
}

@ -1,114 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../data/veggie.dart';
import '../styles.dart';
/// A Card-like Widget that responds to tap events by animating changes to its
/// elevation and invoking an optional [onPressed] callback.
class PressableCard extends StatelessWidget {
const PressableCard({
required this.child,
this.borderRadius = const BorderRadius.all(Radius.circular(16)),
this.onPressed,
super.key,
});
final VoidCallback? onPressed;
final Widget child;
final BorderRadius borderRadius;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: Container(
decoration: BoxDecoration(borderRadius: borderRadius),
child: ClipRRect(borderRadius: borderRadius, child: child),
),
);
}
}
class VeggieCard extends StatelessWidget {
const VeggieCard(
this.veggie,
this.isInSeason,
this.isPreferredCategory, {
super.key,
});
/// Veggie to be displayed by the card.
final Veggie veggie;
/// If the veggie is in season, it's displayed more prominently and the
/// image is fully saturated. Otherwise, it's reduced and de-saturated.
final bool isInSeason;
/// Whether [veggie] falls into one of user's preferred [VeggieCategory]s
final bool isPreferredCategory;
Widget _buildDetails(BuildContext context) {
final themeData = CupertinoTheme.of(context);
return Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 16, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(veggie.name, style: Styles.cardTitleText(themeData)),
const SizedBox(height: 8),
Text(
veggie.shortDescription,
style: Styles.cardDescriptionText(themeData),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return PressableCard(
onPressed: () {
// GoRouter does not support relative routes,
// so navigate to the absolute route.
// see https://github.com/flutter/flutter/issues/108177
context.go('/list/details/${veggie.id}');
},
child: Stack(
children: [
Semantics(
label: 'A card background featuring ${veggie.name}',
child: Container(
height: isInSeason ? 300 : 150,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
colorFilter: isInSeason
? null
: Styles.desaturatedColorFilter,
image: AssetImage(veggie.imageAssetPath),
),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _buildDetails(context),
),
],
),
);
}
}

@ -1,117 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import '../data/veggie.dart';
import '../styles.dart';
class ZoomClipAssetImage extends StatelessWidget {
const ZoomClipAssetImage({
required this.zoom,
this.height,
this.width,
required this.imageAsset,
super.key,
});
final double zoom;
final double? height;
final double? width;
final String imageAsset;
@override
Widget build(BuildContext context) {
return Container(
height: height,
width: width,
alignment: Alignment.center,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: OverflowBox(
maxHeight: height! * zoom,
maxWidth: width! * zoom,
child: Image.asset(imageAsset, fit: BoxFit.fill),
),
),
);
}
}
class VeggieHeadline extends StatelessWidget {
final Veggie veggie;
const VeggieHeadline(this.veggie, {super.key});
List<Widget> _buildSeasonDots(List<Season> seasons) {
var widgets = <Widget>[];
for (var season in seasons) {
widgets.add(const SizedBox(width: 4));
widgets.add(
Container(
height: 10,
width: 10,
decoration: BoxDecoration(
color: Styles.seasonColors[season],
borderRadius: BorderRadius.circular(5),
),
),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
final themeData = CupertinoTheme.of(context);
final String location = GoRouter.of(
context,
).routerDelegate.currentConfiguration.uri.toString();
return GestureDetector(
onTap: () {
// GoRouter does not support relative routes,
// so navigate to the absolute route, which can be either
// `/favorites/details/${veggie.id}` or `/search/details/${veggie.id}`
// see https://github.com/flutter/flutter/issues/108177
context.go('$location/details/${veggie.id}');
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ZoomClipAssetImage(
imageAsset: veggie.imageAssetPath,
zoom: 2.4,
height: 72,
width: 72,
),
const SizedBox(width: 8),
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
veggie.name,
style: Styles.headlineName(themeData),
),
..._buildSeasonDots(veggie.seasons),
],
),
Text(
veggie.shortDescription,
style: themeData.textTheme.textStyle,
),
],
),
),
],
),
);
}
}

@ -1,45 +0,0 @@
// Copyright 2024, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/cupertino.dart';
class VeggieSeasonsPage<T> extends Page<T> {
final Widget child;
const VeggieSeasonsPage({
super.key,
required this.child,
super.restorationId,
});
@override
VeggieSeasonsPageRoute<T> createRoute(BuildContext context) =>
VeggieSeasonsPageRoute<T>(this);
}
class VeggieSeasonsPageRoute<T> extends PageRoute<T> {
VeggieSeasonsPageRoute(VeggieSeasonsPage<T> page)
: super(settings: page);
VeggieSeasonsPage<T> get _page => settings as VeggieSeasonsPage<T>;
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
bool get maintainState => true;
@override
Duration get transitionDuration => Duration.zero;
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) => _page.child;
}

@ -1,82 +0,0 @@
name: veggieseasons
description: An iOS app that shows the fruits and veggies currently in season.
publish_to: none
version: 1.2.0
resolution: workspace
environment:
sdk: ^3.9.0-0
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
font_awesome_flutter: ^10.1.0
intl: ^0.20.0
provider: ^6.0.1
shared_preferences: ^2.0.14
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
# TODO: https://github.com/flutter/samples/issues/1838
# go_router ^7.1.0 is breaking the state restoration tests
go_router: ^16.0.0
dev_dependencies:
analysis_defaults:
path: ../analysis_defaults
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.14.0
flutter:
assets:
- assets/images/apple.jpg
- assets/images/artichoke.jpg
- assets/images/asparagus.jpg
- assets/images/avocado.jpg
- assets/images/blackberry.jpg
- assets/images/cantaloupe.jpg
- assets/images/cauliflower.jpg
- assets/images/endive.jpg
- assets/images/fig.jpg
- assets/images/grape.jpg
- assets/images/green_bell_pepper.jpg
- assets/images/habanero.jpg
- assets/images/kale.jpg
- assets/images/kiwi.jpg
- assets/images/lemon.jpg
- assets/images/lime.jpg
- assets/images/mango.jpg
- assets/images/mushroom.jpg
- assets/images/nectarine.jpg
- assets/images/persimmon.jpg
- assets/images/plum.jpg
- assets/images/potato.jpg
- assets/images/radicchio.jpg
- assets/images/radish.jpg
- assets/images/squash.jpg
- assets/images/strawberry.jpg
- assets/images/tangelo.jpg
- assets/images/tomato.jpg
- assets/images/watermelon.jpg
- assets/images/orange_bell_pepper.jpg
fonts:
- family: NotoSans
fonts:
- asset: assets/fonts/NotoSans-Regular.ttf
weight: 400
- asset: assets/fonts/NotoSans-Bold.ttf
weight: 700
- asset: assets/fonts/NotoSans-BoldItalic.ttf
weight: 700
style: italic
- asset: assets/fonts/NotoSans-Italic.ttf
style: italic
weight: 400
flutter_icons:
ios: true
image_path: "assets/icon/launcher_icon.png"
Loading…
Cancel
Save