diff --git a/lib/domain/models/card.dart b/lib/domain/models/card.dart new file mode 100644 index 0000000..4faa8f7 --- /dev/null +++ b/lib/domain/models/card.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class CardData { + final String text; + final String descriptionText; + final IconData icon; + final String? imageUrl; + + CardData( + this.text, { + required this.descriptionText, + this.icon = Icons.ac_unit_outlined, + this.imageUrl, + }); +} diff --git a/lib/main.dart b/lib/main.dart index ab32a7b..928788e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; - +import 'package:leonteva_pmu/presentation/home_page/home_page.dart'; void main() { runApp(const MyApp()); } @@ -7,119 +7,15 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const MyHomePage(title: 'Leonteva_V.A._ PIbd-33'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + home: const HomePage(title: 'The coolest hamsters on earth ^^'), ); } } diff --git a/lib/presentation/details_page/details_page.dart b/lib/presentation/details_page/details_page.dart new file mode 100644 index 0000000..f3f5724 --- /dev/null +++ b/lib/presentation/details_page/details_page.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:leonteva_pmu/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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Image.network( + data.imageUrl ?? '', + ), + ), + 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, + ) + ], + ), + ); + } +} diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart new file mode 100644 index 0000000..3221934 --- /dev/null +++ b/lib/presentation/home_page/card.dart @@ -0,0 +1,159 @@ +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.ac_unit_outlined, + 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), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(.5), + spreadRadius: 4, + offset: const Offset(0, 5), + blurRadius: 8, + ), + ], + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + topLeft: Radius.circular(20), + ), + child: SizedBox( + height: double.infinity, + width: 120, + child: Stack( + children: [ + Positioned.fill( + child: Image.network( + widget.imageUrl ?? '', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Placeholder(), + ), + ), + Align( + alignment: Alignment.bottomLeft, + child: Container( + decoration: const BoxDecoration( + color: Colors.orangeAccent, + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + // + )), + padding: const EdgeInsets.fromLTRB(8, 2, 8, 2), + child: Text( + 'скидок нет', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Colors.black), + ), + ), + ), + ], + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.text, + style: Theme.of(context).textTheme.headlineLarge, + ), + Text( + widget.descriptionText, + style: Theme.of(context).textTheme.bodyLarge, + ) + ], + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 16, + bottom: 16, + ), + child: GestureDetector( + onTap: () { + setState(() => isLiked = !isLiked); + widget.onLike?.call(widget.text, isLiked); + }, + child: AnimatedSwitcher( + duration: const Duration(microseconds: 200), + 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..b74687b --- /dev/null +++ b/lib/presentation/home_page/home_page.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:leonteva_pmu/presentation/details_page/details_page.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:leonteva_pmu/domain/models/card.dart'; + +part 'card.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + final Color _color = Colors.orangeAccent; + + @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('mouse', + descriptionText: 'hahaha', + icon: Icons.abc, + imageUrl: + 'https://sun9-26.userapi.com/impg/sB_tLPWPCIqxQWmlbcmlRYiw1ibFb70_QMtNwg/56qpyc_C8Go.jpg?size=736x711&quality=95&sign=8f7163b54538a2e7bad5f36a857485d4&type=album'), + CardData('mouse2', + descriptionText: 'ahahaha', + icon: Icons.access_alarm_outlined, + imageUrl: + 'https://sun165-1.userapi.com/impg/EVLbaLilqr8xw5tsqZLQQb6DSYrdKo7Q9sYSsw/H4FRwyMR6Ec.jpg?size=1280x960&quality=96&sign=f606e4ae3d1ccd27917cd1ffa6d91e58&type=album'), + CardData('mouse3', + descriptionText: 'eeee', + icon: Icons.access_alarm_rounded, + imageUrl: + 'https://sun34-1.userapi.com/impg/_DLT-op0LbBdgh5h-ILvC7IMDY5kbLR349v7vA/tX7vtk6mNlA.jpg?size=736x736&quality=96&sign=47f2b0f63bf249c62f4498fb637695d5&type=album'), + ]; + + 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( + 'Cute hamster $title ${isLiked ? 'liked!' : 'disliked :('}', + style: Theme.of(context).textTheme.bodyLarge, + ), + backgroundColor: Colors.orangeAccent, + duration: const Duration(seconds: 1), + )); + }); + } +}