diff --git a/lib/screens/shared/media/carousel_banner.dart b/lib/screens/shared/media/carousel_banner.dart index e9cad90..ef83ea1 100644 --- a/lib/screens/shared/media/carousel_banner.dart +++ b/lib/screens/shared/media/carousel_banner.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; +import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -30,139 +31,190 @@ class CarouselBanner extends ConsumerStatefulWidget { } class _CarouselBannerState extends ConsumerState { + final carouselController = CarouselController(); + bool showControls = false; + @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints(maxHeight: widget.maxHeight), - child: LayoutBuilder( - builder: (context, constraints) { - final maxExtent = (constraints.maxHeight * 2.1).clamp( - 250.0, - (MediaQuery.sizeOf(context).shortestSide * 0.75).clamp(251.0, double.maxFinite), - ); - final border = BorderRadius.circular(18); - return Padding( - padding: - const EdgeInsets.symmetric(horizontal: 4).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), - child: CarouselView( - elevation: 3, - shrinkExtent: 0, - shape: RoundedRectangleBorder(borderRadius: border), - padding: const EdgeInsets.symmetric(horizontal: 6), - enableSplash: false, - itemExtent: widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent, - children: [ - ...widget.items.mapIndexed( - (index, item) => LayoutBuilder(builder: (context, constraints) { - final opacity = (constraints.maxWidth / maxExtent); - return Stack( - clipBehavior: Clip.none, - children: [ - FladderImage(image: item.bannerImage), - Opacity( - opacity: opacity.clamp(0, 1), - child: Stack( + return MouseRegion( + onEnter: (event) => setState(() => showControls = true), + onExit: (event) => setState(() => showControls = false), + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: widget.maxHeight), + child: LayoutBuilder( + builder: (context, constraints) { + final maxExtent = (constraints.maxHeight * 2.1).clamp( + 250.0, + (MediaQuery.sizeOf(context).shortestSide * 0.75).clamp(251.0, double.maxFinite), + ); + final border = BorderRadius.circular(18); + final itemExtent = widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4) + .copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), + child: Stack( + children: [ + CarouselView( + elevation: 3, + shrinkExtent: 0, + controller: carouselController, + shape: RoundedRectangleBorder(borderRadius: border), + padding: const EdgeInsets.symmetric(horizontal: 6), + enableSplash: false, + itemExtent: itemExtent, + children: [ + ...widget.items.mapIndexed( + (index, item) => LayoutBuilder(builder: (context, constraints) { + final opacity = (constraints.maxWidth / maxExtent); + return Stack( + clipBehavior: Clip.none, children: [ - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomLeft, - end: Alignment.topCenter, - colors: [ - ThemesData.of(context) - .dark - .colorScheme - .primaryContainer - .withValues(alpha: 0.85), - Colors.transparent, - ], + FladderImage(image: item.bannerImage), + Opacity( + opacity: opacity.clamp(0, 1), + child: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomLeft, + end: Alignment.topCenter, + colors: [ + ThemesData.of(context) + .dark + .colorScheme + .primaryContainer + .withValues(alpha: 0.85), + Colors.transparent, + ], + ), + ), + ), ), + ], + ), + ), + Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: const EdgeInsets.all(16.0).copyWith(right: constraints.maxWidth * 0.2), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 2, + softWrap: item.title.length > 25, + overflow: TextOverflow.fade, + style: + Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), + ), + if (item.label(context) != null || item.subText != null) + Text( + item.label(context) ?? item.subText ?? "", + maxLines: 2, + softWrap: false, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white), + ), + ].addInBetween(const SizedBox(height: 4)), ), ), ), - ], - ), - ), - Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: const EdgeInsets.all(16.0).copyWith(right: constraints.maxWidth * 0.2), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title, - maxLines: 2, - softWrap: item.title.length > 25, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), + FlatButton( + onTap: () => widget.items[index].navigateTo(context), + onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer + ? null + : () { + final poster = widget.items[index]; + showBottomSheetPill( + context: context, + item: poster, + content: (scrollContext, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: poster + .generateActions(context, ref) + .listTileItems(scrollContext, useIcons: true), + ), + ); + }, + onSecondaryTapDown: AdaptiveLayout.of(context).inputDevice == InputDevice.touch + ? null + : (details) async { + Offset localPosition = details.globalPosition; + RelativeRect position = RelativeRect.fromLTRB(localPosition.dx - 320, + localPosition.dy, localPosition.dx, localPosition.dy); + final poster = widget.items[index]; + + await showMenu( + context: context, + position: position, + items: poster.generateActions(context, ref).popupMenuItems(useIcons: true), + ); + }, + ), + BannerPlayButton(item: widget.items[index]), + IgnorePointer( + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.white.withValues(alpha: 0.1), + width: 1.0, + ), + borderRadius: border), ), - if (item.label(context) != null || item.subText != null) - Text( - item.label(context) ?? item.subText ?? "", - maxLines: 2, - softWrap: false, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white), - ), - ].addInBetween(const SizedBox(height: 4)), + ), + ], + ); + }), + ) + ], + ), + if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) + AnimatedOpacity( + duration: const Duration(milliseconds: 250), + opacity: showControls ? 1 : 0, + child: IgnorePointer( + ignoring: !showControls, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Align( + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton.filledTonal( + onPressed: () { + final currentPos = carouselController.position; + carouselController.animateTo(currentPos.pixels - itemExtent, + curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250)); + }, + icon: const Icon(IconsaxOutline.arrow_left_2), + ), + IconButton.filledTonal( + onPressed: () { + final currentPos = carouselController.position; + carouselController.animateTo(currentPos.pixels + itemExtent, + curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250)); + }, + icon: const Icon(IconsaxOutline.arrow_right_3), + ), + ], ), ), ), - FlatButton( - onTap: () => widget.items[index].navigateTo(context), - onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer - ? null - : () { - final poster = widget.items[index]; - showBottomSheetPill( - context: context, - item: poster, - content: (scrollContext, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: poster - .generateActions(context, ref) - .listTileItems(scrollContext, useIcons: true), - ), - ); - }, - onSecondaryTapDown: AdaptiveLayout.of(context).inputDevice == InputDevice.touch - ? null - : (details) async { - Offset localPosition = details.globalPosition; - RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy); - final poster = widget.items[index]; - - await showMenu( - context: context, - position: position, - items: poster.generateActions(context, ref).popupMenuItems(useIcons: true), - ); - }, - ), - BannerPlayButton(item: widget.items[index]), - IgnorePointer( - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.white.withValues(alpha: 0.1), - width: 1.0, - ), - borderRadius: border), - ), - ), - ], - ); - }), - ) - ], - ), - ); - }, + ), + ), + ], + ), + ); + }, + ), ), ); }