diff --git a/example/assets/data/products.json b/example/assets/data/products.json index 3fe5e4c..ad6ae5c 100644 --- a/example/assets/data/products.json +++ b/example/assets/data/products.json @@ -56,7 +56,7 @@ "description": "OPPO F19 is officially announced on April 2021.", "price": 280, "discountPercentage": 17.91, - "rating": 4.3, + "rating": 2.3, "stock": 123, "brand": "OPPO", "category": "smartphones", @@ -75,7 +75,7 @@ "description": "Huawei’s re-badged P30 Pro New Edition was officially unveiled yesterday in Germany and now the device has made its way to the UK.", "price": 499, "discountPercentage": 10.58, - "rating": 4.09, + "rating": 3.09, "stock": 32, "brand": "Huawei", "category": "smartphones", @@ -110,7 +110,7 @@ "description": "Samsung Galaxy Book S (2020) Laptop With Intel Lakefield Chip, 8GB of RAM Launched", "price": 1499, "discountPercentage": 4.15, - "rating": 4.25, + "rating": 4.7, "stock": 50, "brand": "Samsung", "category": "laptops", @@ -128,7 +128,7 @@ "description": "Style and speed. Stand out on HD video calls backed by Studio Mics. Capture ideas on the vibrant touchscreen.", "price": 1499, "discountPercentage": 10.23, - "rating": 4.43, + "rating": 3.43, "stock": 68, "brand": "Microsoft Surface", "category": "laptops", @@ -147,7 +147,7 @@ "description": "Infinix Inbook X1 Ci3 10th 8GB 256GB 14 Win10 Grey – 1 Year Warranty", "price": 1099, "discountPercentage": 11.83, - "rating": 4.54, + "rating": 2.54, "stock": 96, "brand": "Infinix", "category": "laptops", @@ -166,7 +166,7 @@ "description": "HP Pavilion 15-DK1056WM Gaming Laptop 10th Gen Core i5, 8GB, 256GB SSD, GTX 1650 4GB, Windows 10", "price": 1099, "discountPercentage": 6.18, - "rating": 4.43, + "rating": 3.43, "stock": 89, "brand": "HP Pavilion", "category": "laptops", @@ -184,7 +184,7 @@ "description": "Mega Discount, Impression of Acqua Di Gio by GiorgioArmani concentrated attar Perfume Oil", "price": 13, "discountPercentage": 8.4, - "rating": 4.26, + "rating": 3.26, "stock": 65, "brand": "Impression of Acqua Di Gio", "category": "fragrances", @@ -202,7 +202,7 @@ "description": "Royal_Mirage Sport Brown Perfume for Men & Women - 120ml", "price": 40, "discountPercentage": 15.66, - "rating": 4, + "rating": 2.2, "stock": 52, "brand": "Royal_Mirage", "category": "fragrances", @@ -221,7 +221,7 @@ "description": "Product details of Best Fog Scent Xpressio Perfume 100ml For Men cool long lasting perfumes for Men", "price": 13, "discountPercentage": 8.14, - "rating": 4.59, + "rating": 3.59, "stock": 61, "brand": "Fog Scent Xpressio", "category": "fragrances", @@ -240,7 +240,7 @@ "description": "Original Al Munakh® by Mahal Al Musk | Our Impression of Climate | 6ml Non-Alcoholic Concentrated Perfume Oil", "price": 120, "discountPercentage": 15.6, - "rating": 4.21, + "rating": 1.21, "stock": 114, "brand": "Al Munakh", "category": "fragrances", @@ -296,7 +296,7 @@ "description": "Tea tree oil contains a number of compounds, including terpinen-4-ol, that have been shown to kill certain bacteria,", "price": 12, "discountPercentage": 4.09, - "rating": 4.52, + "rating": 3.52, "stock": 78, "brand": "Hemani Tea", "category": "skincare", @@ -333,7 +333,7 @@ "description": "Product name: rorec collagen hyaluronic acid white face serum riceNet weight: 15 m", "price": 46, "discountPercentage": 10.68, - "rating": 4.42, + "rating": 2.42, "stock": 54, "brand": "ROREC White Rice", "category": "skincare", @@ -351,7 +351,7 @@ "description": "Fair & Clear is Pakistan's only pure Freckle cream which helpsfade Freckles, Darkspots and pigments. Mercury level is 0%, so there are no side effects.", "price": 70, "discountPercentage": 16.99, - "rating": 4.06, + "rating": 1.06, "stock": 140, "brand": "Fair & Clear", "category": "skincare", @@ -370,7 +370,7 @@ "description": "Fine quality Branded Product Keep in a cool and dry place", "price": 20, "discountPercentage": 4.81, - "rating": 4.44, + "rating": 3.44, "stock": 133, "brand": "Saaf & Khaas", "category": "groceries", @@ -387,7 +387,7 @@ "description": "Product details of Bake Parlor Big Elbow Macaroni - 400 gm", "price": 14, "discountPercentage": 15.58, - "rating": 4.57, + "rating": 2.57, "stock": 146, "brand": "Bake Parlor Big", "category": "groceries", @@ -419,11 +419,11 @@ }, { "id": 24, - "title": "cereals muesli fruit nuts", - "description": "original fauji cereal muesli 250gm box pack original fauji cereals muesli fruit nuts flakes breakfast cereal break fast faujicereals cerels cerel foji fouji", + "title": "Cereals muesli fruit nuts", + "description": "Original fauji cereal muesli 250gm box pack original fauji cereals muesli fruit nuts flakes breakfast cereal break fast faujicereals cerels cerel foji fouji", "price": 46, "discountPercentage": 16.8, - "rating": 4.94, + "rating": 3.94, "stock": 113, "brand": "fauji", "category": "groceries", @@ -461,7 +461,7 @@ "description": "Boho Decor Plant Hanger For Home Wall Decoration Macrame Wall Hanging Shelf", "price": 41, "discountPercentage": 17.86, - "rating": 4.08, + "rating": 3.08, "stock": 131, "brand": "Boho Decor", "category": "home-decoration", @@ -500,7 +500,7 @@ "description": "3D led lamp sticker Wall sticker 3d wall art light on/off button cell operated (included)", "price": 20, "discountPercentage": 16.49, - "rating": 4.82, + "rating": 3.82, "stock": 54, "brand": "LED Lights", "category": "home-decoration", @@ -519,7 +519,7 @@ "description": "Handcraft Chinese style art luxury palace hotel villa mansion home decor ceramic vase with brass fruit plate", "price": 60, "discountPercentage": 15.34, - "rating": 4.44, + "rating": 2.44, "stock": 7, "brand": "luxury palace", "category": "home-decoration", diff --git a/example/lib/src/feature/shop/widget/category_screen.dart b/example/lib/src/feature/shop/widget/category_screen.dart index 3e79d73..b5f4ac3 100644 --- a/example/lib/src/feature/shop/widget/category_screen.dart +++ b/example/lib/src/feature/shop/widget/category_screen.dart @@ -171,89 +171,96 @@ class _ProductTile extends StatelessWidget { : child; @override - Widget build(BuildContext context) => Card( - clipBehavior: Clip.antiAlias, - color: Theme.of(context).cardColor, - margin: const EdgeInsets.all(4), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Stack( - children: [ - // Content - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Center( - child: AspectRatio( - aspectRatio: 1, - child: Stack( - children: [ - Positioned.fill( - child: Padding( - padding: const EdgeInsets.all(8), - child: discountBanner( - child: _ProductCardImage(product: product)), - ), - ), - Align( - alignment: const Alignment(-.65, .75), - child: _ProductPriceTag(product: product), + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Card( + clipBehavior: Clip.antiAlias, + color: Theme.of(context).cardColor, + margin: const EdgeInsets.all(4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Stack( + children: [ + // Content + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Center( + child: AspectRatio( + aspectRatio: 1, + child: Stack( + children: [ + Positioned.fill( + child: Padding( + padding: const EdgeInsets.all(8), + child: discountBanner( + child: _ProductCardImage(product: product)), ), - ], - ), + ), + Align( + alignment: const Alignment(-.65, .75), + child: _ProductPriceTag(product: product), + ), + ], ), ), ), - SizedBox( - height: 36, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Center( - child: Text( - product.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 12, - height: 0.8, - letterSpacing: -0.3, - fontWeight: FontWeight.w500, - ), + ), + SizedBox( + height: 36, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Align( + alignment: const Alignment(0, -.5), + child: Text( + product.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 12, + height: 0.9, + letterSpacing: -0.3, + fontWeight: FontWeight.w500, ), ), ), ), - ], - ), + ), + ], ), + ), - // Tap area - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () => onTap == null - ? Octopus.push( - context, - Routes.product, - arguments: { - 'id': product.id.toString() - }, - ) - : onTap?.call(context, product), - ), + // Tap area + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + hoverColor: theme.hoverColor, + splashColor: theme.splashColor, + highlightColor: theme.highlightColor, + onTap: () => onTap == null + ? Octopus.push( + context, + Routes.product, + arguments: { + 'id': product.id.toString() + }, + ) + : onTap?.call(context, product), ), ), - ], - ), - ); + ), + ], + ), + ); + } } class _ProductCardImage extends StatelessWidget { diff --git a/example/lib/src/feature/shop/widget/product_screen.dart b/example/lib/src/feature/shop/widget/product_screen.dart index 7889742..f10ab9b 100644 --- a/example/lib/src/feature/shop/widget/product_screen.dart +++ b/example/lib/src/feature/shop/widget/product_screen.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:collection/collection.dart'; +import 'package:example/src/common/util/color_util.dart'; import 'package:example/src/common/widget/common_actions.dart'; import 'package:example/src/common/widget/form_placeholder.dart'; import 'package:example/src/common/widget/not_found_screen.dart'; @@ -97,7 +99,28 @@ class ProductScreen extends StatelessWidget { sliver: SliverToBoxAdapter( child: Text( product.title, - style: Theme.of(context).textTheme.headlineSmall, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.coiny( + fontSize: 32, + fontWeight: FontWeight.w600, + height: 1, + letterSpacing: 1, + shadows: [ + const BoxShadow( + color: Colors.black12, + offset: Offset(7, 5), + blurRadius: 2, + blurStyle: BlurStyle.solid, + ), + const BoxShadow( + color: Colors.black12, + offset: Offset(7, 5), + blurRadius: 12, + blurStyle: BlurStyle.solid, + ), + ], + ), textAlign: TextAlign.center, ), ), @@ -110,15 +133,6 @@ class ProductScreen extends StatelessWidget { // Product rating and price _ProductRatingAndPrice(product: product), - /* // Favorite button - SliverPadding( - padding: - ScaffoldPadding.of(context).copyWith(bottom: 8, top: 8), - sliver: SliverToBoxAdapter( - child: FavoriteButton(product: product), - ), - ), */ - const SliverPadding( padding: EdgeInsets.only(bottom: 16), ), @@ -196,33 +210,13 @@ class _ProductRatingAndPrice extends StatelessWidget { height: 64, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '${product.rating} ', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: - Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w800, - height: 1, - ), - ), - const Icon( - Icons.star, - color: Colors.orange, - size: 32, - ), - ], - ), + child: _ProductStars(rating: product.rating), ), ), Card( elevation: 2, color: const Color.fromARGB(160, 0, 255, 13), + margin: const EdgeInsets.all(4), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16)), ), @@ -272,35 +266,29 @@ class _ProductRatingAndPrice extends StatelessWidget { child: Text( r'$', style: TextStyle( - fontSize: 20, + fontSize: 16, fontWeight: FontWeight.w800, ), ), ), - const SizedBox(width: 4), + const SizedBox(width: 2), Padding( padding: const EdgeInsets.only(top: 6), child: Text( product.price.toString(), style: const TextStyle( - fontSize: 46, + fontSize: 32, fontWeight: FontWeight.w800, ), ), ), const Padding( - padding: EdgeInsets.only(left: 16), + padding: EdgeInsets.only(left: 8), child: Icon( Icons.shopping_cart, color: Colors.white, - size: 32, + size: 24, shadows: [ - BoxShadow( - color: Colors.black, - offset: Offset.zero, - blurRadius: 1, - blurStyle: BlurStyle.solid, - ), BoxShadow( color: Colors.black, offset: Offset.zero, @@ -324,6 +312,123 @@ class _ProductRatingAndPrice extends StatelessWidget { ); } +class _ProductStars extends StatefulWidget { + const _ProductStars({ + required this.rating, + super.key, // ignore: unused_element + }); + + final double rating; + + @override + State<_ProductStars> createState() => _ProductStarsState(); +} + +class _ProductStarsState extends State<_ProductStars> + with SingleTickerProviderStateMixin { + static const circleRadius = 32.0; + static const iconsSize = 24.0; + late final AnimationController _controller; + final List _icons = []; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 10), + )..repeat(); // Запускает анимацию на повторение + _rebuildIcons(); + } + + @override + void didUpdateWidget(covariant _ProductStars oldWidget) { + super.didUpdateWidget(oldWidget); + _rebuildIcons(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _rebuildIcons() { + _icons.clear(); + final rating = widget.rating; + final rating10 = (rating * 2).round(); + for (var r = 1; r < 11; r += 2) { + if (rating10 > r) { + _icons.add( + const Icon( + Icons.star, + color: Colors.deepOrange, + size: iconsSize, + ), + ); + } else if (rating10 == r) { + _icons.add( + const Icon( + Icons.star_half, + color: Colors.orange, + size: iconsSize, + ), + ); + } else { + _icons.add( + const Icon( + Icons.star_border, + color: Colors.blueGrey, + size: iconsSize, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final rating = widget.rating; + + return Center( + child: Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + rating.toStringAsFixed(1), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.coiny( + fontSize: 24, + fontWeight: FontWeight.w800, + height: 1, + ), + ), + ), + ..._icons.mapIndexed( + (i, icon) => AnimatedBuilder( + animation: _controller, + builder: (context, _) { + var angle = + 2 * math.pi / 5 * i + (2 * math.pi * _controller.value); + return Transform.translate( + offset: Offset( + circleRadius * math.cos(angle), + circleRadius * math.sin(angle), + ), + child: _icons[i], + ); + }, + ), + ), + ], + ), + ); + } +} + class _ProductDivider extends StatelessWidget { const _ProductDivider({ super.key, // ignore: unused_element @@ -371,28 +476,122 @@ class _ProductDividerPainter extends CustomPainter { bool shouldRebuildSemantics(_ProductDividerPainter oldDelegate) => false; } -class _ProductTags extends StatelessWidget { +class _ProductTags extends StatefulWidget { const _ProductTags({super.key}); // ignore: unused_element + @override + State<_ProductTags> createState() => _ProductTagsState(); +} +class _ProductTagsState extends State<_ProductTags> { + final count = math.Random().nextInt(4) + 2; + late final colors = ColorUtil.getColors(count); + // ignore: unused_element @override Widget build(BuildContext context) => SliverPadding( padding: ScaffoldPadding.of(context).copyWith(bottom: 8, top: 8), sliver: SliverToBoxAdapter( child: Wrap( alignment: WrapAlignment.center, - spacing: 8, - runSpacing: 8, + spacing: 4, + runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, runAlignment: WrapAlignment.center, direction: Axis.horizontal, verticalDirection: VerticalDirection.down, children: [ - for (var i = 1; i < 5; i++) - ActionChip( - onPressed: () {}, - label: Text('Tag: $i'), - shape: const StadiumBorder(), + for (var i = 0; i < colors.length; i++) + _ProductTag('Tag', 'Value ${i + 1}', color: colors[i]), + ], + ), + ), + ); +} + +class _ProductTag extends StatelessWidget { + const _ProductTag( + this.k, + this.v, { + required this.color, + super.key, // ignore: unused_element + }); + + final Color color; + final String k; + final String v; + + @override + Widget build(BuildContext context) => SizedBox( + width: 128, + height: 32, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: Theme.of(context).colorScheme.surface, + border: const Border.fromBorderSide( + BorderSide( + color: Colors.black, + width: .5, + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + width: 42, + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(16), + ), + border: const Border.fromBorderSide( + BorderSide( + color: Colors.black, + width: .5, + ), + ), + ), + child: Center( + child: Text( + k.toUpperCase(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.coiny( + fontSize: 12, + fontWeight: FontWeight.w400, + height: 0, + color: Colors.white, + shadows: [ + const BoxShadow( + color: Colors.black, + offset: Offset.zero, + blurRadius: 1, + blurStyle: BlurStyle.inner, + ), + ], + ), + ), + ), + ), + ), + Expanded( + child: Align( + alignment: const Alignment(-0.25, 0), + child: Text( + v, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.coiny( + fontSize: 12, + fontWeight: FontWeight.bold, + height: 0, + ), + ), ), + ), ], ), ),