didvan-app/lib/views/home/main/widgets/didvan_voice_detail_card.dart

274 lines
11 KiB
Dart

import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/didvan_voice_model.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/media/voice.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:just_audio/just_audio.dart';
class DidvanVoiceDetailCard extends StatefulWidget {
final DidvanVoiceModel didvanVoice;
const DidvanVoiceDetailCard({
super.key,
required this.didvanVoice,
});
@override
State<DidvanVoiceDetailCard> createState() => _DidvanVoiceDetailCardState();
}
class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
late AudioPlayer _audioPlayer;
bool _isInitialized = false;
@override
void initState() {
super.initState();
_initializeAudio();
}
void _initializeAudio() async {
_audioPlayer = VoiceService.audioPlayer;
final audioUrl =
'${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}';
debugPrint('🎙️ Didvan Voice Audio URL: $audioUrl');
try {
VoiceService.src = audioUrl;
await _audioPlayer.setUrl(audioUrl);
if (mounted) {
setState(() {
_isInitialized = true;
});
}
} catch (e) {
debugPrint('❌ Audio initialization error: $e');
}
}
@override
void dispose() {
super.dispose();
}
String _formatDuration(Duration? duration) {
if (duration == null) return '00:00';
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
@override
Widget build(BuildContext context) {
final imageUrl = '${RequestHelper.baseUrl}${widget.didvanVoice.image}';
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: DesignConfig.isDark
? const Color.fromARGB(255, 62, 62, 62)
: const Color.fromARGB(255, 235, 235, 235),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 2),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: SkeletonImage(
imageUrl: imageUrl,
width: double.infinity,
height: 200,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
DidvanText(
widget.didvanVoice.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: DesignConfig.isDark ? Colors.white : Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 16),
Directionality(
textDirection: TextDirection.ltr,
child: StreamBuilder<Duration?>(
stream: _audioPlayer.durationStream,
builder: (context, snapshot) {
final duration = snapshot.data ?? Duration.zero;
return StreamBuilder<Duration>(
stream: _audioPlayer.positionStream,
builder: (context, snapshot) {
var position = snapshot.data ?? Duration.zero;
if (position > duration) {
position = duration;
}
return Row(
children: [
SizedBox(
width: 45,
child: Text(
_formatDuration(position),
style: TextStyle(
fontSize: 12,
// ignore: deprecated_member_use
color: Theme.of(context)
.colorScheme
.caption),
textAlign: TextAlign.left,
),
),
const SizedBox(width: 8),
Expanded(
child: SliderTheme(
data: const SliderThemeData(
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 6),
overlayShape: RoundSliderOverlayShape(
overlayRadius: 12),
trackHeight: 4,
thumbColor:
Color.fromARGB(255, 0, 126, 167),
activeTrackColor:
Color.fromARGB(255, 0, 126, 167),
inactiveTrackColor: Colors.grey,
),
child: Slider(
value: position.inMilliseconds.toDouble(),
max: duration.inMilliseconds.toDouble() > 0
? duration.inMilliseconds.toDouble()
: 1.0,
onChanged: (value) {
_audioPlayer.seek(Duration(
milliseconds: value.toInt()));
},
),
),
),
const SizedBox(width: 8),
SizedBox(
width: 45,
child: Text(
_formatDuration(duration),
style: TextStyle(
fontSize: 12,
// ignore: deprecated_member_use
color: Theme.of(context)
.colorScheme
.caption),
textAlign: TextAlign.right,
),
),
],
);
},
);
},
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
final newPosition =
_audioPlayer.position + const Duration(seconds: 10);
_audioPlayer.seek(newPosition);
},
icon: SvgPicture.asset(
'lib/assets/icons/forward-10-seconds.svg',
width: 30,
height: 30,
color: DesignConfig.isDark
? const Color.fromARGB(255, 117, 117, 117)
: null),
),
const SizedBox(width: 15),
StreamBuilder<PlayerState>(
stream: _audioPlayer.playerStateStream,
builder: (context, snapshot) {
final playerState = snapshot.data;
final processingState = playerState?.processingState ??
ProcessingState.idle;
final playing = playerState?.playing ?? false;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
return const SizedBox(
width: 50,
height: 50,
child: Center(
child: CircularProgressIndicator(
color: Color(0xFF3B82F6),
strokeWidth: 3,
),
),
);
}
return GestureDetector(
onTap: () {
if (playing) {
_audioPlayer.pause();
} else {
_audioPlayer.play();
}
},
child: SvgPicture.asset(
playing
? 'lib/assets/icons/pause-circle.svg'
: 'lib/assets/icons/play.svg',
width: 50,
height: 50,
),
);
},
),
const SizedBox(width: 15),
IconButton(
onPressed: () {
final newPosition =
_audioPlayer.position - const Duration(seconds: 5);
_audioPlayer.seek(newPosition < Duration.zero
? Duration.zero
: newPosition);
},
icon: SvgPicture.asset(
'lib/assets/icons/backward-5-seconds.svg',
width: 30,
height: 30,
color: DesignConfig.isDark
? const Color.fromARGB(255, 117, 117, 117)
: null),
),
],
),
],
),
),
],
),
);
}
}