diff --git a/lib/screens/settings/settings_list_tile.dart b/lib/screens/settings/settings_list_tile.dart index 9d9bfea..66534d7 100644 --- a/lib/screens/settings/settings_list_tile.dart +++ b/lib/screens/settings/settings_list_tile.dart @@ -72,54 +72,51 @@ class SettingsListTile extends StatelessWidget { constraints: const BoxConstraints( minHeight: 50, ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - DefaultTextStyle.merge( - style: TextStyle( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + DefaultTextStyle.merge( + style: TextStyle( + color: contentColor ?? Theme.of(context).colorScheme.onSurface, + ), + child: IconTheme( + data: IconThemeData( color: contentColor ?? Theme.of(context).colorScheme.onSurface, ), - child: IconTheme( - data: IconThemeData( - color: contentColor ?? Theme.of(context).colorScheme.onSurface, - ), - child: leadingWidget, - ), + child: leadingWidget, ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Material( + color: Colors.transparent, + textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(color: contentColor), + child: label, + ), + if (subLabel != null) Material( color: Colors.transparent, - textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(color: contentColor), - child: label, + textStyle: Theme.of(context).textTheme.labelLarge?.copyWith( + color: + (contentColor ?? Theme.of(context).colorScheme.onSurface).withValues(alpha: 0.65), + ), + child: subLabel, ), - if (subLabel != null) - Opacity( - opacity: 0.65, - child: Material( - color: Colors.transparent, - textStyle: Theme.of(context).textTheme.labelLarge?.copyWith(color: contentColor), - child: subLabel, - ), - ), - ], - ), + ], ), - if (trailing != null) - ExcludeFocusTraversal( - excluding: onTap != null, - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: trailing, - ), - ) - ], - ), + ), + if (trailing != null) + ExcludeFocusTraversal( + excluding: onTap != null, + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: trailing, + ), + ) + ], ), ), ), diff --git a/lib/screens/shared/detail_scaffold.dart b/lib/screens/shared/detail_scaffold.dart index fea60c9..a807265 100644 --- a/lib/screens/shared/detail_scaffold.dart +++ b/lib/screens/shared/detail_scaffold.dart @@ -104,16 +104,30 @@ class _DetailScaffoldState extends ConsumerState { final minHeight = 450.0.clamp(0, size.height).toDouble(); final maxHeight = size.height - 10; final sideBarPadding = AdaptiveLayout.of(context).sideBarWidth; + final newColorScheme = dominantColor != null + ? ColorScheme.fromSeed( + seedColor: dominantColor!, + brightness: Theme.brightnessOf(context), + dynamicSchemeVariant: ref.watch(clientSettingsProvider.select((value) => value.schemeVariant)), + ) + : null; + final amoledBlack = ref.watch(clientSettingsProvider.select((value) => value.amoledBlack)); + final amoledOverwrite = amoledBlack ? Colors.black : null; return Theme( - data: Theme.of(context).copyWith( - colorScheme: dominantColor != null - ? ColorScheme.fromSeed( - seedColor: dominantColor!, - brightness: Theme.brightnessOf(context), - dynamicSchemeVariant: ref.watch(clientSettingsProvider.select((value) => value.schemeVariant)), - ) - : null, - ), + data: Theme.of(context) + .copyWith( + colorScheme: newColorScheme, + ) + .copyWith( + scaffoldBackgroundColor: amoledOverwrite, + cardColor: amoledOverwrite, + canvasColor: amoledOverwrite, + colorScheme: newColorScheme?.copyWith( + surface: amoledOverwrite, + surfaceContainerHighest: amoledOverwrite, + surfaceContainerLow: amoledOverwrite, + ), + ), child: Builder(builder: (context) { return PullToRefresh( onRefresh: () async { diff --git a/lib/screens/shared/media/carousel_banner.dart b/lib/screens/shared/media/carousel_banner.dart index ed07259..5878270 100644 --- a/lib/screens/shared/media/carousel_banner.dart +++ b/lib/screens/shared/media/carousel_banner.dart @@ -5,13 +5,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/banner_play_button.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; +import 'package:fladder/util/focus_provider.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/themes_data.dart'; +import 'package:fladder/widgets/shared/ensure_visible.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; @@ -64,148 +65,151 @@ class _CarouselBannerState extends ConsumerState { itemExtent: itemExtent, 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( - 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, - ], - ), + (index, item) => LayoutBuilder( + builder: (context, constraints) { + final opacity = (constraints.maxWidth / maxExtent); + return FocusButton( + onTap: () => widget.items[index].navigateTo(context), + onFocusChanged: (hover) { + context.ensureVisible(); + }, + onLongPress: AdaptiveLayout.inputDeviceOf(context) == 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.inputDeviceOf(context) == InputDevice.touch + ? null + : (details) async { + Offset localPosition = details.globalPosition; + RelativeRect position = RelativeRect.fromLTRB( + localPosition.dx, 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), + ); + }, + child: Stack( + children: [ + FladderImage(image: item.bannerImage), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomLeft, + end: Alignment.topCenter, + colors: [ + ThemesData.of(context) + .dark + .colorScheme + .primaryContainer + .withValues(alpha: opacity.clamp(0, 1)), + 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)), ), - ), - ), - FlatButton( - onTap: () => widget.items[index].navigateTo(context), - onLongPress: AdaptiveLayout.inputDeviceOf(context) == 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), + 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), ), - ); - }, - onSecondaryTapDown: AdaptiveLayout.inputDeviceOf(context) == InputDevice.touch - ? null - : (details) async { - Offset localPosition = details.globalPosition; - RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx, 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), - ); - }, - ), - ExcludeFocus( - child: BannerPlayButton(item: widget.items[index]), - ), - IgnorePointer( - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.white.withValues(alpha: 0.1), - width: 1.0, + 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)), ), - borderRadius: border), - ), + ), + ), + ExcludeFocus( + child: 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 (AdaptiveLayout.inputDeviceOf(context) == 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(IconsaxPlusLinear.arrow_left_1), - ), - IconButton.filledTonal( - onPressed: () { - final currentPos = carouselController.position; - carouselController.animateTo(currentPos.pixels + itemExtent, - curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250)); - }, - icon: const Icon(IconsaxPlusLinear.arrow_right_3), - ), - ], + ExcludeFocus( + child: 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(IconsaxPlusLinear.arrow_left_1), + ), + IconButton.filledTonal( + onPressed: () { + final currentPos = carouselController.position; + carouselController.animateTo(currentPos.pixels + itemExtent, + curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250)); + }, + icon: const Icon(IconsaxPlusLinear.arrow_right_3), + ), + ], + ), ), ), ), diff --git a/lib/screens/shared/media/components/next_up_episode.dart b/lib/screens/shared/media/components/next_up_episode.dart index 1fbc010..bdb5dea 100644 --- a/lib/screens/shared/media/components/next_up_episode.dart +++ b/lib/screens/shared/media/components/next_up_episode.dart @@ -20,6 +20,7 @@ class NextUpEpisode extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final alreadyPlayed = nextEpisode.userData.played; final episodeSummary = nextEpisode.overview.summary.maxLength(limitTo: 250); + final style = Theme.of(context).textTheme.titleMedium; return Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, @@ -27,11 +28,10 @@ class NextUpEpisode extends ConsumerWidget { StickyHeaderText( label: alreadyPlayed ? context.localized.reWatch : context.localized.nextUp, ), - Opacity( - opacity: 0.75, - child: SelectableText( - nextEpisode.seasonEpisodeLabelFull(context), - style: Theme.of(context).textTheme.titleMedium, + SelectableText( + nextEpisode.seasonEpisodeLabelFull(context), + style: style?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.75), ), ), SelectableText( diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index 4c5c01b..2d6b540 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -84,205 +84,269 @@ class _PosterImageState extends ConsumerState { return Hero( tag: tag, - child: Card( - elevation: 6, - color: Theme.of(context).colorScheme.secondaryContainer, - shape: RoundedRectangleBorder( - side: BorderSide( - width: 1.0, - color: Colors.white.withValues(alpha: 0.10), + child: FocusButton( + onTap: () => pressedWidget(context), + onFocusChanged: widget.onFocusChanged, + onLongPress: () { + showBottomSheetPill( + context: context, + item: widget.poster, + content: (scrollContext, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: widget.poster + .generateActions( + context, + ref, + exclude: widget.excludeActions, + otherActions: widget.otherActions, + onUserDataChanged: widget.onUserDataChanged, + onDeleteSuccesFully: widget.onItemRemoved, + onItemUpdated: widget.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: widget.poster + .generateActions( + context, + ref, + exclude: widget.excludeActions, + otherActions: widget.otherActions, + onUserDataChanged: widget.onUserDataChanged, + onDeleteSuccesFully: widget.onItemRemoved, + onItemUpdated: widget.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, ), - borderRadius: posterRadius, - ), - child: Stack( - fit: StackFit.expand, - children: [ - FladderImage( - image: widget.primaryPosters - ? widget.poster.images?.primary - : widget.poster.getPosters?.primary ?? widget.poster.getPosters?.backDrop?.lastOrNull, - placeHolder: PosterPlaceholder(item: widget.poster), - ), - if (poster.userData.progress > 0 && widget.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((widget.poster as BookModel).currentPage), - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - fontSize: 12, - ), - ), - ), - ), - ), + child: Stack( + fit: StackFit.expand, + children: [ + FladderImage( + image: widget.primaryPosters + ? widget.poster.images?.primary + : widget.poster.getPosters?.primary ?? widget.poster.getPosters?.backDrop?.lastOrNull, + placeHolder: PosterPlaceholder(item: widget.poster), ), - if (widget.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, + if (poster.userData.progress > 0 && widget.poster.type == FladderItemType.book) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: padding, + child: Card( child: Padding( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(5.5), child: Text( - widget.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 (widget.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) && - widget.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 (widget.inlineTitle) - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - widget.poster.title.maxLength(limitTo: 25), - style: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold), - ), - ), - ), - if ((widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel) || - (widget.poster.playAble && !widget.poster.unWatched)) - IgnorePointer( - child: Align( - alignment: Alignment.topRight, - child: StatusCard( - color: Theme.of(context).colorScheme.primary, - useFittedBox: widget.poster.unPlayedItemCount != 0, - child: Padding( - padding: const EdgeInsets.all(6), - child: widget.poster.unPlayedItemCount != 0 - ? Container( - constraints: const BoxConstraints(minWidth: 16), - child: Text( - widget.poster.userData.unPlayedItemCount.toString(), - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - overflow: TextOverflow.visible, - fontSize: 14, - ), + context.localized.page((widget.poster as BookModel).currentPage), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + fontSize: 12, ), - ) - : Icon( - Icons.check_rounded, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), + ), + ), ), ), ), - ), - if (widget.poster.overview.runTime != null && - ((widget.poster is PhotoModel) && - (widget.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( - widget.poster.overview.runTime.humanizeSmall ?? "", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), + if (widget.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( + widget.poster.name, + maxLines: 2, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith(color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold), ), - const SizedBox(width: 2), - Icon( - Icons.play_arrow_rounded, - color: Theme.of(context).colorScheme.onSurface, + ), + ) + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.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) && + widget.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 (widget.inlineTitle) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + widget.poster.title.maxLength(limitTo: 25), + style: + Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold), ), ), ), - ) - }, - FocusButton( - onTap: () => pressedWidget(context), - onFocusChanged: widget.onFocusChanged, - onLongPress: () { - showBottomSheetPill( - context: context, - item: widget.poster, - content: (scrollContext, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: widget.poster + if ((widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel) || + (widget.poster.playAble && !widget.poster.unWatched)) + IgnorePointer( + child: Align( + alignment: Alignment.topRight, + child: StatusCard( + color: Theme.of(context).colorScheme.primary, + useFittedBox: widget.poster.unPlayedItemCount != 0, + child: Padding( + padding: const EdgeInsets.all(6), + child: widget.poster.unPlayedItemCount != 0 + ? Container( + constraints: const BoxConstraints(minWidth: 16), + child: Text( + widget.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 (widget.poster.overview.runTime != null && + ((widget.poster is PhotoModel) && + (widget.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( + widget.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, + ), + ], + ), + ), + ), + ), + ) + }, + ], + ), + ), + overlays: [ + //Poster Button + if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ + // Play Button + if (widget.poster.playAble) + Align( + alignment: Alignment.center, + child: IconButton.filledTonal( + onPressed: () => widget.playVideo?.call(false), + icon: const Icon( + IconsaxPlusBold.play, + size: 32, + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + PopupMenuButton( + tooltip: "Options", + icon: const Icon( + Icons.more_vert, + color: Colors.white, + ), + itemBuilder: (context) => widget.poster .generateActions( context, ref, @@ -292,76 +356,13 @@ class _PosterImageState extends ConsumerState { onDeleteSuccesFully: widget.onItemRemoved, onItemUpdated: widget.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: widget.poster - .generateActions( - context, - ref, - exclude: widget.excludeActions, - otherActions: widget.otherActions, - onUserDataChanged: widget.onUserDataChanged, - onDeleteSuccesFully: widget.onItemRemoved, - onItemUpdated: widget.onItemUpdated, - ) - .popupMenuItems(useIcons: true), - ); - }, - overlays: [ - //Poster Button - if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ - // Play Button - if (widget.poster.playAble) - Align( - alignment: Alignment.center, - child: IconButton.filledTonal( - onPressed: () => widget.playVideo?.call(false), - icon: const Icon( - IconsaxPlusBold.play, - size: 32, - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - PopupMenuButton( - tooltip: "Options", - icon: const Icon( - Icons.more_vert, - color: Colors.white, - ), - itemBuilder: (context) => widget.poster - .generateActions( - context, - ref, - exclude: widget.excludeActions, - otherActions: widget.otherActions, - onUserDataChanged: widget.onUserDataChanged, - onDeleteSuccesFully: widget.onItemRemoved, - onItemUpdated: widget.onItemUpdated, - ) - .popupMenuItems(useIcons: true), - ), - ], - ), + .popupMenuItems(useIcons: true), ), ], - ], + ), ), ], - ), + ], ), ); } diff --git a/lib/screens/shared/media/components/poster_placeholder.dart b/lib/screens/shared/media/components/poster_placeholder.dart index 9eb1e2c..b4f288d 100644 --- a/lib/screens/shared/media/components/poster_placeholder.dart +++ b/lib/screens/shared/media/components/poster_placeholder.dart @@ -8,6 +8,7 @@ class PosterPlaceholder extends StatelessWidget { @override Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.75); return Stack( alignment: Alignment.center, children: [ @@ -15,7 +16,10 @@ class PosterPlaceholder extends StatelessWidget { alignment: Alignment.topRight, child: Padding( padding: const EdgeInsets.all(12.0), - child: Opacity(opacity: 0.5, child: Icon(item.type.icon)), + child: Icon( + item.type.icon, + color: color.withValues(alpha: 0.5), + ), ), ), Padding( @@ -34,15 +38,14 @@ class PosterPlaceholder extends StatelessWidget { softWrap: true, ), if (item.label(context) != null) ...[ - Opacity( - opacity: 0.75, - child: Text( - item.label(context)!, - maxLines: 2, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleSmall, - softWrap: true, - ), + Text( + item.label(context)!, + maxLines: 2, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: color.withValues(alpha: 0.75), + ), + softWrap: true, ), ], ], diff --git a/lib/screens/shared/media/episode_details_list.dart b/lib/screens/shared/media/episode_details_list.dart index c1d94b3..7be3756 100644 --- a/lib/screens/shared/media/episode_details_list.dart +++ b/lib/screens/shared/media/episode_details_list.dart @@ -38,6 +38,9 @@ class EpisodeDetailsList extends ConsumerWidget { ((AdaptiveLayout.poster(context).gridRatio * 2) * ref.watch(clientSettingsProvider.select((value) => value.posterSize))); final decimals = size - size.toInt(); + final textStyle = Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65), + ); return AnimatedSwitcher( duration: const Duration(milliseconds: 250), child: switch (viewType) { @@ -73,20 +76,14 @@ class EpisodeDetailsList extends ConsumerWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - Opacity( - opacity: 0.65, - child: SelectableText( - episode.seasonEpisodeLabel(context), - style: Theme.of(context).textTheme.titleMedium, - ), + SelectableText( + episode.seasonEpisodeLabel(context), + style: textStyle, ), if (episode.overview.runTime != null) - Opacity( - opacity: 0.65, - child: SelectableText( - " - ${episode.overview.runTime!.humanize!}", - style: Theme.of(context).textTheme.titleMedium, - ), + SelectableText( + " - ${episode.overview.runTime!.humanize!}", + style: textStyle, ), ], ), diff --git a/lib/screens/shared/media/poster_list_item.dart b/lib/screens/shared/media/poster_list_item.dart index 1f609fc..7940343 100644 --- a/lib/screens/shared/media/poster_list_item.dart +++ b/lib/screens/shared/media/poster_list_item.dart @@ -149,13 +149,10 @@ class PosterListItem extends ConsumerWidget { overflow: TextOverflow.ellipsis, ), if ((poster.subText ?? poster.subTextShort(context))?.isNotEmpty == true) - Opacity( - opacity: 0.45, - child: Text( - poster.subText ?? poster.subTextShort(context) ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + Text( + poster.subText ?? poster.subTextShort(context) ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, ), Row( children: [ diff --git a/lib/screens/shared/media/poster_widget.dart b/lib/screens/shared/media/poster_widget.dart index 536fd24..e8f2318 100644 --- a/lib/screens/shared/media/poster_widget.dart +++ b/lib/screens/shared/media/poster_widget.dart @@ -96,10 +96,7 @@ class PosterWidget extends ConsumerWidget { children: [ if (subTitle != null) ...[ Flexible( - child: Opacity( - opacity: opacity, - child: subTitle!, - ), + child: subTitle!, ), ], if (poster.subText?.isNotEmpty ?? false) diff --git a/lib/screens/shared/outlined_text_field.dart b/lib/screens/shared/outlined_text_field.dart index f8593d0..2e3d3c7 100644 --- a/lib/screens/shared/outlined_text_field.dart +++ b/lib/screens/shared/outlined_text_field.dart @@ -200,7 +200,7 @@ class _OutlinedTextFieldState extends ConsumerState { child: KeyboardListener( focusNode: _wrapperFocus, onKeyEvent: (KeyEvent event) { - if (keyboardFocus) return; + if (keyboardFocus || AdaptiveLayout.inputDeviceOf(context) != InputDevice.dPad) return; if (event is KeyDownEvent && acceptKeys.contains(event.logicalKey)) { if (_textFocus.hasFocus) { _wrapperFocus.requestFocus(); diff --git a/lib/screens/shared/user_icon.dart b/lib/screens/shared/user_icon.dart index e42bcdd..6914a16 100644 --- a/lib/screens/shared/user_icon.dart +++ b/lib/screens/shared/user_icon.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/account_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/theme.dart'; import 'package:fladder/util/string_extensions.dart'; class UserIcon extends ConsumerWidget { @@ -18,7 +17,7 @@ class UserIcon extends ConsumerWidget { const UserIcon({ this.size = const Size(50, 50), this.labelStyle, - this.cornerRadius = 5, + this.cornerRadius = 16, this.onTap, this.onLongPress, required this.user, @@ -46,25 +45,23 @@ class UserIcon extends ConsumerWidget { tag: Key(user?.id ?? "empty-user-avatar"), child: AspectRatio( aspectRatio: 1, - child: Card( - elevation: 0, - surfaceTintColor: Colors.transparent, - color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(cornerRadius), + ), clipBehavior: Clip.hardEdge, child: SizedBox.fromSize( size: size, child: Stack( alignment: Alignment.center, + fit: StackFit.expand, children: [ - ClipRRect( - borderRadius: FladderTheme.smallShape.borderRadius, - child: CachedNetworkImage( - imageUrl: user?.avatar ?? "", - progressIndicatorBuilder: (context, url, progress) => placeHolder(), - errorWidget: (context, url, error) => placeHolder(), - memCacheHeight: 128, - fit: BoxFit.cover, - ), + CachedNetworkImage( + imageUrl: user?.avatar ?? "", + progressIndicatorBuilder: (context, url, progress) => placeHolder(), + errorWidget: (context, url, error) => placeHolder(), + memCacheHeight: 128, + fit: BoxFit.cover, ), FlatButton( onTap: onTap, diff --git a/lib/util/focus_provider.dart b/lib/util/focus_provider.dart index 386f942..d2daf22 100644 --- a/lib/util/focus_provider.dart +++ b/lib/util/focus_provider.dart @@ -175,8 +175,8 @@ class FocusButtonState extends State { decoration: BoxDecoration( color: Theme.of(context) .colorScheme - .primaryContainer - .withValues(alpha: widget.darkOverlay ? 0.1 : 0), + .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, ), diff --git a/lib/widgets/navigation_scaffold/components/settings_user_icon.dart b/lib/widgets/navigation_scaffold/components/settings_user_icon.dart index 055908a..75beae9 100644 --- a/lib/widgets/navigation_scaffold/components/settings_user_icon.dart +++ b/lib/widgets/navigation_scaffold/components/settings_user_icon.dart @@ -38,7 +38,7 @@ class SettingsUserIcon extends ConsumerWidget { children: [ UserIcon( user: user, - cornerRadius: 200, + cornerRadius: 8, ), if (hasNewUpdate) Transform.translate( diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart index 94ae4dd..4e0f8cb 100644 --- a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -103,16 +103,16 @@ class _SideNavigationBarState extends ConsumerState { if (expandedSideBar) ...[ Expanded(child: Text(context.localized.navigation)), ], - Opacity( - opacity: largeBar && expandedSideBar ? 0.65 : 1.0, - child: IconButton( - onPressed: !largeBar - ? () => widget.scaffoldKey.currentState?.openDrawer() - : () => setState(() => expandedSideBar = !expandedSideBar), - icon: Icon( - largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu, - ), + IconButton( + onPressed: !largeBar + ? () => widget.scaffoldKey.currentState?.openDrawer() + : () => setState(() => expandedSideBar = !expandedSideBar), + icon: Icon( + largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu, ), + color: Theme.of(context).colorScheme.onSurface.withValues( + alpha: largeBar && expandedSideBar ? 0.65 : 1, + ), ) ], ), diff --git a/lib/widgets/shared/clickable_text.dart b/lib/widgets/shared/clickable_text.dart index 2662bd0..9556854 100644 --- a/lib/widgets/shared/clickable_text.dart +++ b/lib/widgets/shared/clickable_text.dart @@ -28,18 +28,19 @@ class _ClickableTextState extends ConsumerState { bool hovering = false; Widget _textWidget(bool showDecoration) { - return Opacity( - opacity: widget.opacity, - child: Text( - widget.text, - maxLines: widget.maxLines, - overflow: widget.overflow, - style: widget.style?.copyWith( - color: showDecoration ? Theme.of(context).colorScheme.primary : null, - decoration: showDecoration ? TextDecoration.underline : TextDecoration.none, - decorationColor: showDecoration ? Theme.of(context).colorScheme.primary : null, - decorationThickness: 3, - ), + final color = + (showDecoration ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface).withValues( + alpha: widget.opacity, + ); + return Text( + widget.text, + maxLines: widget.maxLines, + overflow: widget.overflow, + style: widget.style?.copyWith( + color: color, + decoration: showDecoration ? TextDecoration.underline : TextDecoration.none, + decorationColor: color, + decorationThickness: 3, ), ); } diff --git a/lib/widgets/shared/horizontal_list.dart b/lib/widgets/shared/horizontal_list.dart index 1f1c916..556b679 100644 --- a/lib/widgets/shared/horizontal_list.dart +++ b/lib/widgets/shared/horizontal_list.dart @@ -53,21 +53,30 @@ class HorizontalList extends ConsumerStatefulWidget { ConsumerState createState() => _HorizontalListState(); } -class _HorizontalListState extends ConsumerState { +class _HorizontalListState extends ConsumerState with TickerProviderStateMixin { final FocusNode parentNode = FocusNode(); FocusNode? lastFocused; final GlobalKey _firstItemKey = GlobalKey(); + final GlobalKey _listViewKey = GlobalKey(); final ScrollController _scrollController = ScrollController(); final contentPadding = 8.0; double? contentWidth; double? _firstItemWidth; + AnimationController? _scrollAnimation; + @override void initState() { super.initState(); _measureFirstItem(); } + @override + void dispose() { + _scrollAnimation?.dispose(); + super.dispose(); + } + void _measureFirstItem() { if (_firstItemWidth != null) return; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -87,16 +96,34 @@ class _HorizontalListState extends ConsumerState { } Future _scrollToPosition(int index) async { - if (_firstItemWidth == null) return; + if (_firstItemWidth == null || !_scrollController.hasClients) return; - final offset = index * (_firstItemWidth! + contentPadding); - final clamped = math.min(offset, _scrollController.position.maxScrollExtent); + final target = (index * (_firstItemWidth! + contentPadding)).clamp(0, _scrollController.position.maxScrollExtent); - await _scrollController.animateTo( - clamped, - duration: const Duration(milliseconds: 250), - curve: Curves.fastOutSlowIn, + // Cancel any ongoing animation + _scrollAnimation?.stop(); + + final controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 125), ); + + _scrollAnimation = controller; + + final tween = Tween( + begin: _scrollController.offset, + end: target.toDouble(), + ); + + controller.addListener(() { + if (_scrollController.hasClients) { + _scrollController.jumpTo(tween.evaluate(controller)); + } + }); + + controller.forward().whenComplete(() { + if (_scrollAnimation == controller) _scrollAnimation = null; + }); } void _scrollToStart() { @@ -146,12 +173,11 @@ class _HorizontalListState extends ConsumerState { if (widget.subtext != null) Flexible( child: ExcludeFocus( - child: Opacity( - opacity: 0.5, - child: Text( - widget.subtext!, - style: Theme.of(context).textTheme.titleMedium, - ), + child: Text( + widget.subtext!, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5), + ), ), ), ), @@ -223,11 +249,16 @@ class _HorizontalListState extends ConsumerState { if (currentNode != null) { lastFocused = currentNode; + final correctIndex = _getCorrectIndexForNode(currentNode); + if (widget.onFocused != null) { - widget.onFocused!(nodesOnSameRow.indexOf(currentNode)); + if (correctIndex != -1) { + widget.onFocused!(correctIndex); + } } else { context.ensureVisible(); } + currentNode.requestFocus(); } } @@ -244,25 +275,17 @@ class _HorizontalListState extends ConsumerState { throttle: Throttler(duration: const Duration(milliseconds: 100)), onFocused: (node) { lastFocused = node; - final nodesOnSameRow = _nodesInRow(parentNode); - if (widget.onFocused != null) { - widget.onFocused?.call(nodesOnSameRow.indexOf(node)); - } - final nodeContext = node.context!; - final renderObject = nodeContext.findRenderObject(); - if (renderObject != null) { - final position = _scrollController.position; - position.ensureVisible( - renderObject, - alignment: _calcAlignmentWithPadding(nodeContext), - duration: const Duration(milliseconds: 175), - curve: Curves.fastOutSlowIn, - ); + final correctIndex = _getCorrectIndexForNode(node); + if (correctIndex != -1) { + widget.onFocused?.call(correctIndex); + _scrollToPosition(correctIndex); } }, ), child: ListView.separated( + key: _listViewKey, controller: _scrollController, + clipBehavior: Clip.none, scrollDirection: Axis.horizontal, padding: widget.contentPadding, itemBuilder: (context, index) => index == widget.items.length @@ -286,10 +309,24 @@ class _HorizontalListState extends ConsumerState { ); } - double _calcAlignmentWithPadding(BuildContext context) { - final viewportWidth = _scrollController.position.viewportDimension; - final double leftPadding = widget.contentPadding.left + (contentPadding * 2); - return leftPadding / viewportWidth; + int _getCorrectIndexForNode(FocusNode node) { + if (!mounted || _firstItemWidth == null || !_scrollController.hasClients || node.context == null) return -1; + + final scrollableContext = _listViewKey.currentContext; + if (scrollableContext == null || !scrollableContext.mounted) return -1; + + final scrollableBox = scrollableContext.findRenderObject() as RenderBox?; + final itemBox = node.context!.findRenderObject() as RenderBox?; + if (scrollableBox == null || itemBox == null) return -1; + + final dx = itemBox.localToGlobal(Offset.zero, ancestor: scrollableBox).dx; + + final totalItemWidth = _firstItemWidth! + contentPadding; + final offset = dx + _scrollController.offset - widget.contentPadding.left; + + final index = ((offset + totalItemWidth / 2) ~/ totalItemWidth).clamp(0, widget.items.length - 1); + + return index; } } @@ -344,12 +381,11 @@ class HorizontalRailFocus extends WidgetOrderTraversalPolicy { @override bool inDirection(FocusNode currentNode, TraversalDirection direction) { - if (throttle?.canRun() == false) return true; - final rowNodes = _nodesInRow(parentNode); final index = rowNodes.indexOf(currentNode); if (direction == TraversalDirection.left) { + if (throttle?.canRun() == false) return true; if (index > 0) { final target = rowNodes[index - 1]; target.requestFocus();