diff --git a/lib/domain/models/game.dart b/lib/domain/models/game.dart new file mode 100644 index 0000000..644ffdf --- /dev/null +++ b/lib/domain/models/game.dart @@ -0,0 +1,14 @@ +class GameData { + final String name; + final int price; + final String? image; + //Описание игры + final String? description; + + GameData( + {required this.name, + required this.price, + this.description, + this.image = + 'https://parpol.ru/wp-content/uploads/2019/09/placeholder.png'}); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b41c1ad..556a4f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'presentation/home_page/home_page.dart'; + void main() { runApp(const MyApp()); } @@ -17,225 +19,4 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color.fromARGB(255, 46, 65, 80), - appBar: AppBar( - backgroundColor: Color.fromARGB(255, 56, 90, 128), - title: Text( - widget.title, - style: - const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - body: Container( - //симметричный боковой отступ от границ экрана - margin: const EdgeInsets.symmetric(horizontal: 15.0), - child: const GameWidget(), - ), - ); - } -} - -class GameWidget extends StatelessWidget { - const GameWidget({super.key}); - - @override - Widget build(BuildContext context) { - final data=[ - _GameData( - name: 'Late Shift', - price: 399, - image: - 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/584980/capsule_616x353.jpg?t=1697110140'), - _GameData( - name: 'Dark Nights with Poe & Munro', - price: 450, - image: - 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1098170/capsule_616x353.jpg?t=1725541685'), - _GameData( - name: 'Неизвестная игра', - price: 999, - ) - ]; - return Center( - child: SingleChildScrollView( - child: Column( - children: data.map((e) => _GameCard.fromData(e)).toList()) - ) - ); - } -} - -class _GameData { - final String name; - final int price; - final String? image; - - _GameData({ - required this.name, - required this.price, - this.image='https://parpol.ru/wp-content/uploads/2019/09/placeholder.png'}); -} - -class _GameCard extends StatelessWidget { - final String name; - final int price; - final String? image; - - //обычный конструктор - const _GameCard({ - super.key, - required this.name, - required this.price, - this.image, - }); - - //именованный конструктор - factory _GameCard.fromData(_GameData data) => _GameCard( - name: data.name, - price: data.price, - image: data.image); - - @override - Widget build(BuildContext context) { - - return Container( - margin: const EdgeInsets.only(top: 10, bottom: 10), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color.fromARGB(255, 56, 90, 128), - borderRadius: BorderRadius.circular(5), - ), - child: - Column( - //Выравнивание по середине - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: 200, - width: MediaQuery.of(context).size.width, - child: Image.network(image ?? '', - fit: BoxFit.fill, - errorBuilder: (_, __, ___) => const Placeholder()), - ), - // Название игры - Text( - name, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 32, - ), - textAlign: TextAlign.center, - // softWrap: true, - ), - // Цена игры - Text( - '$price руб.', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 24, - ), - //softWrap: true, - ), - ], - ) - ); - } -} - -// class Game extends Object { -// final String name; -// final int price; -// final GameType type; //жанр игры -// final String? description; -// -// @override -// bool operator ==(Object other) { -// if (other is Game) { -// return this.name == other.name; -// } -// return false; -// } -// -// //стандартный конструктор с именованными параметрами -// Game( -// {required this.name, -// required this.price, -// required this.type, -// this.description}); -// -// //именованный конструктор с примером игры -// Game.exampleLateShift() -// : name = 'Late Shift', -// price = 200, -// type = GameType.FMV, -// description = null; -// -// void printInfo() { -// print(''' -// Игра: $name -// Жанр: ${GameTypeNames[type]} -// Цена: $price руб. '''); -// if (description != null) { -// print('Описание: $description'); -// } -// } -// -// //асинхронный метод - иммитация скачивания -// Future download() async { -// print('Начало загрузки игры $name'); -// var rand = Random(); -// int time = rand.nextInt(10000) + 100; -// //асинхронность (Future), анонимная функция (Anonymous function) -// await Future.delayed( -// Duration(milliseconds: time), () => print("Игра $name загружена")); -// } -// } -// -// enum GameType { FMV, Shooter, Strategy, Horror } -// -// //Названия жанров на русском -// const Map GameTypeNames = { -// GameType.FMV: "FMV", -// GameType.Shooter: "Шутер", -// GameType.Strategy: "Стратегия", -// GameType.Horror: "Хоррор" -// }; -// -// void availableTypesRus() => printMapValues(GameTypeNames); -// -// //цикл (loop) + generic (хз, куда ещё их можно) -// void printMapValues(Map map) { -// for (var val in map.values) { -// print(val); -// } -// } -// -// extension GameTypeRus on GameType { -// String get rusName { -// switch (this) { -// case GameType.FMV: -// return 'FMV'; -// case GameType.Shooter: -// return 'Шутер'; -// case GameType.Strategy: -// return 'Стратегия'; -// case GameType.Horror: -// return 'Хоррор'; -// } -// } -// } diff --git a/lib/presentation/details_page/details_page.dart b/lib/presentation/details_page/details_page.dart new file mode 100644 index 0000000..9a41708 --- /dev/null +++ b/lib/presentation/details_page/details_page.dart @@ -0,0 +1,69 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mobiles_labs_5th_semester/domain/models/game.dart'; + +class DetailsPage extends StatelessWidget { + final GameData data; + + const DetailsPage(this.data, {super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Color.fromARGB(255, 56, 90, 128), + + //backgroundColor: Color.fromARGB(255, 46, 65, 80), + iconTheme: IconThemeData(color: Colors.white) + ), + backgroundColor: Color.fromARGB(255, 46, 65, 80), + //backgroundColor: Color.fromARGB(255, 56, 90, 128), + body: Center( + //прокрутка по вертикали на случай, если описание длинное + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + SizedBox( + height: 220, + width: MediaQuery.of(context).size.width, + child: Image.network( + data.image ?? '', + fit: BoxFit.fill, + errorBuilder: (_, __, ___) => const Placeholder(), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), + child: Text( + data.name, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 40, + ), + textAlign: TextAlign.center, + ), + ), + Text( + data.description ?? 'У игры нет описания', + style: const TextStyle( + color: Colors.white, + fontSize: 24, + ), + softWrap: true, + // textAlign: TextAlign.left + textAlign: TextAlign.justify + ), + ] + + ), + ), + ) + + ) + ); + + } +} \ No newline at end of file diff --git a/lib/presentation/home_page/gameCard.dart b/lib/presentation/home_page/gameCard.dart new file mode 100644 index 0000000..ac8c8ba --- /dev/null +++ b/lib/presentation/home_page/gameCard.dart @@ -0,0 +1,125 @@ +part of 'home_page.dart'; + +typedef OnLikeCallback = void Function(String title, bool isLiked)?; + +class _GameCard extends StatefulWidget { + final String name; + final int price; + final String? image; + final String? description; + + //OnLikeCallback - пользовательский тип, ф-ия + final OnLikeCallback onLike; + final VoidCallback? onTap; + + //обычный конструктор + const _GameCard({ + super.key, + required this.name, + required this.price, + this.image, + this.description, + this.onLike, + this.onTap, + }); + + //именованный конструктор + factory _GameCard.fromData(GameData data, + {OnLikeCallback onLike, VoidCallback? onTap}) => + _GameCard( + name: data.name, + price: data.price, + image: data.image, + description: data.description, + onLike: onLike, + onTap: onTap, + ); + + //Переход в stateful + @override + State<_GameCard> createState() => _GameCardState(); +} + +//extends - переход в stateful +class _GameCardState extends State<_GameCard> { + //доп. поле, которое будет меняться; состояние того, нравится игра или нет + bool isLiked = false; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(10), + padding: const EdgeInsets.only(top: 3, bottom: 3, left: 10, right: 10), + // constraints: const BoxConstraints(minHeight: 350), + //форма карточки + decoration: BoxDecoration( + color: const Color.fromARGB(255, 56, 90, 128), + borderRadius: BorderRadius.circular(5), + ), + child: IntrinsicHeight( + child: Column( + //Выравнивание по середине + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 4, top: 2), + child: GestureDetector( + onTap: () { + setState(() { + isLiked = !isLiked; + }); + widget.onLike?.call(widget.name, isLiked); + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 175), + child: isLiked + ? const Icon(Icons.favorite, + color: Colors.pink, key: ValueKey(0)) + : const Icon(Icons.favorite_border, + color: Colors.white, key: ValueKey(1))))), + ], + ), + //ClipRRect для скругления краёв фото + ClipRRect( + borderRadius: BorderRadius.circular(4.0), + child: SizedBox( + height: 200, + width: MediaQuery.of(context).size.width, + child: Image.network( + widget.image ?? '', + fit: BoxFit.fill, + errorBuilder: (_, __, ___) => const Placeholder(), + ), + ), + ), + // Название игры + GestureDetector( + onTap: widget.onTap, + child: + Text( + widget.name, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 32, + ), + textAlign: TextAlign.center, + ) + ), + // Цена игры + Text( + '${widget.price} руб.', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + ], + ), + )); + } +} diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart new file mode 100644 index 0000000..9abb866 --- /dev/null +++ b/lib/presentation/home_page/home_page.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:mobiles_labs_5th_semester/domain/models/game.dart'; +import 'package:mobiles_labs_5th_semester/presentation/details_page/details_page.dart'; + +part 'gameCard.dart'; + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromARGB(255, 46, 65, 80), + appBar: AppBar( + backgroundColor: Color.fromARGB(255, 56, 90, 128), + title: Text( + widget.title, + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + body: const Body()); + } +} + +class Body extends StatelessWidget { + const Body({super.key}); + + @override + Widget build(BuildContext context) { + final data = [ + GameData( + name: 'Late Shift', + price: 399, + description: 'Late Shift - криминальный FMV-триллер с невероятно высокими ставками. Вы окажетесь в эпицентре лондонского ограбления и выберете своё приключение в интерактивном кинофильме с меняющейся историей, ведущей к одной из семи концовок. Ваши решения определяют вас.', + image: + 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/584980/capsule_616x353.jpg?t=1697110140'), + GameData( + name: 'Dark Nights with Poe & Munro', + price: 450, + description: 'Проведите местных радиоведущих По и Манро через шесть похожих на короткометражки эпизодов сверъестественной странности и обжигающего сюжета. От создателей The Infectious Madness of Doctor Dekker и The Shapeshifting Detective.', + image: + 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1098170/capsule_616x353.jpg?t=1725541685'), + GameData( + name: 'Неизвестная игра', + price: 999, + ) + ]; + return Center( + child: SingleChildScrollView( + child: Column( + children: data.map((data) { + return _GameCard.fromData( + data, + onLike: (String title, bool isLiked) => + _showSnackBar(context, title, isLiked), + onTap: () => _navToDetails(context, data), + ); + }).toList()))); + } + + void _showSnackBar(BuildContext context, String title, bool isLiked) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + isLiked + ? 'Вы поставили лайк игре "$title"' + : 'Вы убрали лайк у игры "$title"', + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: Colors.pink, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + backgroundColor: Colors.white, + duration: const Duration(seconds: 1), + )); + }); + } + void _navToDetails(BuildContext context, GameData data) { + Navigator.push( + context, + CupertinoPageRoute(builder: (context) => DetailsPage(data)), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 1a93611..f3aa812 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,50 +1,25 @@ name: mobiles_labs_5th_semester description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. +publish_to: 'none' version: 1.0.0+1 environment: sdk: ^3.5.2 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + #добавил в лаб 5 + json_annotation: ^4.8.1 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 - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. + + flutter_lints: ^4.0.0 # For information on the generic Dart part of this file, see the