Refactor HomePage layout to improve audio player integration and enhance UI responsiveness

This commit is contained in:
Leon Bösche
2026-01-16 16:09:07 +01:00
parent 0b2a9bad2f
commit 072564fb0f
2 changed files with 169 additions and 73 deletions

View File

@@ -405,35 +405,78 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
backgroundColor: AppTheme.primaryBackground, backgroundColor: AppTheme.primaryBackground,
body: Stack( body: Stack(
children: [ children: [
// Audio bar between title and button row // Title and audio bar row
Positioned( Positioned(
top: MediaQuery.of(context).size.width < 600 ? 36 : 60, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: AnimatedBuilder( child: Padding(
animation: _audioBarController, padding: EdgeInsets.only(
builder: (context, child) { top: MediaQuery.of(context).size.width < 600 ? 16 : 24,
return (_showAudioBar && left: 32,
_audioFileName != null && right: 32,
_audioFileUrl != null) ),
? SlideTransition( child: Row(
position: _audioBarOffset, crossAxisAlignment: CrossAxisAlignment.start,
child: Padding( children: [
padding: const EdgeInsets.symmetric( // Title
horizontal: 32.0, Expanded(
), flex: 3,
child: AudioPlayerBar( child: Align(
fileName: _audioFileName!, alignment: Alignment.centerLeft,
fileUrl: _audioFileUrl!, child: Builder(
onClose: () { builder: (context) {
_audioBarController.reverse(); final screenWidth = MediaQuery.of(
setState(() => _showAudioBar = false); context,
}, ).size.width;
), final fontSize = screenWidth < 600 ? 24.0 : 48.0;
), return Text(
) 'b0esche.cloud',
: const SizedBox.shrink(); style: TextStyle(
}, fontFamily: 'PixelatedElegance',
fontSize: fontSize,
color: AppTheme.primaryText,
decoration: TextDecoration.underline,
decorationColor: AppTheme.primaryText,
fontFeatures: const [FontFeature.slashedZero()],
),
);
},
),
),
),
// Audio bar (max 1/4 width, right-aligned)
Expanded(
flex: 1,
child: AnimatedBuilder(
animation: _audioBarController,
builder: (context, child) {
return (_showAudioBar &&
_audioFileName != null &&
_audioFileUrl != null)
? Align(
alignment: Alignment.topRight,
child: FractionallySizedBox(
widthFactor: 1.0,
child: SlideTransition(
position: _audioBarOffset,
child: AudioPlayerBar(
fileName: _audioFileName!,
fileUrl: _audioFileUrl!,
onClose: () {
_audioBarController.reverse();
setState(() => _showAudioBar = false);
},
),
),
),
)
: const SizedBox.shrink();
},
),
),
],
),
), ),
), ),
Center( Center(

View File

@@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../theme/modern_glass_button.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
class AudioPlayerBar extends StatefulWidget { class AudioPlayerBar extends StatefulWidget {
@@ -56,34 +59,29 @@ class _AudioPlayerBarState extends State<AudioPlayerBar> {
} }
} }
@override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
String _formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(d.inMinutes.remainder(60));
final seconds = twoDigits(d.inSeconds.remainder(60));
return '$minutes:$seconds';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return AnimatedContainer(
elevation: 8, duration: const Duration(milliseconds: 300),
color: Theme.of(context).colorScheme.surface, height: 64,
child: AnimatedContainer( decoration: AppTheme.glassDecoration.copyWith(
duration: const Duration(milliseconds: 300), borderRadius: BorderRadius.circular(16),
height: 64, boxShadow: [
padding: const EdgeInsets.symmetric(horizontal: 16), BoxShadow(
child: Row( color: AppTheme.accentColor.withOpacity(0.18),
children: [ blurRadius: 16,
IconButton( offset: const Offset(0, 4),
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow), ),
],
),
child: Row(
children: [
// Animated play/pause button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ModernGlassButton(
onPressed: _isLoading onPressed: _isLoading
? null ? () {} // no-op when loading
: () { : () {
if (_isPlaying) { if (_isPlaying) {
_audioPlayer.pause(); _audioPlayer.pause();
@@ -91,18 +89,50 @@ class _AudioPlayerBarState extends State<AudioPlayerBar> {
_audioPlayer.play(); _audioPlayer.play();
} }
}, },
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
transitionBuilder: (child, anim) =>
ScaleTransition(scale: anim, child: child),
child: _isPlaying
? const Icon(
Icons.pause,
key: ValueKey('pause'),
color: AppTheme.primaryText,
)
: const Icon(
Icons.play_arrow,
key: ValueKey('play'),
color: AppTheme.primaryText,
),
),
), ),
Expanded( ),
child: Column( // File name and slider
mainAxisAlignment: MainAxisAlignment.center, Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
Text( crossAxisAlignment: CrossAxisAlignment.start,
widget.fileName, children: [
style: Theme.of(context).textTheme.bodyLarge, Text(
overflow: TextOverflow.ellipsis, widget.fileName,
style: const TextStyle(
color: AppTheme.primaryText,
fontWeight: FontWeight.w600,
fontSize: 15,
), ),
Slider( overflow: TextOverflow.ellipsis,
),
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 3,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 7,
),
overlayShape: SliderComponentShape.noOverlay,
activeTrackColor: AppTheme.accentColor,
inactiveTrackColor: AppTheme.accentColor.withOpacity(0.2),
),
child: Slider(
min: 0, min: 0,
max: _duration.inMilliseconds.toDouble(), max: _duration.inMilliseconds.toDouble(),
value: _position.inMilliseconds value: _position.inMilliseconds
@@ -115,25 +145,48 @@ class _AudioPlayerBarState extends State<AudioPlayerBar> {
Duration(milliseconds: value.toInt()), Duration(milliseconds: value.toInt()),
); );
}, },
activeColor: AppTheme.accentColor,
inactiveColor: AppTheme.accentColor.withOpacity(0.2),
), ),
], ),
],
),
),
// Time
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'${_formatDuration(_position)} / ${_formatDuration(_duration)}',
style: const TextStyle(
color: AppTheme.secondaryText,
fontSize: 13,
fontWeight: FontWeight.w500,
), ),
), ),
),
// Close button
if (widget.onClose != null)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: Text( child: ModernGlassButton(
'${_formatDuration(_position)} / ${_formatDuration(_duration)}', onPressed: widget.onClose!,
style: Theme.of(context).textTheme.bodySmall, child: const Icon(
Icons.close,
color: AppTheme.primaryText,
size: 20,
),
), ),
), ),
if (widget.onClose != null) ],
IconButton(
icon: const Icon(Icons.close),
onPressed: widget.onClose,
),
],
),
), ),
); );
} }
// Utility to format duration
String _formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(d.inMinutes.remainder(60));
final seconds = twoDigits(d.inSeconds.remainder(60));
return '$minutes:$seconds';
}
} }