// lib/views/story_viewer/story_viewer_page.dart import 'package:cached_network_image/cached_network_image.dart'; import 'package:didvan/models/story_model.dart'; import 'package:didvan/services/story_service.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; class StoryViewerPage extends StatefulWidget { final List stories; final int tappedIndex; const StoryViewerPage({ super.key, required this.stories, required this.tappedIndex, }); @override State createState() => _StoryViewerPageState(); } class _StoryViewerPageState extends State { late PageController _pageController; @override void initState() { super.initState(); _pageController = PageController(initialPage: widget.tappedIndex); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Directionality( textDirection: TextDirection.ltr, child: PageView.builder( controller: _pageController, itemCount: widget.stories.length, itemBuilder: (context, index) { final userStories = widget.stories[index]; return UserStoryViewer( key: ValueKey(userStories.user.name + index.toString()), userStories: userStories, onComplete: () { if (index < widget.stories.length - 1) { _pageController.nextPage( duration: const Duration(milliseconds: 300), curve: Curves.easeIn, ); } else { Navigator.of(context).pop(); } }, ); }, ), ), ); } } class UserStoryViewer extends StatefulWidget { final UserStories userStories; final VoidCallback onComplete; const UserStoryViewer({ super.key, required this.userStories, required this.onComplete, }); @override State createState() => _UserStoryViewerState(); } class _UserStoryViewerState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; VideoPlayerController? _videoController; int _currentStoryIndex = 0; @override void initState() { super.initState(); _animationController = AnimationController(vsync: this); // final allStoriesInGroupViewed = // widget.userStories.stories.every((story) => story.isViewed.value); // if (allStoriesInGroupViewed) { // for (final story in widget.userStories.stories) { // story.isViewed.value = false; // } // } _currentStoryIndex = widget.userStories.stories.indexWhere((story) => !story.isViewed.value); if (_currentStoryIndex == -1) { _currentStoryIndex = 0; } _loadStory(story: widget.userStories.stories[_currentStoryIndex]); _animationController.addStatusListener((status) { if (status == AnimationStatus.completed) { _nextStory(); } }); } @override void dispose() { _animationController.dispose(); _videoController?.dispose(); super.dispose(); } void _loadStory({required StoryItem story}) { if (!story.isViewed.value) { StoryService.markStoryAsViewed(widget.userStories.id, story.id); story.isViewed.value = true; } _animationController.stop(); _animationController.reset(); _videoController?.dispose(); _videoController = null; switch (story.media) { case MediaType.image: case MediaType.gif: _animationController.duration = story.duration; _animationController.forward(); break; case MediaType.video: _videoController = VideoPlayerController.networkUrl(Uri.parse(story.url)); _videoController!.initialize().then((_) { if (mounted) { setState(() { if (_videoController!.value.isInitialized && _videoController!.value.duration > Duration.zero) { _animationController.duration = _videoController!.value.duration; _videoController!.play(); _animationController.forward(); } else { // ignore: avoid_print print( "Video failed to initialize or has zero duration. Skipping."); _nextStory(); } }); } }).catchError((error) { // ignore: avoid_print print("Error loading video: $error. Skipping."); if (mounted) { _nextStory(); } }); break; } setState(() {}); } void _nextStory() { if (_currentStoryIndex < widget.userStories.stories.length - 1) { setState(() { _currentStoryIndex++; }); _loadStory(story: widget.userStories.stories[_currentStoryIndex]); } else { widget.onComplete(); } } void _previousStory() { if (_currentStoryIndex > 0) { setState(() { _currentStoryIndex--; }); _loadStory(story: widget.userStories.stories[_currentStoryIndex]); } } void _pauseStory() { _animationController.stop(); _videoController?.pause(); } void _resumeStory() { _animationController.forward(); _videoController?.play(); } @override Widget build(BuildContext context) { final story = widget.userStories.stories[_currentStoryIndex]; return Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ _buildMediaViewer(story), Row( children: [ Expanded( child: GestureDetector( onTap: _previousStory, onLongPress: _pauseStory, onLongPressUp: _resumeStory, child: Container(color: Colors.transparent), ), ), Expanded( child: GestureDetector( onTap: _nextStory, onLongPress: _pauseStory, onLongPressUp: _resumeStory, child: Container(color: Colors.transparent), ), ), ], ), _buildStoryHeader(), ], ), ); } Widget _buildMediaViewer(StoryItem story) { switch (story.media) { case MediaType.image: return CachedNetworkImage( placeholder: (context, url) => const ShimmerPlaceholder(), imageUrl: story.url, fit: BoxFit.fill, width: double.infinity, height: double.infinity); case MediaType.gif: return Image.network(story.url, fit: BoxFit.fill, width: double.infinity, height: double.infinity); case MediaType.video: if (_videoController?.value.isInitialized ?? false) { return FittedBox( fit: BoxFit.cover, child: SizedBox( width: _videoController!.value.size.width, height: _videoController!.value.size.height, child: VideoPlayer(_videoController!))); } return const Center(child: CircularProgressIndicator()); } } Widget _buildStoryHeader() { return Positioned( top: 35.0, left: 10.0, right: 10.0, child: Column( children: [ Directionality( textDirection: TextDirection.ltr, child: Row( children: widget.userStories.stories .asMap() .map((i, e) { return MapEntry( i, _AnimatedBar( animationController: _animationController, position: i, currentIndex: _currentStoryIndex, ), ); }) .values .toList(), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: _UserInfo(user: widget.userStories.user), ), ], ), ); } } class _AnimatedBar extends StatelessWidget { final AnimationController animationController; final int position; final int currentIndex; const _AnimatedBar({ required this.animationController, required this.position, required this.currentIndex, }); @override Widget build(BuildContext context) { return Flexible( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 1.5), child: LayoutBuilder( builder: (context, constraints) { return Stack( children: [ Container( height: 6.0, width: double.infinity, decoration: BoxDecoration( color: position < currentIndex ? Colors.white // ignore: deprecated_member_use : Colors.white.withOpacity(0.5), border: Border.all(color: Colors.black26, width: 0.8), borderRadius: BorderRadius.circular(30.0), ), ), if (position == currentIndex) Align( alignment: Alignment.centerLeft, child: AnimatedBuilder( animation: animationController, builder: (context, child) { return Container( height: 6.0, width: constraints.maxWidth * animationController.value, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(30.0), ), ); }, ), ), ], ); }, ), ), ); } } class _UserInfo extends StatelessWidget { final User user; const _UserInfo({required this.user}); @override Widget build(BuildContext context) { return Row( children: [ CircleAvatar( radius: 20.0, backgroundColor: Colors.grey[300], child: ClipOval( child: Padding( padding: const EdgeInsets.all(0.0), child: Image.asset( user.profileImageUrl, width: 40.0, height: 40.0, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Icon(Icons.person, color: Colors.grey, size: 40.0); }, ), ), ), ), const SizedBox(width: 10.0), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.name, style: const TextStyle( color: Colors.white, fontSize: 18.0, fontWeight: FontWeight.w600, ), ), ], ), ), IconButton( icon: const Icon(Icons.close, size: 30.0, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), ], ); } }