From e0fc990b0e1fead79e97eb3e0a978b79c07d7a46 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:49:22 +0100 Subject: [PATCH] refactor: back button unification --- lib/screens/about_screen.dart | 7 +- lib/screens/profile_switch_screen.dart | 2 +- lib/screens/season_detail_screen.dart | 8 +- lib/screens/server_selection_screen.dart | 2 +- lib/widgets/app_bar_back_button.dart | 150 +++++++++++++++-------- lib/widgets/desktop_app_bar.dart | 134 ++++++++++++++++++++ lib/widgets/plex_video_controls.dart | 2 - 7 files changed, 239 insertions(+), 66 deletions(-) diff --git a/lib/screens/about_screen.dart b/lib/screens/about_screen.dart index 0e559313..94207956 100644 --- a/lib/screens/about_screen.dart +++ b/lib/screens/about_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../widgets/desktop_app_bar.dart'; -import '../widgets/app_bar_back_button.dart'; class AboutScreen extends StatefulWidget { const AboutScreen({super.key}); @@ -36,11 +35,7 @@ class _AboutScreenState extends State { return Scaffold( body: CustomScrollView( slivers: [ - DesktopSliverAppBar( - title: const Text('About'), - pinned: true, - leading: const AppBarBackButton(style: BackButtonStyle.circular), - ), + CustomAppBar(title: const Text('About'), pinned: true), SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverList( diff --git a/lib/screens/profile_switch_screen.dart b/lib/screens/profile_switch_screen.dart index 08a4af8f..31f3f666 100644 --- a/lib/screens/profile_switch_screen.dart +++ b/lib/screens/profile_switch_screen.dart @@ -16,7 +16,7 @@ class ProfileSwitchScreen extends StatelessWidget { return Scaffold( body: CustomScrollView( slivers: [ - const DesktopSliverAppBar(title: Text('Switch Profile')), + const CustomAppBar(title: Text('Switch Profile')), SliverFillRemaining( child: Consumer( builder: (context, userProvider, child) { diff --git a/lib/screens/season_detail_screen.dart b/lib/screens/season_detail_screen.dart index 8501c59f..c86f2e49 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/screens/season_detail_screen.dart @@ -7,7 +7,6 @@ import '../models/plex_user_profile.dart'; import '../providers/plex_client_provider.dart'; import '../utils/provider_extensions.dart'; import '../widgets/desktop_app_bar.dart'; -import '../widgets/app_bar_back_button.dart'; import '../widgets/media_context_menu.dart'; import '../mixins/item_updatable.dart'; import '../theme/theme_helper.dart'; @@ -81,13 +80,10 @@ class _SeasonDetailScreenState extends State return Scaffold( body: CustomScrollView( slivers: [ - DesktopSliverAppBar( + CustomAppBar( title: Text(widget.season.title), pinned: true, - leading: AppBarBackButton( - style: BackButtonStyle.circular, - onPressed: () => Navigator.pop(context, _watchStateChanged), - ), + onBackPressed: () => Navigator.pop(context, _watchStateChanged), ), if (_isLoadingEpisodes) const SliverFillRemaining( diff --git a/lib/screens/server_selection_screen.dart b/lib/screens/server_selection_screen.dart index d7e7605a..c8d8f905 100644 --- a/lib/screens/server_selection_screen.dart +++ b/lib/screens/server_selection_screen.dart @@ -186,7 +186,7 @@ class _ServerSelectionScreenState extends State { return Scaffold( body: CustomScrollView( slivers: [ - const DesktopSliverAppBar(title: Text('Select Server')), + const CustomAppBar(title: Text('Select Server')), SliverFillRemaining( child: _isLoading ? const Center(child: CircularProgressIndicator()) diff --git a/lib/widgets/app_bar_back_button.dart b/lib/widgets/app_bar_back_button.dart index 7d517d7f..a23b924f 100644 --- a/lib/widgets/app_bar_back_button.dart +++ b/lib/widgets/app_bar_back_button.dart @@ -23,7 +23,7 @@ enum BackButtonStyle { /// ```dart /// AppBarBackButton(style: BackButtonStyle.circular) /// ``` -class AppBarBackButton extends StatelessWidget { +class AppBarBackButton extends StatefulWidget { /// Creates a back button with the specified style. /// /// [style] determines the visual appearance of the back button. @@ -45,9 +45,44 @@ class AppBarBackButton extends StatelessWidget { /// The color of the back arrow icon. If null, uses style-appropriate default. final Color? color; - void _handlePressed(BuildContext context) { - if (onPressed != null) { - onPressed!(); + @override + State createState() => _AppBarBackButtonState(); +} + +class _AppBarBackButtonState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _backgroundAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _backgroundAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _onHoverChange(bool isHovered) { + if (isHovered) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + } + + void _handlePressed() { + if (widget.onPressed != null) { + widget.onPressed!(); } else { Navigator.of(context).pop(); } @@ -55,57 +90,72 @@ class AppBarBackButton extends StatelessWidget { @override Widget build(BuildContext context) { - switch (style) { - case BackButtonStyle.circular: - return _buildCircularBackButton(context); - case BackButtonStyle.plain: - return _buildPlainBackButton(context); - case BackButtonStyle.video: - return _buildVideoBackButton(context); - } - } + final theme = Theme.of(context); + final isDarkTheme = theme.brightness == Brightness.dark; - /// Builds a back button with circular semi-transparent background - Widget _buildCircularBackButton(BuildContext context) { - return SafeArea( + final Color effectiveColor; + switch (widget.style) { + case BackButtonStyle.plain: + effectiveColor = + widget.color ?? (isDarkTheme ? Colors.white : Colors.black); + break; + case BackButtonStyle.circular: + case BackButtonStyle.video: + effectiveColor = widget.color ?? Colors.white; + break; + } + + final Color baseColor; + final Color hoverColor; + switch (widget.style) { + case BackButtonStyle.circular: + baseColor = Colors.black.withValues(alpha: 0.3); + hoverColor = Colors.black.withValues(alpha: 0.5); + break; + case BackButtonStyle.plain: + hoverColor = (isDarkTheme ? Colors.white : Colors.black).withValues( + alpha: 0.2, + ); + baseColor = Colors.transparent; + break; + case BackButtonStyle.video: + baseColor = Colors.transparent; + hoverColor = Colors.black.withValues(alpha: 0.3); + break; + } + + final button = MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => _onHoverChange(true), + onExit: (_) => _onHoverChange(false), child: GestureDetector( - onTap: () => _handlePressed(context), - child: Container( - margin: const EdgeInsets.all(8), - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.5), - shape: BoxShape.circle, - ), - child: Icon( - Icons.arrow_back, - color: color ?? Colors.white, - size: 20, - ), + onTap: _handlePressed, + child: AnimatedBuilder( + animation: _backgroundAnimation, + builder: (context, child) { + final currentColor = Color.lerp( + baseColor, + hoverColor, + _backgroundAnimation.value, + ); + + return Container( + margin: const EdgeInsets.all(8), + width: 40, + height: 40, + decoration: BoxDecoration( + color: currentColor, + shape: BoxShape.circle, + ), + child: Icon(Icons.arrow_back, color: effectiveColor, size: 20), + ); + }, ), ), ); - } - /// Builds a plain back button without background - Widget _buildPlainBackButton(BuildContext context) { - return IconButton( - icon: Icon(Icons.arrow_back, color: color), - onPressed: () => _handlePressed(context), - tooltip: 'Back', - ); - } - - /// Builds a back button styled for video player overlay - Widget _buildVideoBackButton(BuildContext context) { - return Container( - margin: const EdgeInsets.only(left: 8), - child: IconButton( - icon: Icon(Icons.arrow_back, color: color ?? Colors.white), - onPressed: () => _handlePressed(context), - tooltip: 'Back', - ), - ); + return widget.style == BackButtonStyle.circular + ? SafeArea(child: button) + : button; } } diff --git a/lib/widgets/desktop_app_bar.dart b/lib/widgets/desktop_app_bar.dart index 3d065c1f..2d39a8ba 100644 --- a/lib/widgets/desktop_app_bar.dart +++ b/lib/widgets/desktop_app_bar.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import '../utils/desktop_window_padding.dart'; +import '../services/fullscreen_state_manager.dart'; +import 'app_bar_back_button.dart'; /// A custom app bar that automatically handles desktop window controls spacing. /// Use this instead of AppBar for consistent desktop platform behavior. @@ -139,3 +141,135 @@ class DesktopSliverAppBar extends StatelessWidget { ); } } + +/// Convenient wrapper for DesktopAppBar with built-in back button handling +class PlexAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget? title; + final List? actions; + final VoidCallback? onBackPressed; + final double? elevation; + final Color? backgroundColor; + final Color? surfaceTintColor; + final Color? shadowColor; + final double? scrolledUnderElevation; + + const PlexAppBar({ + super.key, + this.title, + this.actions, + this.onBackPressed, + this.elevation, + this.backgroundColor, + this.surfaceTintColor, + this.shadowColor, + this.scrolledUnderElevation, + }); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: FullscreenStateManager(), + builder: (context, _) { + final isFullscreen = FullscreenStateManager().isFullscreen; + + return DesktopAppBar( + key: ValueKey('plex_app_bar_$isFullscreen'), + title: title, + actions: actions, + leading: _shouldShowBackButton(context) + ? AppBarBackButton( + style: BackButtonStyle.plain, + onPressed: onBackPressed, + ) + : null, + automaticallyImplyLeading: false, + elevation: elevation, + backgroundColor: backgroundColor, + surfaceTintColor: surfaceTintColor, + shadowColor: shadowColor, + scrolledUnderElevation: scrolledUnderElevation, + ); + }, + ); + } + + bool _shouldShowBackButton(BuildContext context) { + final parentRoute = ModalRoute.of(context); + return parentRoute?.canPop ?? false; + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +/// Convenient wrapper for DesktopSliverAppBar with built-in back button handling +class CustomAppBar extends StatelessWidget { + final Widget? title; + final List? actions; + final VoidCallback? onBackPressed; + final double? elevation; + final Color? backgroundColor; + final Color? surfaceTintColor; + final Color? shadowColor; + final double? scrolledUnderElevation; + final bool floating; + final bool pinned; + final double? expandedHeight; + final Widget? flexibleSpace; + final PreferredSizeWidget? bottom; + + const CustomAppBar({ + super.key, + this.title, + this.actions, + this.onBackPressed, + this.elevation, + this.backgroundColor, + this.surfaceTintColor, + this.shadowColor, + this.scrolledUnderElevation, + this.floating = false, + this.pinned = false, + this.expandedHeight, + this.flexibleSpace, + this.bottom, + }); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: FullscreenStateManager(), + builder: (context, _) { + final isFullscreen = FullscreenStateManager().isFullscreen; + + return DesktopSliverAppBar( + key: ValueKey('plex_sliver_app_bar_$isFullscreen'), + title: title, + actions: actions, + leading: _shouldShowBackButton(context) + ? AppBarBackButton( + style: BackButtonStyle.plain, + onPressed: onBackPressed, + ) + : null, + automaticallyImplyLeading: false, + elevation: elevation, + backgroundColor: backgroundColor, + surfaceTintColor: surfaceTintColor, + shadowColor: shadowColor, + scrolledUnderElevation: scrolledUnderElevation, + floating: floating, + pinned: pinned, + expandedHeight: expandedHeight, + flexibleSpace: flexibleSpace, + bottom: bottom, + ); + }, + ); + } + + bool _shouldShowBackButton(BuildContext context) { + final parentRoute = ModalRoute.of(context); + return parentRoute?.canPop ?? false; + } +} diff --git a/lib/widgets/plex_video_controls.dart b/lib/widgets/plex_video_controls.dart index 4da28372..fb57c475 100644 --- a/lib/widgets/plex_video_controls.dart +++ b/lib/widgets/plex_video_controls.dart @@ -789,8 +789,6 @@ class _PlexVideoControlsState extends State padding: EdgeInsets.only( left: leftPadding, right: 16, - top: isMacOS ? 8 : 16, - bottom: isMacOS ? 8 : 16, ), child: Row( children: [