diff --git a/lib/screens/shared/flat_button.dart b/lib/screens/shared/flat_button.dart index c350d10..dc9442d 100644 --- a/lib/screens/shared/flat_button.dart +++ b/lib/screens/shared/flat_button.dart @@ -18,6 +18,7 @@ class FlatButton extends ConsumerWidget { final double elevation; final bool showFeedback; final Clip clipBehavior; + final List overlays; const FlatButton({ this.child, this.onFocusChange, @@ -32,6 +33,7 @@ class FlatButton extends ConsumerWidget { this.elevation = 0, this.showFeedback = true, this.clipBehavior = Clip.none, + this.overlays = const [], super.key, }); @@ -67,6 +69,7 @@ class FlatButton extends ConsumerWidget { ), ), ), + ...overlays, ], ); } diff --git a/lib/screens/shared/media/carousel_banner.dart b/lib/screens/shared/media/carousel_banner.dart index 5878270..7b08f2d 100644 --- a/lib/screens/shared/media/carousel_banner.dart +++ b/lib/screens/shared/media/carousel_banner.dart @@ -70,6 +70,7 @@ class _CarouselBannerState extends ConsumerState { final opacity = (constraints.maxWidth / maxExtent); return FocusButton( onTap: () => widget.items[index].navigateTo(context), + borderRadius: border, onFocusChanged: (hover) { context.ensureVisible(); }, @@ -155,9 +156,6 @@ class _CarouselBannerState extends ConsumerState { ), ), ), - ExcludeFocus( - child: BannerPlayButton(item: widget.items[index]), - ), IgnorePointer( child: Container( decoration: BoxDecoration( @@ -171,6 +169,11 @@ class _CarouselBannerState extends ConsumerState { ), ], ), + overlays: [ + ExcludeFocus( + child: BannerPlayButton(item: widget.items[index]), + ), + ], ); }, ), diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index 46de949..16ee3e1 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -1,3 +1,4 @@ +// poster_image.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -54,7 +55,7 @@ class PosterImage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final posterRadius = FladderTheme.smallShape.borderRadius; + final radius = FladderTheme.smallShape.borderRadius; final padding = const EdgeInsets.all(5); final myKey = key ?? UniqueKey(); @@ -74,240 +75,188 @@ class PosterImage extends ConsumerWidget { } }, onFocusChanged: onFocusChanged, - onLongPress: () { - showBottomSheetPill( - context: context, - item: poster, - content: (scrollContext, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: poster - .generateActions( - context, - ref, - exclude: excludeActions, - otherActions: otherActions, - onUserDataChanged: onUserDataChanged, - onDeleteSuccesFully: onItemRemoved, - onItemUpdated: onItemUpdated, - ) - .listTileItems(scrollContext, useIcons: true), - ), - ); - }, - onSecondaryTapDown: (details) async { - Offset localPosition = details.globalPosition; - RelativeRect position = - RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); - await showMenu( - context: context, - position: position, - items: poster - .generateActions( - context, - ref, - exclude: excludeActions, - otherActions: otherActions, - onUserDataChanged: onUserDataChanged, - onDeleteSuccesFully: onItemRemoved, - onItemUpdated: onItemUpdated, - ) - .popupMenuItems(useIcons: true), - ); - }, - child: Card( - elevation: 6, - color: Theme.of(context).colorScheme.secondaryContainer, - shape: RoundedRectangleBorder( - side: BorderSide( - width: 1.0, - color: Colors.white.withValues(alpha: 0.10), - ), - borderRadius: posterRadius, - ), - child: Stack( - fit: StackFit.expand, - children: [ - FladderImage( - image: primaryPosters - ? poster.images?.primary - : poster.getPosters?.primary ?? poster.getPosters?.backDrop?.lastOrNull, - placeHolder: PosterPlaceholder(item: poster), - ), - if (poster.userData.progress > 0 && poster.type == FladderItemType.book) - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: padding, - child: Card( - child: Padding( - padding: const EdgeInsets.all(5.5), - child: Text( - context.localized.page((poster as BookModel).currentPage), - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - fontSize: 12, - ), - ), - ), - ), - ), - ), - if (selected == true) - Container( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.15), - border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary), - borderRadius: posterRadius, - ), - clipBehavior: Clip.hardEdge, - child: Stack( - alignment: Alignment.topCenter, - children: [ - Container( - color: Theme.of(context).colorScheme.primary, - width: double.infinity, - child: Padding( - padding: const EdgeInsets.all(2), - child: Text( - poster.name, - maxLines: 2, - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith(color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold), - ), - ), - ) - ], - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (poster.userData.isFavourite) - const Row( - children: [ - StatusCard( - color: Colors.red, - child: Icon( - IconsaxPlusBold.heart, - size: 21, - color: Colors.red, - ), - ), - ], - ), - if ((poster.userData.progress > 0 && poster.userData.progress < 100) && - poster.type != FladderItemType.book) ...{ - const SizedBox( - height: 4, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3).copyWith(bottom: 3).add(padding), - child: Card( - color: Colors.transparent, - elevation: 3, - shadowColor: Colors.transparent, - child: LinearProgressIndicator( - minHeight: 7.5, - backgroundColor: Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.5), - value: poster.userData.progress / 100, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - }, - ], - ), - ), - if (inlineTitle) - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - poster.title.maxLength(limitTo: 25), - style: - Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold), - ), - ), - ), - if ((poster.unPlayedItemCount != null && poster is SeriesModel) || - (poster.playAble && !poster.unWatched && poster is! PhotoAlbumModel)) - IgnorePointer( - child: Align( - alignment: Alignment.topRight, - child: StatusCard( - color: Theme.of(context).colorScheme.primary, - useFittedBox: poster.unPlayedItemCount != 0, - child: Padding( - padding: const EdgeInsets.all(6), - child: poster.unPlayedItemCount != 0 - ? Container( - constraints: const BoxConstraints(minWidth: 16), - child: Text( - poster.userData.unPlayedItemCount.toString(), - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - overflow: TextOverflow.visible, - fontSize: 14, - ), - ), - ) - : Icon( - Icons.check_rounded, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - ), - if (poster.overview.runTime != null && - ((poster is PhotoModel) && (poster as PhotoModel).internalType == FladderItemType.video)) ...{ - Align( - alignment: Alignment.topRight, - child: Padding( - padding: padding, - child: Card( - elevation: 5, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - poster.overview.runTime.humanizeSmall ?? "", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(width: 2), - Icon( - Icons.play_arrow_rounded, - color: Theme.of(context).colorScheme.onSurface, - ), - ], - ), - ), - ), - ), - ) - }, - ], + onLongPress: () => _showBottomSheet(context, ref), + onSecondaryTapDown: (details) => _showContextMenu(context, ref, details.globalPosition), + child: Container( + color: Theme.of(context).cardColor, + child: FladderImage( + image: primaryPosters + ? poster.images?.primary + : poster.getPosters?.primary ?? poster.getPosters?.backDrop?.lastOrNull, + placeHolder: PosterPlaceholder(item: poster), ), ), overlays: [ - //Poster Button + if (poster.userData.progress > 0 && poster.type == FladderItemType.book) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: padding, + child: Card( + child: Padding( + padding: const EdgeInsets.all(5.5), + child: Text( + context.localized.page((poster as BookModel).currentPage), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + fontSize: 12, + ), + ), + ), + ), + ), + ), + if (selected == true) + Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.15), + border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary), + borderRadius: radius, + ), + clipBehavior: Clip.hardEdge, + child: Stack( + alignment: Alignment.topCenter, + children: [ + Container( + color: Theme.of(context).colorScheme.primary, + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(2), + child: Text( + poster.name, + maxLines: 2, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith(color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold), + ), + ), + ) + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (poster.userData.isFavourite) + const Row( + children: [ + StatusCard( + color: Colors.red, + child: Icon( + IconsaxPlusBold.heart, + size: 21, + color: Colors.red, + ), + ), + ], + ), + if ((poster.userData.progress > 0 && poster.userData.progress < 100) && + poster.type != FladderItemType.book) ...{ + const SizedBox( + height: 4, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3).copyWith(bottom: 3).add(padding), + child: Card( + color: Colors.transparent, + elevation: 3, + shadowColor: Colors.transparent, + child: LinearProgressIndicator( + minHeight: 7.5, + backgroundColor: Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.5), + value: poster.userData.progress / 100, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + }, + ], + ), + ), + if (inlineTitle) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + poster.title.maxLength(limitTo: 25), + style: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ), + if (poster is! PhotoAlbumModel && (poster.unPlayedItemCount != null && poster is SeriesModel) || + (poster.playAble && !poster.unWatched)) + IgnorePointer( + child: Align( + alignment: Alignment.topRight, + child: StatusCard( + color: Theme.of(context).colorScheme.primary, + useFittedBox: poster.unPlayedItemCount != 0, + child: Padding( + padding: const EdgeInsets.all(6), + child: poster.unPlayedItemCount != 0 + ? Container( + constraints: const BoxConstraints(minWidth: 16), + child: Text( + poster.userData.unPlayedItemCount.toString(), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + overflow: TextOverflow.visible, + fontSize: 14, + ), + ), + ) + : Icon( + Icons.check_rounded, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ), + if (poster.overview.runTime != null && + ((poster is PhotoModel) && (poster as PhotoModel).internalType == FladderItemType.video)) ...{ + Align( + alignment: Alignment.topRight, + child: Padding( + padding: padding, + child: Card( + elevation: 5, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + poster.overview.runTime.humanizeSmall ?? "", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(width: 2), + Icon( + Icons.play_arrow_rounded, + color: Theme.of(context).colorScheme.onSurface, + ), + ], + ), + ), + ), + ), + ) + }, + ], + focusedOverlays: [ if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ // Play Button if (poster.playAble) @@ -352,4 +301,45 @@ class PosterImage extends ConsumerWidget { ), ); } + + void _showBottomSheet(BuildContext context, WidgetRef ref) { + showBottomSheetPill( + context: context, + item: poster, + content: (scrollContext, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: poster + .generateActions( + context, + ref, + exclude: excludeActions, + otherActions: otherActions, + onUserDataChanged: onUserDataChanged, + onDeleteSuccesFully: onItemRemoved, + onItemUpdated: onItemUpdated, + ) + .listTileItems(scrollContext, useIcons: true), + ), + ); + } + + Future _showContextMenu(BuildContext context, WidgetRef ref, Offset globalPos) async { + final position = RelativeRect.fromLTRB(globalPos.dx, globalPos.dy, globalPos.dx, globalPos.dy); + await showMenu( + context: context, + position: position, + items: poster + .generateActions( + context, + ref, + exclude: excludeActions, + otherActions: otherActions, + onUserDataChanged: onUserDataChanged, + onDeleteSuccesFully: onItemRemoved, + onItemUpdated: onItemUpdated, + ) + .popupMenuItems(useIcons: true), + ); + } } diff --git a/lib/screens/shared/media/episode_posters.dart b/lib/screens/shared/media/episode_posters.dart index 626e6c6..45781d4 100644 --- a/lib/screens/shared/media/episode_posters.dart +++ b/lib/screens/shared/media/episode_posters.dart @@ -112,9 +112,7 @@ class _EpisodePosterState extends ConsumerState { return ListView( shrinkWrap: true, controller: scrollController, - children: [ - ...episode.generateActions(context, ref).listTileItems(context, useIcons: true), - ], + children: episode.generateActions(context, ref).listTileItems(context, useIcons: true).toList(), ); }, ); @@ -165,116 +163,109 @@ class EpisodePoster extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Flexible( - child: Card( - child: Stack( - fit: StackFit.expand, - children: [ - FocusButton( - onTap: onTap, - onLongPress: onLongPress, - onSecondaryTapDown: (details) async { - Offset localPosition = details.globalPosition; - RelativeRect position = - RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); - - await showMenu( - context: context, position: position, items: actions.popupMenuItems(useIcons: true)); - }, - child: Hero( - tag: heroTag ?? UniqueKey(), - child: FladderImage( - image: !episodeAvailable ? episode.parentImages?.primary : episode.images?.primary, - placeHolder: placeHolder, - blurOnly: !episodeAvailable - ? true - : ref.watch(clientSettingsProvider.select((value) => value.blurUpcomingEpisodes)) - ? blur - : false, - decodeHeight: 250, + child: FocusButton( + onTap: onTap, + onLongPress: onLongPress, + onSecondaryTapDown: (details) async { + Offset localPosition = details.globalPosition; + RelativeRect position = + RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); + await showMenu(context: context, position: position, items: actions.popupMenuItems(useIcons: true)); + }, + child: Hero( + tag: heroTag ?? UniqueKey(), + child: FladderImage( + image: !episodeAvailable ? episode.parentImages?.primary : episode.images?.primary, + placeHolder: placeHolder, + blurOnly: !episodeAvailable + ? true + : ref.watch(clientSettingsProvider.select((value) => value.blurUpcomingEpisodes)) + ? blur + : false, + decodeHeight: 250, + ), + ), + overlays: [ + if (!episodeAvailable) + Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Card( + color: episode.status.color, + elevation: 3, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + episode.status.label(context), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), ), ), - overlays: [ - if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer && actions.isNotEmpty) - ExcludeFocus( - child: Align( - alignment: Alignment.bottomRight, - child: PopupMenuButton( - tooltip: context.localized.options, - icon: const Icon( - Icons.more_vert, - color: Colors.white, - ), - itemBuilder: (context) => actions.popupMenuItems(useIcons: true), - ), + ), + Align( + alignment: Alignment.topRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + switch (syncedDetails) { + AsyncValue(:final value) => Builder( + builder: (context) { + if (value == null) { + return const SizedBox.shrink(); + } + return StatusCard( + child: SyncButton(item: episode, syncedItem: value), + ); + }, + ), + }, + if (episode.userData.isFavourite) + const StatusCard( + color: Colors.red, + child: Icon( + Icons.favorite_rounded, + ), + ), + if (episode.userData.played) + StatusCard( + color: Theme.of(context).colorScheme.primary, + child: const Icon( + Icons.check_rounded, ), ), ], ), - if (!episodeAvailable) - Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Card( - color: episode.status.color, - elevation: 3, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - episode.status.label(context), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ), + ), + if ((episode.userData.progress) > 0) Align( - alignment: Alignment.topRight, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - switch (syncedDetails) { - AsyncValue(:final value) => Builder( - builder: (context) { - if (value == null) { - return const SizedBox.shrink(); - } - return StatusCard( - child: SyncButton(item: episode, syncedItem: value), - ); - }, - ), - }, - if (episode.userData.isFavourite) - const StatusCard( - color: Colors.red, - child: Icon( - Icons.favorite_rounded, - ), - ), - if (episode.userData.played) - StatusCard( - color: Theme.of(context).colorScheme.primary, - child: const Icon( - Icons.check_rounded, - ), - ), - ], + alignment: Alignment.bottomCenter, + child: LinearProgressIndicator( + minHeight: 6, + backgroundColor: Colors.black.withValues(alpha: 0.75), + value: episode.userData.progress / 100, ), ), - if ((episode.userData.progress) > 0) - Align( - alignment: Alignment.bottomCenter, - child: LinearProgressIndicator( - minHeight: 6, - backgroundColor: Colors.black.withValues(alpha: 0.75), - value: episode.userData.progress / 100, + ], + focusedOverlays: [ + if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer && actions.isNotEmpty) + ExcludeFocus( + child: Align( + alignment: Alignment.bottomRight, + child: PopupMenuButton( + tooltip: context.localized.options, + icon: const Icon( + Icons.more_vert, + color: Colors.white, + ), + itemBuilder: (context) => actions.popupMenuItems(useIcons: true), ), ), - ], - ), + ), + ], ), ), if (showLabel) ...{ diff --git a/lib/screens/shared/media/people_row.dart b/lib/screens/shared/media/people_row.dart index 4abae7e..4b2ec42 100644 --- a/lib/screens/shared/media/people_row.dart +++ b/lib/screens/shared/media/people_row.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:animations/animations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/screens/details_screens/person_detail_screen.dart'; +import 'package:fladder/theme.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/focus_provider.dart'; @@ -26,10 +26,11 @@ class PeopleRow extends ConsumerWidget { child: SizedBox( height: 75, width: 75, - child: Card( - elevation: 5, - shadowColor: Colors.transparent, - color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.50), + child: Container( + decoration: BoxDecoration( + borderRadius: FladderTheme.smallShape.borderRadius, + color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.50), + ), child: Center( child: Text( name.getInitials(), @@ -55,26 +56,24 @@ class PeopleRow extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Flexible( - child: OpenContainer( - closedColor: Colors.transparent, - closedElevation: 5, - openElevation: 0, - closedShape: const RoundedRectangleBorder(), - transitionType: ContainerTransitionType.fadeThrough, - openColor: Colors.transparent, - tappable: false, - closedBuilder: (context, action) => Card( - child: FocusButton( - onTap: () => action(), - child: FladderImage( - image: person.image, - placeHolder: placeHolder(person.name), - fit: BoxFit.cover, + child: Container( + decoration: BoxDecoration( + borderRadius: FladderTheme.smallShape.borderRadius, + color: Theme.of(context).cardTheme.color?.withValues(alpha: 0.1), + ), + child: FocusButton( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => PersonDetailScreen( + person: person, + ), ), ), - ), - openBuilder: (context, action) => PersonDetailScreen( - person: person, + child: FladderImage( + image: person.image, + placeHolder: placeHolder(person.name), + fit: BoxFit.cover, + ), ), ), ), diff --git a/lib/screens/shared/media/season_row.dart b/lib/screens/shared/media/season_row.dart index d402d5d..b48e407 100644 --- a/lib/screens/shared/media/season_row.dart +++ b/lib/screens/shared/media/season_row.dart @@ -80,20 +80,42 @@ class SeasonPoster extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( - child: Card( - child: Stack( - children: [ - Positioned.fill( - child: Hero( - tag: myKey, - child: FladderImage( - image: season.getPosters?.primary ?? - season.parentImages?.backDrop?.firstOrNull ?? - season.parentImages?.primary, - placeHolder: placeHolder(season.name), - ), - ), - ), + child: Hero( + tag: myKey, + child: FocusButton( + child: FladderImage( + image: season.getPosters?.primary ?? + season.parentImages?.backDrop?.firstOrNull ?? + season.parentImages?.primary, + placeHolder: placeHolder(season.name), + ), + onSecondaryTapDown: (details) async { + Offset localPosition = details.globalPosition; + RelativeRect position = + RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); + await showMenu( + context: context, + position: position, + items: season.generateActions(context, ref).popupMenuItems(useIcons: true)); + }, + onTap: () async { + await season.navigateTo(context, ref: ref, tag: myKey); + if (!context.mounted) return; + context.refreshData(); + }, + onLongPress: AdaptiveLayout.inputDeviceOf(context) == InputDevice.touch + ? () { + showBottomSheetPill( + context: context, + content: (context, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: season.generateActions(context, ref).listTileItems(context, useIcons: true), + ), + ); + } + : null, + overlays: [ if (season.images?.primary == null) Align( alignment: Alignment.topLeft, @@ -141,50 +163,19 @@ class SeasonPoster extends ConsumerWidget { ], ), ), - Positioned.fill( - child: FocusButton( - onSecondaryTapDown: (details) async { - Offset localPosition = details.globalPosition; - RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); - await showMenu( - context: context, - position: position, - items: season.generateActions(context, ref).popupMenuItems(useIcons: true)); - }, - onTap: () async { - await season.navigateTo(context, ref: ref, tag: myKey); - if (!context.mounted) return; - context.refreshData(); - }, - onLongPress: AdaptiveLayout.inputDeviceOf(context) == InputDevice.touch - ? () { - showBottomSheetPill( - context: context, - content: (context, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: season.generateActions(context, ref).listTileItems(context, useIcons: true), - ), - ); - } - : null, - overlays: [ - if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) - ExcludeFocus( - child: Align( - alignment: Alignment.bottomRight, - child: PopupMenuButton( - tooltip: context.localized.options, - icon: const Icon(Icons.more_vert, color: Colors.white), - itemBuilder: (context) => - season.generateActions(context, ref).popupMenuItems(useIcons: true), - ), - ), - ), - ], + ], + focusedOverlays: [ + if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) + ExcludeFocus( + child: Align( + alignment: Alignment.bottomRight, + child: PopupMenuButton( + tooltip: context.localized.options, + icon: const Icon(Icons.more_vert, color: Colors.white), + itemBuilder: (context) => season.generateActions(context, ref).popupMenuItems(useIcons: true), + ), + ), ), - ), ], ), ), diff --git a/lib/util/focus_provider.dart b/lib/util/focus_provider.dart index 15322ef..4e27ba4 100644 --- a/lib/util/focus_provider.dart +++ b/lib/util/focus_provider.dart @@ -46,6 +46,7 @@ class FocusButton extends StatefulWidget { final Widget? child; final bool autoFocus; final FocusNode? focusNode; + final List focusedOverlays; final List overlays; final Function()? onTap; final Function()? onLongPress; @@ -58,6 +59,7 @@ class FocusButton extends StatefulWidget { this.child, this.autoFocus = false, this.focusNode, + this.focusedOverlays = const [], this.overlays = const [], this.onTap, this.onLongPress, @@ -74,7 +76,7 @@ class FocusButton extends StatefulWidget { class FocusButtonState extends State { late FocusNode focusNode = widget.focusNode ?? FocusNode(); - ValueNotifier onHover = ValueNotifier(false); + ValueNotifier onHover = ValueNotifier(false); Timer? _longPressTimer; bool _longPressTriggered = false; bool _keyDownActive = false; @@ -153,47 +155,49 @@ class FocusButtonState extends State { }, onKeyEvent: _handleKey, child: ExcludeFocus( - child: Stack( - children: [ - FlatButton( - onTap: widget.onTap, - onSecondaryTapDown: widget.onSecondaryTapDown, - onLongPress: widget.onLongPress, - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: widget.borderRadius ?? FladderTheme.smallShape.borderRadius, - ), - child: widget.child, + child: ValueListenableBuilder( + valueListenable: onHover, + builder: (context, value, child) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: widget.borderRadius ?? FladderTheme.smallShape.borderRadius, ), - ), - Positioned.fill( - child: ValueListenableBuilder( - valueListenable: onHover, - builder: (context, value, child) => AnimatedOpacity( - opacity: value ? 1 : 0, - duration: const Duration(milliseconds: 125), - child: Stack( - children: [ - IgnorePointer( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surfaceContainerLowest - .withValues(alpha: widget.darkOverlay ? 0.35 : 0), - border: Border.all(width: 3, color: Theme.of(context).colorScheme.onPrimaryContainer), - borderRadius: widget.borderRadius ?? FladderTheme.smallShape.borderRadius, - ), - ), + foregroundDecoration: BoxDecoration( + borderRadius: widget.borderRadius ?? FladderTheme.smallShape.borderRadius, + color: widget.darkOverlay + ? Theme.of(context).colorScheme.primaryFixedDim.withValues(alpha: value ? 0.10 : 0.0) + : null, + border: Border.all( + width: value ? 3.5 : 2, + color: value ? Theme.of(context).colorScheme.primary : Colors.white.withAlpha(15), + ), + ), + child: FlatButton( + onTap: widget.onTap, + onSecondaryTapDown: widget.onSecondaryTapDown, + onLongPress: widget.onLongPress, + child: Stack( + children: [ + if (widget.child != null) widget.child!, + ], + ), + overlays: [ + if (widget.overlays.isNotEmpty) ...widget.overlays, + if (widget.focusedOverlays.isNotEmpty) + AnimatedOpacity( + opacity: value ? 1 : 0, + duration: const Duration(milliseconds: 250), + child: Stack( + children: [...widget.focusedOverlays], ), - ...widget.overlays, - ], - ), - ), + ), + ], ), - ), - ], + ); + }, ), ), ),