refactor: back button unification
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user