refactor: back button unification

This commit is contained in:
edde746
2025-10-31 00:51:45 +01:00
parent 36b897f963
commit e0fc990b0e
7 changed files with 239 additions and 66 deletions
+1 -6
View File
@@ -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<AboutScreen> {
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(
+1 -1
View File
@@ -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<UserProfileProvider>(
builder: (context, userProvider, child) {
+2 -6
View File
@@ -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<SeasonDetailScreen>
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(
+1 -1
View File
@@ -186,7 +186,7 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
return Scaffold(
body: CustomScrollView(
slivers: [
const DesktopSliverAppBar(title: Text('Select Server')),
const CustomAppBar(title: Text('Select Server')),
SliverFillRemaining(
child: _isLoading
? const Center(child: CircularProgressIndicator())
+100 -50
View File
@@ -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<AppBarBackButton> createState() => _AppBarBackButtonState();
}
class _AppBarBackButtonState extends State<AppBarBackButton>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _backgroundAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_backgroundAnimation = Tween<double>(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;
}
}
+134
View File
@@ -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<Widget>? 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<Widget>? 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;
}
}
-2
View File
@@ -789,8 +789,6 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
padding: EdgeInsets.only(
left: leftPadding,
right: 16,
top: isMacOS ? 8 : 16,
bottom: isMacOS ? 8 : 16,
),
child: Row(
children: [