Refactor HomePage layout to improve audio player integration and enhance UI responsiveness
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user