didvan-app/lib/views/story_viewer/story_viewer_page.dart

420 lines
12 KiB
Dart

// 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<UserStories> stories;
final int tappedIndex;
const StoryViewerPage({
super.key,
required this.stories,
required this.tappedIndex,
});
@override
State<StoryViewerPage> createState() => _StoryViewerPageState();
}
class _StoryViewerPageState extends State<StoryViewerPage> {
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<UserStoryViewer> createState() => _UserStoryViewerState();
}
class _UserStoryViewerState extends State<UserStoryViewer>
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:
debugPrint("Playing story video from URL: ${story.url}");
_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();
}
}).timeout(const Duration(seconds: 10), onTimeout: () {
// ignore: avoid_print
print("Video initialization timed out. 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: <Widget>[
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: <Widget>[
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(),
),
],
);
}
}