Files
afterwork/lib/presentation/widgets/social/fullscreen_video_player.dart
dahoud 92612abbd7 fix(chat): Correction race condition + Implémentation TODOs
## Corrections Critiques

### Race Condition - Statuts de Messages
- Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas
- Cause : WebSocket delivery confirmations arrivaient avant messages locaux
- Solution : Pattern Optimistic UI dans chat_bloc.dart
  - Création message temporaire immédiate
  - Ajout à la liste AVANT requête HTTP
  - Remplacement par message serveur à la réponse
- Fichier : lib/presentation/state_management/chat_bloc.dart

## Implémentation TODOs (13/21)

### Social (social_header_widget.dart)
-  Copier lien du post dans presse-papiers
-  Partage natif via Share.share()
-  Dialogue de signalement avec 5 raisons

### Partage (share_post_dialog.dart)
-  Interface sélection d'amis avec checkboxes
-  Partage externe via Share API

### Média (media_upload_service.dart)
-  Parsing JSON réponse backend
-  Méthode deleteMedia() pour suppression
-  Génération miniature vidéo

### Posts (create_post_dialog.dart, edit_post_dialog.dart)
-  Extraction URL depuis uploads
-  Documentation chargement médias

### Chat (conversations_screen.dart)
-  Navigation vers notifications
-  ConversationSearchDelegate pour recherche

## Nouveaux Fichiers

### Configuration
- build-prod.ps1 : Script build production avec dart-define
- lib/core/constants/env_config.dart : Gestion environnements

### Documentation
- TODOS_IMPLEMENTED.md : Documentation complète TODOs

## Améliorations

### Architecture
- Refactoring injection de dépendances
- Amélioration routing et navigation
- Optimisation providers (UserProvider, FriendsProvider)

### UI/UX
- Amélioration thème et couleurs
- Optimisation animations
- Meilleure gestion erreurs

### Services
- Configuration API avec env_config
- Amélioration datasources (events, users)
- Optimisation modèles de données
2026-01-10 10:43:17 +00:00

390 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
import '../../../core/constants/design_system.dart';
/// Lecteur vidéo plein écran avec contrôles.
///
/// Affiche une vidéo en plein écran avec contrôles de lecture,
/// barre de progression et gestion de l'orientation.
class FullscreenVideoPlayer extends StatefulWidget {
const FullscreenVideoPlayer({
required this.videoUrl,
this.heroTag,
this.title,
super.key,
});
final String videoUrl;
final String? heroTag;
final String? title;
@override
State<FullscreenVideoPlayer> createState() => _FullscreenVideoPlayerState();
/// Affiche le lecteur vidéo en plein écran.
static Future<void> show({
required BuildContext context,
required String videoUrl,
String? heroTag,
String? title,
}) {
return Navigator.push<void>(
context,
PageRouteBuilder<void>(
opaque: false,
barrierColor: Colors.black,
pageBuilder: (context, animation, secondaryAnimation) {
return FadeTransition(
opacity: animation,
child: FullscreenVideoPlayer(
videoUrl: videoUrl,
heroTag: heroTag,
title: title,
),
);
},
),
);
}
}
class _FullscreenVideoPlayerState extends State<FullscreenVideoPlayer> {
late VideoPlayerController _controller;
bool _isInitialized = false;
bool _showControls = true;
bool _isPlaying = false;
@override
void initState() {
super.initState();
_initializePlayer();
_setLandscapeOrientation();
}
@override
void dispose() {
_resetOrientation();
_controller.dispose();
super.dispose();
}
Future<void> _initializePlayer() async {
// Déterminer si c'est une URL réseau ou locale
if (widget.videoUrl.startsWith('http')) {
_controller = VideoPlayerController.networkUrl(
Uri.parse(widget.videoUrl),
);
} else {
// Pour les fichiers locaux (pas encore implémenté)
// _controller = VideoPlayerController.file(File(widget.videoUrl));
_controller = VideoPlayerController.networkUrl(
Uri.parse(widget.videoUrl),
);
}
try {
await _controller.initialize();
_controller.addListener(_videoListener);
if (mounted) {
setState(() {
_isInitialized = true;
});
// Démarrer la lecture automatiquement
_controller.play();
_isPlaying = true;
}
} catch (e) {
debugPrint('[FullscreenVideoPlayer] Erreur initialisation: $e');
}
}
void _videoListener() {
if (_controller.value.isPlaying != _isPlaying) {
setState(() {
_isPlaying = _controller.value.isPlaying;
});
}
}
Future<void> _setLandscapeOrientation() async {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
}
Future<void> _resetOrientation() async {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
}
void _togglePlayPause() {
setState(() {
if (_controller.value.isPlaying) {
_controller.pause();
} else {
_controller.play();
}
});
}
void _toggleControls() {
setState(() {
_showControls = !_showControls;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onTap: _toggleControls,
child: Stack(
fit: StackFit.expand,
children: [
// Vidéo
Center(
child: _isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
),
// Contrôles
if (_showControls) ...[
// Header
_buildHeader(context),
// Contrôles centraux
_buildCenterControls(),
// Footer avec barre de progression
_buildFooter(),
],
],
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(DesignSystem.spacingMd),
child: Row(
children: [
// Bouton retour
IconButton(
icon: const Icon(
Icons.close_rounded,
color: Colors.white,
size: 28,
),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(width: DesignSystem.spacingMd),
// Titre
if (widget.title != null && widget.title!.isNotEmpty)
Expanded(
child: Text(
widget.title!,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
),
);
}
Widget _buildCenterControls() {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Reculer de 10s
_buildControlButton(
icon: Icons.replay_10_rounded,
onTap: () {
final currentPosition = _controller.value.position;
final newPosition = currentPosition - const Duration(seconds: 10);
_controller.seekTo(
newPosition < Duration.zero ? Duration.zero : newPosition,
);
},
),
const SizedBox(width: DesignSystem.spacingXl),
// Play/Pause
_buildControlButton(
icon: _isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
onTap: _togglePlayPause,
size: 80,
),
const SizedBox(width: DesignSystem.spacingXl),
// Avancer de 10s
_buildControlButton(
icon: Icons.forward_10_rounded,
onTap: () {
final currentPosition = _controller.value.position;
final duration = _controller.value.duration;
final newPosition = currentPosition + const Duration(seconds: 10);
_controller.seekTo(
newPosition > duration ? duration : newPosition,
);
},
),
],
),
);
}
Widget _buildControlButton({
required IconData icon,
required VoidCallback onTap,
double size = 60,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.all(size / 5),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: size / 1.5,
),
),
);
}
Widget _buildFooter() {
if (!_isInitialized) return const SizedBox.shrink();
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(DesignSystem.spacingMd),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Barre de progression
VideoProgressIndicator(
_controller,
allowScrubbing: true,
colors: const VideoProgressColors(
playedColor: Colors.red,
bufferedColor: Colors.white54,
backgroundColor: Colors.white24,
),
padding: EdgeInsets.zero,
),
const SizedBox(height: DesignSystem.spacingSm),
// Temps
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDuration(_controller.value.position),
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
Text(
_formatDuration(_controller.value.duration),
style: const TextStyle(
color: Colors.white70,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
),
),
);
}
String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}