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/utils/date_time.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:video_player/video_player.dart'; // Main PageViewer to swipe between users 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; bool _isLongPressing = false; @override void initState() { super.initState(); _animationController = AnimationController(vsync: this); _loadStory(story: widget.userStories.stories.first); _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 { print( "Video failed to initialize or has zero duration. Skipping."); _nextStory(); } }); } }).catchError((error) { 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(); } void _handleTap(TapUpDetails details) { if (_isLongPressing) { _isLongPressing = false; _resumeStory(); return; } final double screenWidth = MediaQuery.of(context).size.width; final double dx = details.globalPosition.dx; if (dx > screenWidth / 2) { _nextStory(); } else { _previousStory(); } } @override Widget build(BuildContext context) { final story = widget.userStories.stories[_currentStoryIndex]; return Scaffold( backgroundColor: Colors.black, body: GestureDetector( onTapUp: _handleTap, onLongPressStart: (_) { _isLongPressing = true; _pauseStory(); }, onLongPressEnd: (_) { _isLongPressing = false; _resumeStory(); }, child: Stack( fit: StackFit.expand, children: [ _buildMediaViewer(story), _buildStoryHeader(), ], ), ), ); } Widget _buildMediaViewer(StoryItem story) { switch (story.media) { case MediaType.image: return CachedNetworkImage( imageUrl: story.url, fit: BoxFit.cover, width: double.infinity, height: double.infinity); case MediaType.gif: return Image.network(story.url, fit: BoxFit.cover, 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 : 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, ), ), DidvanText( DateTimeUtils.momentGenerator(user.createdAt), style: const TextStyle( color: Colors.white70, fontSize: 14.0, ), ), ], ), ), IconButton( icon: const Icon(Icons.close, size: 30.0, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), ], ); } }