diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bdb9329..77a52bc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1289,5 +1289,7 @@ "syncResumeAll": "Resume all", "syncStopAll": "Stop all", "syncDeleteAll": "Delete all files", - "syncAllFiles": "Sync all files" + "syncAllFiles": "Sync all files", + "usePostersForLibraryIconsTitle": "Show posters for library icons", + "usePostersForLibraryIconsDesc": "Show posters instead of icons for libraries" } \ No newline at end of file diff --git a/lib/localization_delegates.dart b/lib/localization_delegates.dart index 55beb90..ec21bbe 100644 --- a/lib/localization_delegates.dart +++ b/lib/localization_delegates.dart @@ -181,7 +181,6 @@ class FladderCupertinoLocalizationsDelegate extends LocalizationsDelegate 'DirectPlaybackModel(item: $item, playbackInfo: $playbackInfo)'; diff --git a/lib/models/playback/offline_playback_model.dart b/lib/models/playback/offline_playback_model.dart index 074f166..bc38ff0 100644 --- a/lib/models/playback/offline_playback_model.dart +++ b/lib/models/playback/offline_playback_model.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/chapters_model.dart'; +import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/trick_play_model.dart'; @@ -96,6 +97,15 @@ class OfflinePlaybackModel extends PlaybackModel { return false; } + @override + OfflinePlaybackModel? updateUserData(UserData userData) { + return copyWith( + item: item.copyWith( + userData: userData, + ), + ); + } + @override String toString() => 'OfflinePlaybackModel(item: $item, syncedItem: $syncedItem)'; diff --git a/lib/models/playback/playback_model.dart b/lib/models/playback/playback_model.dart index afac46c..0d2ae7e 100644 --- a/lib/models/playback/playback_model.dart +++ b/lib/models/playback/playback_model.dart @@ -12,6 +12,7 @@ import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/episode_model.dart'; +import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/season_model.dart'; @@ -84,6 +85,8 @@ class PlaybackModel { Future? startDuration() async => item.userData.playBackPosition; + PlaybackModel? updateUserData(UserData userData) => throw UnimplementedError(); + Future? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError(); Future? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError(); Future? setQualityOption(Map map) => throw UnimplementedError(); diff --git a/lib/models/playback/transcode_playback_model.dart b/lib/models/playback/transcode_playback_model.dart index 165c3c4..4b01105 100644 --- a/lib/models/playback/transcode_playback_model.dart +++ b/lib/models/playback/transcode_playback_model.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/chapters_model.dart'; +import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/trick_play_model.dart'; @@ -51,7 +52,7 @@ class TranscodePlaybackModel extends PlaybackModel { } @override - Future? setQualityOption(Map map) async => copyWith(bitRateOptions: map); + Future? setQualityOption(Map map) async => copyWith(bitRateOptions: map); @override Future playbackStarted(Duration position, Ref ref) async { @@ -114,6 +115,15 @@ class TranscodePlaybackModel extends PlaybackModel { return this; } + @override + TranscodePlaybackModel? updateUserData(UserData userData) { + return copyWith( + item: item.copyWith( + userData: userData, + ), + ); + } + @override String toString() => 'TranscodePlaybackModel(item: $item, playbackInfo: $playbackInfo)'; diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index f8b9f35..15a2b3d 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -36,6 +36,7 @@ class ClientSettingsModel with _$ClientSettingsModel { @Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant, @Default(true) bool backgroundPosters, @Default(true) bool checkForUpdates, + @Default(false) bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize, }) = _ClientSettingsModel; diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index d506115..ffa166f 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -42,6 +42,7 @@ mixin _$ClientSettingsModel { DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError; bool get backgroundPosters => throw _privateConstructorUsedError; bool get checkForUpdates => throw _privateConstructorUsedError; + bool get usePosterForLibrary => throw _privateConstructorUsedError; String? get lastViewedUpdate => throw _privateConstructorUsedError; int? get libraryPageSize => throw _privateConstructorUsedError; @@ -83,6 +84,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> { DynamicSchemeVariant schemeVariant, bool backgroundPosters, bool checkForUpdates, + bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize}); } @@ -123,6 +125,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel> Object? schemeVariant = null, Object? backgroundPosters = null, Object? checkForUpdates = null, + Object? usePosterForLibrary = null, Object? lastViewedUpdate = freezed, Object? libraryPageSize = freezed, }) { @@ -211,6 +214,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel> ? _value.checkForUpdates : checkForUpdates // ignore: cast_nullable_to_non_nullable as bool, + usePosterForLibrary: null == usePosterForLibrary + ? _value.usePosterForLibrary + : usePosterForLibrary // ignore: cast_nullable_to_non_nullable + as bool, lastViewedUpdate: freezed == lastViewedUpdate ? _value.lastViewedUpdate : lastViewedUpdate // ignore: cast_nullable_to_non_nullable @@ -253,6 +260,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res> DynamicSchemeVariant schemeVariant, bool backgroundPosters, bool checkForUpdates, + bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize}); } @@ -291,6 +299,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res> Object? schemeVariant = null, Object? backgroundPosters = null, Object? checkForUpdates = null, + Object? usePosterForLibrary = null, Object? lastViewedUpdate = freezed, Object? libraryPageSize = freezed, }) { @@ -379,6 +388,10 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res> ? _value.checkForUpdates : checkForUpdates // ignore: cast_nullable_to_non_nullable as bool, + usePosterForLibrary: null == usePosterForLibrary + ? _value.usePosterForLibrary + : usePosterForLibrary // ignore: cast_nullable_to_non_nullable + as bool, lastViewedUpdate: freezed == lastViewedUpdate ? _value.lastViewedUpdate : lastViewedUpdate // ignore: cast_nullable_to_non_nullable @@ -417,6 +430,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel this.schemeVariant = DynamicSchemeVariant.rainbow, this.backgroundPosters = true, this.checkForUpdates = true, + this.usePosterForLibrary = false, this.lastViewedUpdate, this.libraryPageSize}) : super._(); @@ -485,13 +499,16 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel @JsonKey() final bool checkForUpdates; @override + @JsonKey() + final bool usePosterForLibrary; + @override final String? lastViewedUpdate; @override final int? libraryPageSize; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)'; + return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)'; } @override @@ -522,6 +539,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel ..add(DiagnosticsProperty('schemeVariant', schemeVariant)) ..add(DiagnosticsProperty('backgroundPosters', backgroundPosters)) ..add(DiagnosticsProperty('checkForUpdates', checkForUpdates)) + ..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary)) ..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate)) ..add(DiagnosticsProperty('libraryPageSize', libraryPageSize)); } @@ -566,6 +584,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel { final DynamicSchemeVariant schemeVariant, final bool backgroundPosters, final bool checkForUpdates, + final bool usePosterForLibrary, final String? lastViewedUpdate, final int? libraryPageSize}) = _$ClientSettingsModelImpl; _ClientSettingsModel._() : super._(); @@ -617,6 +636,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel { @override bool get checkForUpdates; @override + bool get usePosterForLibrary; + @override String? get lastViewedUpdate; @override int? get libraryPageSize; diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart index 6c1c6b5..ed42ef6 100644 --- a/lib/models/settings/client_settings_model.g.dart +++ b/lib/models/settings/client_settings_model.g.dart @@ -43,6 +43,7 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson( DynamicSchemeVariant.rainbow, backgroundPosters: json['backgroundPosters'] as bool? ?? true, checkForUpdates: json['checkForUpdates'] as bool? ?? true, + usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false, lastViewedUpdate: json['lastViewedUpdate'] as String?, libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(), ); @@ -71,6 +72,7 @@ Map _$$ClientSettingsModelImplToJson( 'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!, 'backgroundPosters': instance.backgroundPosters, 'checkForUpdates': instance.checkForUpdates, + 'usePosterForLibrary': instance.usePosterForLibrary, 'lastViewedUpdate': instance.lastViewedUpdate, 'libraryPageSize': instance.libraryPageSize, }; diff --git a/lib/models/view_model.dart b/lib/models/view_model.dart index 53c8d2b..649ecb9 100644 --- a/lib/models/view_model.dart +++ b/lib/models/view_model.dart @@ -113,6 +113,7 @@ class ViewModel { FutureOr Function() action, { FutureOr Function()? onLongPress, List? trailing, + Widget? customIcon, }) { return NavigationButton( label: name, @@ -121,6 +122,7 @@ class ViewModel { onLongPress: onLongPress, horizontal: horizontal, expanded: expanded, + customIcon: customIcon, trailing: trailing ?? [], selectedIcon: Icon(collectionType.icon), icon: Icon(collectionType.iconOutlined), diff --git a/lib/routes/auto_router.dart b/lib/routes/auto_router.dart index 9bca7e9..533a4c2 100644 --- a/lib/routes/auto_router.dart +++ b/lib/routes/auto_router.dart @@ -125,6 +125,10 @@ class AuthGuard extends AutoRouteGuard { @override Future onNavigation(NavigationResolver resolver, StackRouter router) async { + if (resolver.route == router.current.route) { + return; + } + if (ref.read(userProvider) != null || resolver.routeName == const LoginRoute().routeName || resolver.routeName == SplashRoute().routeName) { diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index af0ea5d..1329647 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -107,15 +107,12 @@ class _DashboardScreenState extends ConsumerState { ), if (homeBanner && homeCarouselItems.isNotEmpty) ...{ SliverToBoxAdapter( - child: Transform.translate( - offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0), - child: Padding( - padding: AdaptiveLayout.adaptivePadding( - context, - horizontalPadding: 0, - ), - child: HomeBannerWidget(posters: homeCarouselItems), + child: Padding( + padding: AdaptiveLayout.adaptivePadding( + context, + horizontalPadding: 0, ), + child: HomeBannerWidget(posters: homeCarouselItems), ), ), }, diff --git a/lib/screens/favourites/favourites_screen.dart b/lib/screens/favourites/favourites_screen.dart index afee5db..bf3e94b 100644 --- a/lib/screens/favourites/favourites_screen.dart +++ b/lib/screens/favourites/favourites_screen.dart @@ -38,7 +38,6 @@ class FavouritesScreen extends ConsumerWidget { slivers: [ if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) NestedSliverAppBar( - searchTitle: "${context.localized.search} ${context.localized.favorites.toLowerCase()}...", parent: context, route: LibrarySearchRoute(favourites: true), ) diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index faff11f..d632371 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -108,6 +108,13 @@ class HomeScreen extends ConsumerWidget { selectedIcon: Icon(e.selectedIcon), route: const LibraryRoute(), action: () => e.navigate(context), + floatingActionButton: AdaptiveFab( + context: context, + title: context.localized.search, + key: Key(e.name.capitalize()), + onPressed: () => context.router.navigate(LibrarySearchRoute()), + child: const Icon(IconsaxPlusLinear.search_status), + ), ); } }) diff --git a/lib/screens/settings/client_sections/client_settings_visual.dart b/lib/screens/settings/client_sections/client_settings_visual.dart index 5666573..64076fe 100644 --- a/lib/screens/settings/client_sections/client_settings_visual.dart +++ b/lib/screens/settings/client_sections/client_settings_visual.dart @@ -99,6 +99,18 @@ List buildClientSettingsVisual( ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundPosters: value)), ), ), + SettingsListTile( + label: Text(context.localized.usePostersForLibraryIconsTitle), + subLabel: Text(context.localized.usePostersForLibraryIconsDesc), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((cb) => cb.copyWith(usePosterForLibrary: !clientSettings.usePosterForLibrary)), + trailing: Switch( + value: clientSettings.usePosterForLibrary, + onChanged: (value) => + ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(usePosterForLibrary: value)), + ), + ), SettingsListTile( label: Text(context.localized.settingsNextUpCutoffDays), trailing: SizedBox( diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index 51512ca..f792caf 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -249,6 +249,21 @@ class _PlayerSettingsPageState extends ConsumerState { onChanged: (value) => provider.setUseLibass(value), ), ), + if (!videoSettings.useLibass) + SettingsListTile( + label: Text(context.localized.settingsPlayerCustomSubtitlesTitle), + subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc), + onTap: videoSettings.useLibass + ? null + : () { + showDialog( + context: context, + barrierDismissible: true, + useSafeArea: false, + builder: (context) => const SubtitleEditor(), + ); + }, + ), AnimatedFadeSize( child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid ? SettingsMessageBox( @@ -272,20 +287,6 @@ class _PlayerSettingsPageState extends ConsumerState { }, )), ), - SettingsListTile( - label: Text(context.localized.settingsPlayerCustomSubtitlesTitle), - subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc), - onTap: videoSettings.useLibass - ? null - : () { - showDialog( - context: context, - barrierDismissible: false, - useSafeArea: false, - builder: (context) => const SubtitleEditor(), - ); - }, - ), ], ), _ => SettingsMessageBox( diff --git a/lib/screens/settings/widgets/subtitle_editor.dart b/lib/screens/settings/widgets/subtitle_editor.dart index 0d80071..66b4052 100644 --- a/lib/screens/settings/widgets/subtitle_editor.dart +++ b/lib/screens/settings/widgets/subtitle_editor.dart @@ -7,9 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart'; import 'package:fladder/providers/settings/subtitle_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; -import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; class SubtitleEditor extends ConsumerStatefulWidget { const SubtitleEditor({super.key}); @@ -27,8 +25,9 @@ class _SubtitleEditorState extends ConsumerState { final fakeText = context.localized.subtitleConfiguratorPlaceHolder; double lastScale = 0.0; - return Scaffold( - body: Dialog.fullscreen( + return Padding( + padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + child: Dialog.fullscreen( child: GestureDetector( onScaleUpdate: (details) { lastScale = details.scale; @@ -73,7 +72,6 @@ class _SubtitleEditorState extends ConsumerState { padding: MediaQuery.paddingOf(context), child: Column( children: [ - if (AdaptiveLayout.of(context).isDesktop) const FladderAppBar(), Row( children: [ const BackButton(), diff --git a/lib/screens/shared/default_title_bar.dart b/lib/screens/shared/default_title_bar.dart index d12ff14..aff9362 100644 --- a/lib/screens/shared/default_title_bar.dart +++ b/lib/screens/shared/default_title_bar.dart @@ -38,14 +38,21 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return const SizedBox.shrink(); final brightness = widget.brightness ?? Theme.of(context).brightness; final iconColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65); + final surfaceColor = Theme.of(context).colorScheme.surface; return MouseRegion( onEnter: (event) => setState(() => hovering = true), onExit: (event) => setState(() => hovering = false), child: AnimatedContainer( duration: const Duration(milliseconds: 250), decoration: BoxDecoration( - color: hovering ? Colors.black.withValues(alpha: 0.15) : Colors.transparent, - ), + gradient: LinearGradient( + colors: [ + surfaceColor.withValues(alpha: 0.7), + surfaceColor.withValues(alpha: hovering ? 0.7 : 0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + )), height: widget.height, child: kIsWeb ? const SizedBox.shrink() diff --git a/lib/screens/shared/media/components/item_logo.dart b/lib/screens/shared/media/components/item_logo.dart index fd9fdcd..5b215a9 100644 --- a/lib/screens/shared/media/components/item_logo.dart +++ b/lib/screens/shared/media/components/item_logo.dart @@ -17,8 +17,10 @@ class ItemLogo extends StatelessWidget { @override Widget build(BuildContext context) { final logo = item.getPosters?.logo; + final size = MediaQuery.sizeOf(context); + final maxHeight = size.height * 0.45; final textWidget = Container( - height: 512, + height: maxHeight, alignment: imageAlignment, child: Text( item.parentBaseModel.name, @@ -33,13 +35,14 @@ class ItemLogo extends StatelessWidget { ); return logo != null ? ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500, maxHeight: 500), + constraints: BoxConstraints(maxWidth: size.width * 0.35, maxHeight: maxHeight), child: FladderImage( image: logo, disableBlur: true, - alignment: imageAlignment, + stackFit: StackFit.passthrough, + alignment: Alignment.bottomLeft, imageErrorBuilder: (context, object, stack) => textWidget, - placeHolder: const SizedBox(height: 0), + placeHolder: textWidget, fit: BoxFit.contain, ), ) diff --git a/lib/screens/shared/nested_scaffold.dart b/lib/screens/shared/nested_scaffold.dart index 7971dc4..62fded7 100644 --- a/lib/screens/shared/nested_scaffold.dart +++ b/lib/screens/shared/nested_scaffold.dart @@ -23,8 +23,8 @@ class NestedScaffold extends ConsumerWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Theme.of(context).colorScheme.surface.withValues(alpha: 0.98), - Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), + Theme.of(context).colorScheme.surface.withValues(alpha: 0.85), + Theme.of(context).colorScheme.surface.withValues(alpha: 0.7), ], ), ), diff --git a/lib/screens/shared/nested_sliver_appbar.dart b/lib/screens/shared/nested_sliver_appbar.dart index c2a212d..e080308 100644 --- a/lib/screens/shared/nested_sliver_appbar.dart +++ b/lib/screens/shared/nested_sliver_appbar.dart @@ -4,77 +4,62 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; -import 'package:fladder/widgets/shared/shapes.dart'; class NestedSliverAppBar extends ConsumerWidget { final BuildContext parent; - final String? searchTitle; final PageRouteInfo? route; - const NestedSliverAppBar({required this.parent, this.route, this.searchTitle, super.key}); + const NestedSliverAppBar({required this.parent, this.route, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final surfaceColor = Theme.of(context).colorScheme.surface; + final buttonStyle = Theme.of(context).filledButtonTheme.style?.copyWith( + backgroundColor: WidgetStatePropertyAll( + surfaceColor.withValues(alpha: 0.8), + ), + ); return SliverAppBar( automaticallyImplyLeading: false, - elevation: 16, - forceElevated: true, + elevation: 0, + forceElevated: false, surfaceTintColor: Colors.transparent, shadowColor: Colors.transparent, - backgroundColor: Theme.of(context).colorScheme.surface, - shape: AppBarShape(), - title: SizedBox( - height: 65, + backgroundColor: Colors.transparent, + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + surfaceColor.withValues(alpha: 0.7), + surfaceColor.withValues(alpha: 0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + )), child: Padding( - padding: const EdgeInsets.only(bottom: 24), - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 10, - children: [ - SizedBox( - width: 30, - child: IconButton( - onPressed: () => Scaffold.of(parent).openDrawer(), - icon: const Icon(IconsaxPlusLinear.menu), - padding: EdgeInsets.zero, - ), - ), - Expanded( - child: Hero( - tag: "PrimarySearch", - child: Card( - elevation: 3, - shadowColor: Colors.transparent, - child: InkWell( - onTap: route != null - ? () { - route?.push(context); - } - : null, - child: Padding( - padding: const EdgeInsets.all(10), - child: Opacity( - opacity: 0.65, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(IconsaxPlusLinear.search_normal), - const SizedBox(width: 16), - Transform.translate( - offset: const Offset(0, 1.5), - child: Text(searchTitle ?? "${context.localized.search}..."), - ), - ], - ), - ), - ), + padding: MediaQuery.paddingOf(context).copyWith(bottom: 0), + child: Padding( + padding: const EdgeInsets.all(14), + child: SizedBox( + height: 50, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 12, + children: [ + AspectRatio( + aspectRatio: 1.0, + child: IconButton.filledTonal( + style: buttonStyle, + onPressed: () => Scaffold.of(parent).openDrawer(), + icon: const Icon(IconsaxPlusLinear.menu), + padding: EdgeInsets.zero, ), ), - ), + const Spacer(), + const SettingsUserIcon() + ], ), - const SettingsUserIcon() - ], + ), ), ), ), diff --git a/lib/screens/syncing/sync_item_details.dart b/lib/screens/syncing/sync_item_details.dart index a60585b..2641baf 100644 --- a/lib/screens/syncing/sync_item_details.dart +++ b/lib/screens/syncing/sync_item_details.dart @@ -92,8 +92,7 @@ class _SyncItemDetailsState extends ConsumerState { children: [ if (baseItem != null) ...{ Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, spacing: 16, children: [ SizedBox( @@ -193,7 +192,10 @@ class _SyncItemDetailsState extends ConsumerState { ), }, if (children?.isNotEmpty == true) ...[ - const Divider(), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Divider(), + ), ...children!.map( (e) => ChildSyncWidget(syncedChild: e), ), diff --git a/lib/screens/syncing/synced_screen.dart b/lib/screens/syncing/synced_screen.dart index c96257a..792fd95 100644 --- a/lib/screens/syncing/synced_screen.dart +++ b/lib/screens/syncing/synced_screen.dart @@ -47,7 +47,6 @@ class _SyncedScreenState extends ConsumerState { slivers: [ if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) NestedSliverAppBar( - searchTitle: "${context.localized.search} ...", parent: context, route: LibrarySearchRoute(), ) diff --git a/lib/screens/syncing/widgets/sync_options_button.dart b/lib/screens/syncing/widgets/sync_options_button.dart index 10fd615..5ebb555 100644 --- a/lib/screens/syncing/widgets/sync_options_button.dart +++ b/lib/screens/syncing/widgets/sync_options_button.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; @@ -51,6 +52,19 @@ class SyncOptionsButton extends ConsumerWidget { final enqueuedTasks = syncTasks.where((element) => element.status == TaskStatus.enqueued).toList(); final pausedTasks = syncTasks.where((element) => element.status == TaskStatus.paused).toList(); return [ + PopupMenuItem( + child: Row( + spacing: 12, + children: [ + const Icon(IconsaxPlusLinear.arrow_right), + Text(context.localized.showDetails), + ], + ), + onTap: () { + syncedItem.itemModel?.navigateTo(context); + context.maybePop(); + }, + ), PopupMenuItem( child: Row( spacing: 12, diff --git a/lib/screens/video_player/components/video_subtitle_controls.dart b/lib/screens/video_player/components/video_subtitle_controls.dart index e82e716..3bcb8d9 100644 --- a/lib/screens/video_player/components/video_subtitle_controls.dart +++ b/lib/screens/video_player/components/video_subtitle_controls.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/providers/settings/subtitle_settings_provider.dart'; import 'package:fladder/screens/shared/flat_button.dart'; @@ -60,7 +62,8 @@ class _VideoSubtitleControlsState extends ConsumerState { duration: const Duration(milliseconds: 250), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: controlsHidden ? DialogTheme.of(context).backgroundColor?.withValues(alpha: 0.75) : Colors.transparent, + color: + controlsHidden ? Theme.of(context).colorScheme.surfaceContainer.withValues(alpha: 0.8) : Colors.transparent, ), child: Padding( padding: const EdgeInsets.all(16), @@ -68,10 +71,19 @@ class _VideoSubtitleControlsState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ if (widget.label?.isNotEmpty == true) - Text( - widget.label!, - style: Theme.of(context).textTheme.headlineMedium, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.label!, + style: Theme.of(context).textTheme.headlineMedium, + ), + IconButton( + onPressed: () => context.maybePop(), + icon: const Icon(IconsaxPlusBold.close_circle), + ) + ], + ).addVisiblity(activeKey == null ? controlsHidden : activeKey == const Key('title')), IconButton.filledTonal( isSelected: !hideControls, onPressed: () => setState(() => hideControls = !hideControls), diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 2741714..d795fcb 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -250,8 +250,9 @@ class _DesktopControlsState extends ConsumerState { Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( + spacing: 16, mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( onPressed: () => minimizePlayer(context), @@ -260,26 +261,26 @@ class _DesktopControlsState extends ConsumerState { size: 24, ), ), - const SizedBox(width: 16), if (currentItem != null) - Expanded( - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: 150.clamp(50, MediaQuery.sizeOf(context).height * 0.25).toDouble(), - ), - child: ItemLogo( - item: currentItem, - imageAlignment: Alignment.topLeft, - textStyle: Theme.of(context).textTheme.headlineLarge, - ), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: 150.clamp(50, MediaQuery.sizeOf(context).height * 0.25).toDouble(), + ), + child: ItemLogo( + item: currentItem, + imageAlignment: Alignment.topLeft, + textStyle: Theme.of(context).textTheme.headlineLarge, ), ), - const SizedBox(width: 16), + const Spacer(), if (AdaptiveLayout.of(context).inputDevice == InputDevice.touch) - Tooltip( - message: context.localized.stop, - child: IconButton( - onPressed: () => closePlayer(), icon: const Icon(IconsaxPlusLinear.close_square))), + Align( + alignment: Alignment.centerRight, + child: Tooltip( + message: context.localized.stop, + child: IconButton( + onPressed: () => closePlayer(), icon: const Icon(IconsaxPlusLinear.close_square))), + ), ], ), ), diff --git a/lib/util/fladder_image.dart b/lib/util/fladder_image.dart index 2d60063..b46e30c 100644 --- a/lib/util/fladder_image.dart +++ b/lib/util/fladder_image.dart @@ -12,6 +12,7 @@ class FladderImage extends ConsumerWidget { final Widget Function(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded)? frameBuilder; final Widget Function(BuildContext context, Object object, StackTrace? stack)? imageErrorBuilder; final Widget? placeHolder; + final StackFit stackFit; final BoxFit fit; final BoxFit? blurFit; final AlignmentGeometry? alignment; @@ -22,6 +23,7 @@ class FladderImage extends ConsumerWidget { this.frameBuilder, this.imageErrorBuilder, this.placeHolder, + this.stackFit = StackFit.expand, this.fit = BoxFit.cover, this.blurFit, this.alignment, @@ -39,7 +41,7 @@ class FladderImage extends ConsumerWidget { } else { return Stack( key: Key(newImage.key), - fit: StackFit.expand, + fit: stackFit, children: [ if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty || blurOnly) BlurHash( diff --git a/lib/util/widget_extensions.dart b/lib/util/widget_extensions.dart index d97c23e..68df463 100644 --- a/lib/util/widget_extensions.dart +++ b/lib/util/widget_extensions.dart @@ -24,6 +24,6 @@ extension WidgetExtensions on Widget { } Widget addVisiblity(bool visible) { - return AnimatedOpacity(duration: const Duration(milliseconds: 250), opacity: visible ? 1 : 0, child: this); + return AnimatedOpacity(duration: const Duration(milliseconds: 250), opacity: visible ? 1 : 0.10, child: this); } } diff --git a/lib/widgets/navigation_scaffold/components/destination_model.dart b/lib/widgets/navigation_scaffold/components/destination_model.dart index 7be11e9..28f53ca 100644 --- a/lib/widgets/navigation_scaffold/components/destination_model.dart +++ b/lib/widgets/navigation_scaffold/components/destination_model.dart @@ -81,13 +81,14 @@ class DestinationModel { ); } - NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded) { + NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded, {Widget? customIcon}) { return NavigationButton( label: label, selected: selected, onPressed: action, horizontal: horizontal, expanded: expanded, + customIcon: customIcon, selectedIcon: selectedIcon!, icon: icon!, ); diff --git a/lib/widgets/navigation_scaffold/components/drawer_list_button.dart b/lib/widgets/navigation_scaffold/components/drawer_list_button.dart index 5b5477f..93df459 100644 --- a/lib/widgets/navigation_scaffold/components/drawer_list_button.dart +++ b/lib/widgets/navigation_scaffold/components/drawer_list_button.dart @@ -1,9 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class DrawerListButton extends ConsumerStatefulWidget { final String label; diff --git a/lib/widgets/navigation_scaffold/components/floating_player_bar.dart b/lib/widgets/navigation_scaffold/components/floating_player_bar.dart index b9078b7..6a514e1 100644 --- a/lib/widgets/navigation_scaffold/components/floating_player_bar.dart +++ b/lib/widgets/navigation_scaffold/components/floating_player_bar.dart @@ -3,10 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:overflow_view/overflow_view.dart'; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/media_playback_model.dart'; -import 'package:fladder/providers/settings/video_player_settings_provider.dart'; +import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/flat_button.dart'; @@ -15,10 +16,16 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; +import 'package:fladder/widgets/shared/fladder_slider.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; const videoPlayerHeroTag = "HeroPlayer"; -const floatingPlayerHeight = 70.0; +double floatingPlayerHeight(BuildContext context) => switch (AdaptiveLayout.viewSizeOf(context)) { + ViewSize.phone => 75, + ViewSize.tablet => 85, + ViewSize.desktop => 95, + }; class FloatingPlayerBar extends ConsumerStatefulWidget { const FloatingPlayerBar({super.key}); @@ -29,6 +36,8 @@ class FloatingPlayerBar extends ConsumerStatefulWidget { class _CurrentlyPlayingBarState extends ConsumerState { bool showExpandButton = false; + bool changingSliderValue = false; + Duration lastPosition = Duration.zero; Future openFullScreenPlayer() async { setState(() => showExpandButton = false); @@ -58,155 +67,239 @@ class _CurrentlyPlayingBarState extends ConsumerState { Widget build(BuildContext context) { final playbackInfo = ref.watch(mediaPlaybackProvider); final player = ref.watch(videoPlayerProvider); - final playbackModel = ref.watch(playBackModel.select((value) => value?.item)); - final progress = playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds; - return Dismissible( - key: const Key("CurrentlyPlayingBar"), - confirmDismiss: (direction) async { - if (direction == DismissDirection.up) { - await openFullScreenPlayer(); - } else { - await stopPlayer(); - } - return false; - }, - direction: DismissDirection.vertical, - child: InkWell( - onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"), - child: Card( - elevation: 5, - color: Theme.of(context).colorScheme.primaryContainer, - child: SizedBox( - height: floatingPlayerHeight, - child: LayoutBuilder(builder: (context, constraints) { - return Row( - children: [ - Flexible( - child: Padding( - padding: MediaQuery.paddingOf(context).copyWith(top: 0, bottom: 0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(6), - child: Row( - spacing: 7, - children: [ - if (playbackInfo.state == VideoPlayerState.minimized) - Card( - child: AspectRatio( - aspectRatio: 1.67, - child: MouseRegion( - onEnter: (event) => setState(() => showExpandButton = true), - onExit: (event) => setState(() => showExpandButton = false), - child: Stack( - children: [ - Hero( - tag: videoPlayerHeroTag, - child: player.videoWidget( - UniqueKey(), - BoxFit.fitHeight, - ) ?? - const SizedBox.shrink(), - ), - Positioned.fill( - child: Tooltip( - message: "Expand player", - waitDuration: const Duration(milliseconds: 500), - child: AnimatedOpacity( - opacity: showExpandButton ? 1 : 0, - duration: const Duration(milliseconds: 125), - child: Container( - color: Colors.black.withValues(alpha: 0.6), - child: FlatButton( - onTap: () async => openFullScreenPlayer(), - child: const Icon(Icons.keyboard_arrow_up_rounded), - ), - ), - ), - ), - ) - ], - ), - ), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + final item = ref.watch(playBackModel.select((value) => value?.item)); + if (!changingSliderValue) { + lastPosition = playbackInfo.position; + } + + var isFavourite = item?.userData.isFavourite == true; + + final isDesktop = AdaptiveLayout.of(context).isDesktop; + + final itemActions = [ + ItemActionButton( + label: Text(context.localized.audio), + icon: Consumer( + builder: (context, ref, child) { + var volume = (player.lastState?.volume ?? 0) <= 0; + return Icon( + volume ? IconsaxPlusBold.volume_cross : IconsaxPlusBold.volume_high, + ); + }, + ), + action: () { + final volume = player.lastState?.volume == 0 ? 100.0 : 0.0; + player.setVolume(volume); + }), + ItemActionButton( + label: Text(context.localized.stop), + action: () async => stopPlayer(), + icon: const Icon(IconsaxPlusBold.stop), + ), + ItemActionButton( + label: Text(isFavourite ? context.localized.removeAsFavorite : context.localized.addAsFavorite), + icon: Icon( + color: isFavourite ? Colors.red : null, + isFavourite ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, + ), + action: () async { + final result = (await ref.read(userProvider.notifier).setAsFavorite( + !isFavourite, + item?.id ?? "", + )) + ?.body; + + if (result != null) { + ref.read(playBackModel.notifier).update((state) => state?.updateUserData(result)); + } + }, + ), + ]; + return Padding( + padding: + MediaQuery.paddingOf(context).copyWith(top: 0, bottom: isDesktop ? 0 : MediaQuery.paddingOf(context).bottom), + child: Dismissible( + key: const Key("CurrentlyPlayingBar"), + confirmDismiss: (direction) async { + if (direction == DismissDirection.up) { + await openFullScreenPlayer(); + } else { + await stopPlayer(); + } + return false; + }, + direction: DismissDirection.vertical, + child: InkWell( + onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"), + child: Card( + elevation: 5, + color: Theme.of(context).colorScheme.primaryContainer, + child: SizedBox( + height: floatingPlayerHeight(context), + child: LayoutBuilder(builder: (context, constraints) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(6), + child: Row( + spacing: 12, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (playbackInfo.state == VideoPlayerState.minimized) + Card( + child: AspectRatio( + aspectRatio: 1.67, + child: MouseRegion( + onEnter: (event) => setState(() => showExpandButton = true), + onExit: (event) => setState(() => showExpandButton = false), + child: Stack( children: [ - Flexible( - child: Text( - playbackModel?.title ?? "", - style: Theme.of(context).textTheme.titleMedium, - ), + Hero( + tag: videoPlayerHeroTag, + child: player.videoWidget( + UniqueKey(), + BoxFit.fitHeight, + ) ?? + const SizedBox.shrink(), ), - if (playbackModel?.detailedName(context)?.isNotEmpty == true) - Flexible( - child: Text( - playbackModel?.detailedName(context) ?? "", - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: - Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65), - ), + Positioned.fill( + child: Tooltip( + message: "Expand player", + waitDuration: const Duration(milliseconds: 500), + child: AnimatedOpacity( + opacity: showExpandButton ? 1 : 0, + duration: const Duration(milliseconds: 125), + child: Container( + color: Colors.black.withValues(alpha: 0.6), + child: FlatButton( + onTap: () async => openFullScreenPlayer(), + child: const Icon(Icons.keyboard_arrow_up_rounded), + ), + ), ), ), + ) ], ), ), - if (!progress.isNaN && constraints.maxWidth > 500) - Text( - "${playbackInfo.position.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: IconButton.filledTonal( - onPressed: () => ref.read(videoPlayerProvider).playOrPause(), - icon: playbackInfo.playing - ? const Icon(Icons.pause_rounded) - : const Icon(Icons.play_arrow_rounded), - ), - ), - if (constraints.maxWidth > 500) ...[ - IconButton( - onPressed: () { - final volume = player.lastState?.volume == 0 ? 100.0 : 0.0; - player.setVolume(volume); - }, - icon: Icon( - ref.watch(videoPlayerSettingsProvider.select((value) => value.volume)) <= 0 - ? IconsaxPlusBold.volume_cross - : IconsaxPlusBold.volume_high, + ), + ), + Expanded( + child: InkWell( + onTap: () => item?.navigateTo(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + item?.title ?? "", + style: Theme.of(context).textTheme.titleMedium, + maxLines: 1, ), ), - Tooltip( - message: context.localized.stop, - waitDuration: const Duration(milliseconds: 500), - child: IconButton( - onPressed: () async => stopPlayer(), - icon: const Icon(IconsaxPlusBold.stop), + if (item?.detailedName(context)?.isNotEmpty == true) + Flexible( + child: Text( + item?.detailedName(context) ?? "", + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65), + ), + maxLines: 1, + ), ), - ), ], - ], + ), ), ), - ), - LinearProgressIndicator( - minHeight: 6, - backgroundColor: Colors.black.withValues(alpha: 0.25), - color: Theme.of(context).colorScheme.primary, - value: progress.clamp(0, 1), - ), - ], + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (constraints.maxWidth > 500) + Flexible( + child: Text( + "${lastPosition.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: IconButton.filledTonal( + onPressed: () => ref.read(videoPlayerProvider).playOrPause(), + icon: playbackInfo.playing + ? const Icon(Icons.pause_rounded) + : const Icon(Icons.play_arrow_rounded), + ), + ), + ), + Flexible( + child: OverflowView.flexible( + builder: (context, remainingItemCount) => PopupMenuButton( + iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45), + padding: EdgeInsets.zero, + itemBuilder: (context) => itemActions + .sublist(itemActions.length - remainingItemCount) + .map( + (e) => e.toPopupMenuItem(useIcons: true), + ) + .toList(), + ), + children: itemActions.map((e) => e.toButton()).toList(), + ), + ) + ], + ), + ) + ], + ), ), ), - ), - ], - ); - }), + AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer + ? SizedBox( + height: 8, + child: FladderSlider( + value: lastPosition.inMilliseconds.toDouble(), + min: 0.0, + max: playbackInfo.duration.inMilliseconds.toDouble(), + thumbWidth: 8, + onChangeStart: (value) { + setState(() { + changingSliderValue = true; + }); + }, + onChangeEnd: (value) async { + await player.seek(Duration(milliseconds: value ~/ 1)); + await Future.delayed(const Duration(milliseconds: 250)); + if (player.lastState?.playing == true) { + player.play(); + } + setState(() { + lastPosition = Duration(milliseconds: value.toInt()); + changingSliderValue = false; + }); + }, + onChanged: (value) { + setState(() { + lastPosition = Duration(milliseconds: value.toInt()); + }); + }, + ), + ) + : LinearProgressIndicator( + minHeight: 8, + backgroundColor: Colors.black.withValues(alpha: 0.25), + color: Theme.of(context).colorScheme.primary, + value: (playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds) + .clamp(0, 1), + ) + ], + ); + }), + ), ), ), ), diff --git a/lib/widgets/navigation_scaffold/components/navigation_button.dart b/lib/widgets/navigation_scaffold/components/navigation_button.dart index 004fec5..95b83b4 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_button.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_button.dart @@ -14,6 +14,7 @@ class NavigationButton extends ConsumerStatefulWidget { final Function()? onPressed; final Function()? onLongPress; final List trailing; + final Widget? customIcon; final bool selected; final Duration duration; const NavigationButton({ @@ -24,6 +25,7 @@ class NavigationButton extends ConsumerStatefulWidget { this.expanded = false, this.onPressed, this.onLongPress, + this.customIcon, this.selected = false, this.trailing = const [], this.duration = const Duration(milliseconds: 125), @@ -64,9 +66,11 @@ class _NavigationButtonState extends ConsumerState { onLongPress: widget.onLongPress, child: widget.horizontal ? Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + padding: widget.customIcon != null + ? EdgeInsetsGeometry.zero + : const EdgeInsets.symmetric(vertical: 6, horizontal: 8), child: SizedBox( - height: 35, + height: widget.customIcon != null ? 60 : 35, child: Row( spacing: 4, mainAxisAlignment: MainAxisAlignment.center, @@ -85,10 +89,11 @@ class _NavigationButtonState extends ConsumerState { .withValues(alpha: widget.selected && !widget.expanded ? 1 : 0), ), ), - AnimatedSwitcher( - duration: widget.duration, - child: widget.selected ? widget.selectedIcon : widget.icon, - ), + widget.customIcon ?? + AnimatedSwitcher( + duration: widget.duration, + child: widget.selected ? widget.selectedIcon : widget.icon, + ), const SizedBox(width: 6), if (widget.horizontal && widget.expanded) ...[ if (widget.label != null) @@ -119,17 +124,18 @@ class _NavigationButtonState extends ConsumerState { ), ) : Padding( - padding: const EdgeInsets.all(8), + padding: widget.customIcon != null ? EdgeInsetsGeometry.zero : const EdgeInsets.all(8), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( spacing: 8, children: [ - AnimatedSwitcher( - duration: widget.duration, - child: widget.selected ? widget.selectedIcon : widget.icon, - ), + widget.customIcon ?? + AnimatedSwitcher( + duration: widget.duration, + child: widget.selected ? widget.selectedIcon : widget.icon, + ), if (widget.label != null && widget.horizontal && widget.expanded) Flexible(child: Text(widget.label!)) ], diff --git a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart index ba5ec29..4fed8c1 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart @@ -7,10 +7,13 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/collection_types.dart'; import 'package:fladder/models/view_model.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart'; import 'package:fladder/screens/shared/animated_fade_size.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/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; @@ -36,6 +39,7 @@ class NestedNavigationDrawer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final useLibraryPosters = ref.watch(clientSettingsProvider.select((value) => value.usePosterForLibrary)); return NavigationDrawer( key: const Key('navigation_drawer'), backgroundColor: isExpanded ? Colors.transparent : null, @@ -91,22 +95,41 @@ class NestedNavigationDrawer extends ConsumerWidget { style: Theme.of(context).textTheme.titleMedium, ), ), - ...views.map((library) => DrawerListButton( - label: library.name, - selected: context.router.currentUrl.contains(library.id), - actions: [ - ItemActionButton( - label: Text(context.localized.scanLibrary), - icon: const Icon(IconsaxPlusLinear.refresh), - action: () => showRefreshPopup(context, library.id, library.name), - ), - ], - onPressed: () { - context.router.push(LibrarySearchRoute(viewModelId: library.id)); - Scaffold.of(context).closeDrawer(); - }, - selectedIcon: Icon(library.collectionType.icon), - icon: Icon(library.collectionType.iconOutlined))), + ...views.map((library) { + var selected = context.router.currentUrl.contains(library.id); + final Widget? posterIcon = useLibraryPosters + ? ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: AspectRatio( + aspectRatio: 1.0, + child: FladderImage( + image: library.imageData?.primary, + placeHolder: Card( + child: Icon( + selected ? library.collectionType.icon : library.collectionType.iconOutlined, + ), + ), + ), + ), + ) + : null; + return DrawerListButton( + label: library.name, + selected: selected, + actions: [ + ItemActionButton( + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + action: () => showRefreshPopup(context, library.id, library.name), + ), + ], + onPressed: () { + context.router.push(LibrarySearchRoute(viewModelId: library.id)); + Scaffold.of(context).closeDrawer(); + }, + selectedIcon: posterIcon ?? Icon(library.collectionType.icon), + icon: posterIcon ?? Icon(library.collectionType.iconOutlined)); + }), }, const Divider(indent: 28, endIndent: 28), if (isExpanded) diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart index 44ba989..3f57d0e 100644 --- a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; @@ -9,16 +7,20 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:overflow_view/overflow_view.dart'; import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart'; import 'package:fladder/screens/shared/animated_fade_size.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/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; +import 'package:fladder/widgets/shared/custom_tooltip.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; @@ -43,39 +45,21 @@ class SideNavigationBar extends ConsumerStatefulWidget { class _SideNavigationBarState extends ConsumerState { bool expandedSideBar = false; - bool showOnHover = false; - Timer? timer; - double currentWidth = 80; - - void startTimer() { - timer?.cancel(); - timer = Timer(const Duration(milliseconds: 650), () { - setState(() { - showOnHover = true; - }); - }); - } - - void stopTimer() { - timer?.cancel(); - timer = Timer(const Duration(milliseconds: 125), () { - setState(() { - showOnHover = false; - }); - }); - } @override Widget build(BuildContext context) { final views = ref.watch(viewsProvider.select((value) => value.views)); + final usePostersForLibrary = ref.watch(clientSettingsProvider.select((value) => value.usePosterForLibrary)); + final expandedWidth = 250.0; final padding = MediaQuery.paddingOf(context); - final collapsedWidth = 90.0 + padding.left; + final collapsedWidth = 90 + padding.left; final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; final fullyExpanded = largeBar ? expandedSideBar : false; - final shouldExpand = showOnHover || fullyExpanded; + final shouldExpand = fullyExpanded; final isDesktop = AdaptiveLayout.of(context).isDesktop; + return Stack( children: [ AdaptiveLayoutBuilder( @@ -88,151 +72,229 @@ class _SideNavigationBarState extends ConsumerState { color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85), width: shouldExpand ? expandedWidth : collapsedWidth, child: MouseRegion( - onEnter: (value) => startTimer(), - onExit: (event) => stopTimer(), - onHover: (value) => startTimer(), - child: Column( - children: [ - if (isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{ - const SizedBox(height: 4), - Text( - "Fladder", - style: Theme.of(context).textTheme.titleSmall, - ), - }, - if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) SizedBox(height: padding.top), - Expanded( - child: Padding( - key: const Key('navigation_rail'), - padding: padding.copyWith(right: 0, top: isDesktop ? 8 : null), - child: Column( - spacing: 2, + child: Padding( + key: const Key('navigation_rail'), + padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null), + child: Column( + spacing: 2, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Align( - alignment: largeBar && expandedSideBar ? Alignment.centerRight : Alignment.center, - child: Opacity( - opacity: largeBar && expandedSideBar ? 0.65 : 1.0, - child: IconButton( - onPressed: !largeBar - ? () => widget.scaffoldKey.currentState?.openDrawer() - : () => setState(() { - expandedSideBar = !expandedSideBar; - if (!expandedSideBar) { - showOnHover = false; - } - }), - icon: Icon( - largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu, - ), + 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, ), ), - ), - const SizedBox(height: 8), - if (largeBar) ...[ - AnimatedFadeSize( - duration: const Duration(milliseconds: 250), - child: shouldExpand ? actionButton(context).extended : actionButton(context).normal, - ), - ], - Expanded( - child: Column( - spacing: 2, - mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start, - children: [ - ...widget.destinations.mapIndexed( - (index, destination) => - destination.toNavigationButton(widget.currentIndex == index, true, shouldExpand), - ), - if (views.isNotEmpty && largeBar) ...[ - const Divider( - indent: 32, - endIndent: 32, - ), - Flexible( - child: OverflowView.flexible( - direction: Axis.vertical, - spacing: 4, - children: views.map( - (view) { - final actions = [ - ItemActionButton( - label: Text(context.localized.scanLibrary), - icon: const Icon(IconsaxPlusLinear.refresh), - action: () => showRefreshPopup(context, view.id, view.name), - ) - ]; - return view.toNavigationButton( - context.router.currentUrl.contains(view.id), - true, - shouldExpand, - () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), - onLongPress: () => showBottomSheetPill( - context: context, - content: (context, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: actions.listTileItems(context, useIcons: true), - ), - ), - trailing: actions, - ); - }, - ).toList(), - builder: (context, remaining) { - return PopupMenuButton( - iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45), - padding: EdgeInsets.zero, - icon: NavigationButton( - label: context.localized.other, - selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down), - icon: const Icon(IconsaxPlusLinear.arrow_square_down), - expanded: shouldExpand, - horizontal: true, - ), - itemBuilder: (context) => views - .sublist(views.length - remaining) - .map( - (e) => PopupMenuItem( - onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)), - child: Row( - spacing: 8, - children: [ - Icon(e.collectionType.iconOutlined), - Text(e.name), - ], - ), - ), - ) - .toList(), - ); - }, - ), - ), - ], - ], - ), - ), - NavigationButton( - label: context.localized.settings, - selected: widget.currentLocation.contains(const SettingsRoute().routeName), - selectedIcon: const Icon(IconsaxPlusBold.setting_3), - horizontal: true, - expanded: shouldExpand, - icon: const SettingsUserIcon(), - onPressed: () { - if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { - context.router.push(const SettingsRoute()); - } else { - context.router.push(const ClientSettingsRoute()); - } - }, - ), + ) ], ), ), - ), - if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16), - ], + if (largeBar) ...[ + AnimatedFadeSize( + duration: const Duration(milliseconds: 250), + child: shouldExpand ? actionButton(context).extended : actionButton(context).normal, + ), + ], + Expanded( + child: Column( + mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + ...widget.destinations.mapIndexed( + (index, destination) => CustomTooltip( + tooltipContent: expandedSideBar + ? null + : Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + destination.label, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ), + position: TooltipPosition.right, + child: destination.toNavigationButton( + widget.currentIndex == index, + true, + shouldExpand, + ), + ), + ), + if (views.isNotEmpty && largeBar) ...[ + const Divider( + indent: 32, + endIndent: 32, + ), + Flexible( + child: OverflowView.flexible( + direction: Axis.vertical, + spacing: 4, + children: views.map( + (view) { + final selected = context.router.currentUrl.contains(view.id); + final actions = [ + ItemActionButton( + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + action: () => showRefreshPopup(context, view.id, view.name), + ) + ]; + return CustomTooltip( + tooltipContent: expandedSideBar + ? null + : Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + view.name, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ), + position: TooltipPosition.right, + child: view.toNavigationButton( + selected, + true, + shouldExpand, + () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), + onLongPress: () => showBottomSheetPill( + context: context, + content: (context, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: actions.listTileItems(context, useIcons: true), + ), + ), + customIcon: usePostersForLibrary + ? ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: SizedBox.square( + dimension: 50, + child: FladderImage( + image: view.imageData?.primary, + placeHolder: Card( + child: Icon( + selected + ? view.collectionType.icon + : view.collectionType.iconOutlined, + ), + ), + ), + ), + ) + : null, + trailing: actions, + ), + ); + }, + ).toList(), + builder: (context, remaining) { + return CustomTooltip( + tooltipContent: expandedSideBar + ? null + : Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + context.localized.moreOptions, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ), + position: TooltipPosition.right, + child: PopupMenuButton( + iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45), + padding: EdgeInsets.zero, + tooltip: "", + icon: NavigationButton( + label: context.localized.other, + selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down), + icon: const Icon(IconsaxPlusLinear.arrow_square_down), + expanded: shouldExpand, + customIcon: usePostersForLibrary + ? ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: const SizedBox.square( + dimension: 50, + child: Card( + child: Icon(IconsaxPlusLinear.arrow_square_down), + ), + ), + ) + : null, + horizontal: true, + ), + itemBuilder: (context) => views + .sublist(views.length - remaining) + .map( + (e) => PopupMenuItem( + onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)), + child: Row( + spacing: 8, + children: [ + usePostersForLibrary + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: SizedBox.square( + dimension: 45, + child: FladderImage( + image: e.imageData?.primary, + placeHolder: Card( + child: Icon( + e.collectionType.iconOutlined, + ), + ), + ), + ), + ), + ) + : Icon(e.collectionType.iconOutlined), + Text(e.name), + ], + ), + ), + ) + .toList(), + ), + ); + }, + ), + ), + ], + ], + ), + ), + NavigationButton( + label: context.localized.settings, + selected: widget.currentLocation.contains(const SettingsRoute().routeName), + selectedIcon: const Icon(IconsaxPlusBold.setting_3), + horizontal: true, + expanded: shouldExpand, + icon: const SettingsUserIcon(), + onPressed: () { + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { + context.router.push(const SettingsRoute()); + } else { + context.router.push(const ClientSettingsRoute()); + } + }, + ), + if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16), + ], + ), ), ), ), diff --git a/lib/widgets/navigation_scaffold/navigation_scaffold.dart b/lib/widgets/navigation_scaffold/navigation_scaffold.dart index c6cbb47..ce54583 100644 --- a/lib/widgets/navigation_scaffold/navigation_scaffold.dart +++ b/lib/widgets/navigation_scaffold/navigation_scaffold.dart @@ -10,7 +10,6 @@ import 'package:fladder/routes/auto_router.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/nested_bottom_appbar.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; -import 'package:fladder/util/theme_extensions.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; @@ -58,9 +57,15 @@ class _NavigationScaffoldState extends ConsumerState { final isDesktop = AdaptiveLayout.of(context).isDesktop; - final bottomPadding = isDesktop || kIsWeb ? 0.0 : MediaQuery.paddingOf(context).bottom; + final mediaQuery = MediaQuery.of(context); + final paddingOf = mediaQuery.padding; + final viewPaddingOf = mediaQuery.viewPadding; + + final bottomPadding = isDesktop || kIsWeb ? 0.0 : paddingOf.bottom; + final bottomViewPadding = isDesktop || kIsWeb ? 0.0 : viewPaddingOf.bottom; final isHomeScreen = currentIndex != -1; + return PopScope( canPop: currentIndex == 0, onPopInvokedWithResult: (didPop, result) { @@ -72,58 +77,66 @@ class _NavigationScaffoldState extends ConsumerState { alignment: Alignment.bottomCenter, children: [ Positioned.fill( - child: Padding( - padding: EdgeInsets.only(bottom: showPlayerBar ? floatingPlayerHeight - 12 + bottomPadding : 0), - child: Scaffold( - key: _key, - appBar: const FladderAppBar(), - extendBodyBehindAppBar: true, - resizeToAvoidBottomInset: false, - extendBody: true, - floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen - ? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal - : null, - drawer: homeRoutes.any((element) => element.name.contains(currentLocation)) - ? NestedNavigationDrawer( - actionButton: null, - toggleExpanded: (value) => _key.currentState?.closeDrawer(), - views: views, - destinations: widget.destinations, - currentLocation: currentLocation, - ) - : null, - bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone - ? HideOnScroll( - controller: AdaptiveLayout.scrollOf(context), - forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), - child: NestedBottomAppBar( - child: SizedBox( - height: 65, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: widget.destinations - .map( - (destination) => destination.toNavigationButton( - widget.currentRouteName == destination.route?.routeName, false, false), - ) - .toList(), + child: MediaQuery( + data: mediaQuery.copyWith( + padding: paddingOf.copyWith( + bottom: showPlayerBar ? floatingPlayerHeight(context) + 12 + bottomPadding : bottomPadding), + viewPadding: viewPaddingOf.copyWith( + bottom: showPlayerBar ? floatingPlayerHeight(context) + bottomViewPadding : bottomViewPadding), + ), + //Builder to correctly apply new padding + child: Builder(builder: (context) { + return Scaffold( + key: _key, + appBar: const FladderAppBar(), + extendBodyBehindAppBar: true, + resizeToAvoidBottomInset: false, + extendBody: true, + floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen + ? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal + : null, + drawer: homeRoutes.any((element) => element.name.contains(currentLocation)) + ? NestedNavigationDrawer( + actionButton: null, + toggleExpanded: (value) => _key.currentState?.closeDrawer(), + views: views, + destinations: widget.destinations, + currentLocation: currentLocation, + ) + : null, + bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone + ? HideOnScroll( + controller: AdaptiveLayout.scrollOf(context), + forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), + child: NestedBottomAppBar( + child: SizedBox( + height: 65, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: widget.destinations + .map( + (destination) => destination.toNavigationButton( + widget.currentRouteName == destination.route?.routeName, false, false), + ) + .toList(), + ), ), ), - ), - ) - : null, - body: widget.nestedChild != null - ? NavigationBody( - child: widget.nestedChild!, - parentContext: context, - currentIndex: currentIndex, - destinations: widget.destinations, - currentLocation: currentLocation, - drawerKey: _key, - ) - : null, - ), + ) + : null, + body: widget.nestedChild != null + ? NavigationBody( + child: widget.nestedChild!, + parentContext: context, + currentIndex: currentIndex, + destinations: widget.destinations, + currentLocation: currentLocation, + drawerKey: _key, + ) + : null, + ); + }), ), ), Material( @@ -131,19 +144,7 @@ class _NavigationScaffoldState extends ConsumerState { child: AnimatedFadeSize( child: Container( width: double.infinity, - decoration: BoxDecoration( - color: context.colors.surface, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), - child: showPlayerBar - ? Padding( - padding: EdgeInsets.only(bottom: bottomPadding), - child: const FloatingPlayerBar(), - ) - : const SizedBox.shrink(), + child: showPlayerBar ? const FloatingPlayerBar() : const SizedBox.shrink(), ), ), ) diff --git a/lib/widgets/shared/custom_tooltip.dart b/lib/widgets/shared/custom_tooltip.dart new file mode 100644 index 0000000..a148713 --- /dev/null +++ b/lib/widgets/shared/custom_tooltip.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class CustomTooltip extends StatefulWidget { + final Widget child; + final Widget? tooltipContent; + final double offset; + final TooltipPosition position; + final Duration showDelay; + + const CustomTooltip({ + required this.child, + required this.tooltipContent, + this.offset = 12, + this.position = TooltipPosition.top, + this.showDelay = const Duration(milliseconds: 125), + super.key, + }); + + @override + CustomTooltipState createState() => CustomTooltipState(); +} + +enum TooltipPosition { top, bottom, left, right } + +class CustomTooltipState extends State { + OverlayEntry? _overlayEntry; + Timer? _tooltipTimer; + final GlobalKey _tooltipKey = GlobalKey(); + + void _showTooltip() { + _tooltipTimer?.cancel(); + + _tooltipTimer = Timer(widget.showDelay, () { + if (_overlayEntry == null) { + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry!); + } + }); + } + + void _hideTooltip() { + _tooltipTimer?.cancel(); + _overlayEntry?.remove(); + _overlayEntry = null; + } + + OverlayEntry _createOverlayEntry() { + RenderBox renderBox = context.findRenderObject() as RenderBox; + Offset targetPosition = renderBox.localToGlobal(Offset.zero); + Size targetSize = renderBox.size; + + return OverlayEntry( + builder: (context) { + final tooltipRenderBox = _tooltipKey.currentContext?.findRenderObject() as RenderBox?; + if (tooltipRenderBox != null) { + Size tooltipSize = tooltipRenderBox.size; + + Offset tooltipPosition; + switch (widget.position) { + case TooltipPosition.top: + tooltipPosition = Offset( + targetPosition.dx + (targetSize.width - tooltipSize.width) / 2, + targetPosition.dy - tooltipSize.height - widget.offset, + ); + break; + case TooltipPosition.bottom: + tooltipPosition = Offset( + targetPosition.dx + (targetSize.width - tooltipSize.width) / 2, + targetPosition.dy + targetSize.height + widget.offset, + ); + break; + case TooltipPosition.left: + tooltipPosition = Offset( + targetPosition.dx - tooltipSize.width - widget.offset, + targetPosition.dy + (targetSize.height - tooltipSize.height) / 2, + ); + break; + case TooltipPosition.right: + tooltipPosition = Offset( + targetPosition.dx + targetSize.width + widget.offset, + targetPosition.dy + (targetSize.height - tooltipSize.height) / 2, + ); + break; + } + + return Positioned( + left: tooltipPosition.dx, + top: tooltipPosition.dy, + child: Material( + color: Colors.transparent, + child: widget.tooltipContent, + ), + ); + } + return const SizedBox.shrink(); + }, + ); + } + + @override + Widget build(BuildContext context) { + if (widget.tooltipContent == null) return widget.child; + return MouseRegion( + onEnter: (_) => _showTooltip(), + onExit: (_) => _hideTooltip(), + child: Stack( + children: [ + widget.child, + Positioned( + left: -1000, + top: -1000, + child: Container( + key: _tooltipKey, + child: widget.tooltipContent, + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _tooltipTimer?.cancel(); + _hideTooltip(); // Ensure the tooltip is hidden on dispose + super.dispose(); + } +} diff --git a/lib/widgets/shared/item_actions.dart b/lib/widgets/shared/item_actions.dart index 274cc37..c267f4c 100644 --- a/lib/widgets/shared/item_actions.dart +++ b/lib/widgets/shared/item_actions.dart @@ -10,6 +10,7 @@ abstract class ItemAction { PopupMenuEntry toPopupMenuItem({bool useIcons = false}); Widget toLabel(); Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true}); + Widget toButton(); } class ItemActionDivider extends ItemAction { @@ -26,6 +27,9 @@ class ItemActionDivider extends ItemAction { @override Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true}) => const Divider(); + + @override + Widget toButton() => Container(); } class ItemActionButton extends ItemAction { @@ -51,9 +55,10 @@ class ItemActionButton extends ItemAction { } @override - MenuItemButton toMenuItemButton() { - return MenuItemButton(leadingIcon: icon, onPressed: action, child: label); - } + MenuItemButton toMenuItemButton() => MenuItemButton(leadingIcon: icon, onPressed: action, child: label); + + @override + Widget toButton() => IconButton(onPressed: action, icon: icon ?? const SizedBox.shrink()); @override PopupMenuItem toPopupMenuItem({bool useIcons = false}) {