diff --git a/lib/main.dart b/lib/main.dart index fd137e3..45df3e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,208 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:pmd_labs/presentation/home_page/home_page.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override Widget build(BuildContext context) { return MaterialApp( - title: 'Рецепты', + title: 'Flutter Demo', + debugShowCheckedModeBanner: false, theme: ThemeData( - scaffoldBackgroundColor: Colors.purple[100], + colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange), + useMaterial3: true, ), - home: RecipeHome(), + home: const MyHomePage(title: 'Анекдоты и мопсы'), ); } } -enum Cuisine { Italian, Indian, Chinese, Georgian } -class Recipe { - String name; - Cuisine cuisine; - List ingredients; - String imageUrl; - bool liked; - Recipe({ - required this.name, - required this.cuisine, - required this.ingredients, - required this.imageUrl, - this.liked = false, - }); - - String getDetails() { - return 'Рецепт: $name\nКухня: ${cuisine.toString().split('.').last}\nИнгредиенты: ${ingredients.join(', ')}'; - } -} - -class RecipeManager { - List _recipes = []; - - Future addRecipe(T recipe) async { - await Future.delayed(Duration(seconds: 1)); - _recipes.add(recipe); - } - - List getAllRecipes() => _recipes; - - T? getRecipeAt(int index) { - if (index < _recipes.length) { - return _recipes[index]; - } - return null; - } -} - -class RecipeHome extends StatefulWidget { - @override - _RecipeHomeState createState() => _RecipeHomeState(); -} - -class _RecipeHomeState extends State { - final RecipeManager _recipeManager = RecipeManager(); - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _ingredientsController = TextEditingController(); - final TextEditingController _imageUrlController = TextEditingController(); - Cuisine? _selectedCuisine; - - void _addRecipe() { - final name = _nameController.text; - final ingredients = _ingredientsController.text.split(',').map((s) => s.trim()).toList(); - final imageUrl = _imageUrlController.text; - - if (name.isNotEmpty && ingredients.isNotEmpty && _selectedCuisine != null && imageUrl.isNotEmpty) { - final recipe = Recipe( - name: name, - cuisine: _selectedCuisine!, - ingredients: ingredients, - imageUrl: imageUrl, - ); - _recipeManager.addRecipe(recipe).then((_) { - _nameController.clear(); - _ingredientsController.clear(); - _imageUrlController.clear(); - _selectedCuisine = null; - setState(() {}); - }); - } - } - - void _likeRecipe(int index) { - setState(() { - final recipe = _recipeManager.getRecipeAt(index); - if (recipe != null) { - recipe.liked = !recipe.liked; - final message = recipe.liked ? 'Рецепт добавлен в избранное!' : 'Лайк убран!'; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); - } - }); - } - - void _showRecipeDetail(Recipe recipe) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RecipeDetail(recipe: recipe), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Создание Рецептов')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - TextField( - controller: _nameController, - decoration: InputDecoration(labelText: 'Название рецепта'), - ), - DropdownButton( - hint: Text('Выберите кухню'), - value: _selectedCuisine, - onChanged: (Cuisine? newValue) { - setState(() { - _selectedCuisine = newValue; - }); - }, - items: Cuisine.values.map((Cuisine cuisine) { - return DropdownMenuItem( - value: cuisine, - child: Text(cuisine.toString().split('.').last), - ); - }).toList(), - ), - TextField( - controller: _ingredientsController, - decoration: InputDecoration(labelText: 'Ингредиенты (через запятую)'), - ), - TextField( - controller: _imageUrlController, - decoration: InputDecoration(labelText: 'URL изображения'), - ), - SizedBox(height: 20), - ElevatedButton( - onPressed: _addRecipe, - child: Text('Добавить Рецепт'), - ), - SizedBox(height: 20), - Expanded( - child: ListView.builder( - itemCount: _recipeManager.getAllRecipes().length, - itemBuilder: (context, index) { - final recipe = _recipeManager.getRecipeAt(index); - return ListTile( - leading: recipe != null && recipe.imageUrl.isNotEmpty - ? Image.network(recipe.imageUrl, width: 50, height: 50, fit: BoxFit.cover) - : null, - title: Text(recipe?.name ?? 'Ошибка'), - subtitle: Text(recipe?.getDetails() ?? ''), - trailing: IconButton( - icon: Icon(recipe?.liked == true ? Icons.favorite : Icons.favorite_border), - onPressed: () => _likeRecipe(index), - ), - onTap: () { - if (recipe != null) { - _showRecipeDetail(recipe); - } - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} - -class RecipeDetail extends StatelessWidget { - final Recipe recipe; - - RecipeDetail({required this.recipe}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(recipe.name)), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.network(recipe.imageUrl), - SizedBox(height: 20), - Text(recipe.getDetails(), style: TextStyle(fontSize: 18)), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/presentation/details_page/details_page.dart b/lib/presentation/details_page/details_page.dart new file mode 100644 index 0000000..7d66f69 --- /dev/null +++ b/lib/presentation/details_page/details_page.dart @@ -0,0 +1,58 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:pmd_labs/domain/models/card.dart'; + +class DetailsPage extends StatelessWidget { + final CardData data; + + const DetailsPage(this.data, {super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Изображение слева + Padding( + padding: const EdgeInsets.only(right: 16.0), // Отступ справа от изображения + child: ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: Image.network( + data.imageUrl ?? '', + fit: BoxFit.cover, + width: 150.0, // Ширина изображения + height: 150.0, // Высота изображения + ), + ), + ), + // Текст справа от изображения + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + data.text, + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + Text( + data.descriptionText, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontSize: 25.0, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart new file mode 100644 index 0000000..290ff6a --- /dev/null +++ b/lib/presentation/home_page/card.dart @@ -0,0 +1,131 @@ +part of 'home_page.dart'; + + +typedef OnLikeCallback = void Function(String title, bool isLiked)?; + +class _Card extends StatefulWidget { + final String text; + final String descriptionText; + final IconData icon; + final String? imageUrl; + final OnLikeCallback onLike; + final VoidCallback? onTap; + + const _Card( + this.text, { + this.icon = Icons.catching_pokemon, + required this.descriptionText, + this.imageUrl, + this.onLike, + this.onTap, + }); + + factory _Card.fromData( + CardData data, { + OnLikeCallback onLike, + VoidCallback? onTap, + }) => + _Card( + data.text, + descriptionText: data.descriptionText, + icon: data.icon, + imageUrl: data.imageUrl, + onLike: onLike, + onTap: onTap, + ); + + @override + State<_Card> createState() => _CardState(); +} + +class _CardState extends State<_Card> { + bool isLiked = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onTap, + child: Container( + margin: const EdgeInsets.all(16), + constraints: const BoxConstraints(minHeight: 140), + decoration: BoxDecoration( + color: Colors.white70, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(.5), + spreadRadius: 4, + offset: const Offset(0, 5), + blurRadius: 8, + ), + ], + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + height: 140, + width: 140, + child: Image.network( + widget.imageUrl ?? '', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Placeholder(), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.text, + style: Theme + .of(context) + .textTheme + .bodyLarge + ?.copyWith( + fontSize: 25.0, + ), + ), + + ], + ), + ), + ), + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: GestureDetector( + onTap: () { + setState(() { + isLiked = !isLiked; + }); + widget.onLike?.call(widget.text, isLiked); + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: isLiked + ? const Icon( + Icons.favorite, + color: Colors.redAccent, + key: ValueKey(0), + ) + : const Icon( + Icons.favorite_border, + key: ValueKey(1), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart new file mode 100644 index 0000000..fd7490b --- /dev/null +++ b/lib/presentation/home_page/home_page.dart @@ -0,0 +1,106 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:pmd_labs/domain/models/card.dart'; + +import '../details_page/details_page.dart'; + +part 'card.dart'; + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final Color _color = Colors.brown.shade300; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: _color, + title: Text(widget.title), + ), + body: const Body(), + ); + } +} + +class Body extends StatelessWidget { + const Body({super.key}); + + @override + Widget build(BuildContext context) { + final data = [ + CardData( + 'Анекдот №1', + descriptionText: '— Итак, Сара, вот ваша диета: одно яблоко, одно вареное яйцо, нежирный творог, зелень\n' + '— Ясно, доктор, но это до или после еды?', + icon: Icons.favorite, + imageUrl: 'https://i.pinimg.com/474x/97/ee/af/97eeaffa3171b0a53bb4161bfc9e3756.jpg', + ), + CardData( + 'Анекдот №2', + descriptionText: '– Яна Моисеевна, а что вам муженек на день рождения подарил?\n' + '– Вон видите феррари за окном стоит?\n' + '– Не может быть!\n' + '– Вот точно такого же цвета шарфик.\n', + icon: Icons.favorite, + imageUrl: + 'https://i.pinimg.com/474x/8c/4f/40/8c4f4093ab3ff9ddb1d8880a94b4d586.jpg', + ), + CardData( + 'Анекдот №3', + descriptionText: 'Чем дольше звонят в дверь, тем больше Рабиновича нет дома.', + icon: Icons.favorite, + imageUrl: + 'https://i.pinimg.com/474x/3a/1f/3c/3a1f3ca4a57ff8a16882093c2f278922.jpg', + ), + ]; + + return Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: data.map((data) { + return _Card.fromData( + data, + onLike: (String title, bool isLiked) => + _showSnackBar(context, title, isLiked), + onTap: () => _navToDetails(context, data), + ); + }).toList(), + ), + ), + ); + } + + void _navToDetails(BuildContext context, CardData data){ + Navigator.push( + context, + CupertinoPageRoute(builder: (context) => DetailsPage(data)), + ); + } + + void _showSnackBar(BuildContext context, String title, bool isLiked) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + 'Вам ${isLiked ? 'понравился' : 'больше не нравится'} $title', + style: Theme.of(context).textTheme.bodyLarge, + ), + backgroundColor: Colors.brown.shade400, + duration: const Duration(seconds: 1), + )); + }); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 07514d0..f5f4863 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -75,6 +75,22 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: "direct main" + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" leak_tracker: dependency: transitive description: @@ -192,6 +208,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 37f6555..5aecbe5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ environment: dependencies: flutter: sdk: flutter + http: ^0.13.3 # The following adds the Cupertino Icons font to your application. @@ -40,6 +41,11 @@ dev_dependencies: flutter_test: sdk: flutter + + + + + # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your