didvan-app/lib/views/home/media/video_details_page.dart

734 lines
30 KiB
Dart

// ignore_for_file: use_build_context_synchronously, deprecated_member_use
import 'package:chewie/chewie.dart';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/comments/comments.dart';
import 'package:didvan/views/comments/comments_state.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:didvan/views/podcasts/studio_details/widgets/studio_details_widget.dart';
import 'package:didvan/views/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/overview/multitype.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:didvan/views/widgets/tag_item.dart';
import 'package:didvan/views/widgets/video/primary_controls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:video_player/video_player.dart';
class VideoDetailsPage extends StatefulWidget {
final Map<String, dynamic> pageData;
const VideoDetailsPage({Key? key, required this.pageData}) : super(key: key);
@override
State<VideoDetailsPage> createState() => _VideoDetailsPageState();
}
class _VideoDetailsPageState extends State<VideoDetailsPage>
with TickerProviderStateMixin {
int _currentlyPlayingId = 0;
VideoPlayerController? _videoPlayerController;
ChewieController? _chewieController;
bool _isDescriptionExpanded = false;
final _focusNode = FocusNode();
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
final GlobalKey<AnimatedListState> _relatedContentKey =
GlobalKey<AnimatedListState>();
final GlobalKey<AnimatedListState> _commentsKey =
GlobalKey<AnimatedListState>();
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeIn,
),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.2), end: Offset.zero).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
),
);
final state = context.read<StudioDetailsState>();
state.args = widget.pageData['args'];
Future.delayed(
Duration.zero,
() => state.getStudioDetails(widget.pageData['id']).then((_) {
if (mounted) {
_initializePlayer(state.studio);
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
state.getRelatedContents();
_animationController.forward();
}
});
}
}),
);
}
Future<void> _initializePlayer(StudioDetailsData studio) async {
if (studio.type == 'video') {
_videoPlayerController?.dispose();
_chewieController?.dispose();
debugPrint("Playing video from URL: ${studio.link}");
_videoPlayerController = VideoPlayerController.network(studio.link);
try {
await _videoPlayerController!.initialize();
if (mounted) {
setState(() {
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController!,
customControls: const PrimaryControls(),
autoPlay: true,
looping: true,
aspectRatio: 16 / 9,
materialProgressColors: ChewieProgressColors(
playedColor: Theme.of(context).colorScheme.title,
handleColor: Theme.of(context).colorScheme.title,
),
);
_currentlyPlayingId = studio.id;
});
}
} catch (e) {
debugPrint("Error initializing video player: $e");
}
}
}
@override
Widget build(BuildContext context) {
return Consumer<StudioDetailsState>(
builder: (context, state, child) {
if (state.isStudioLoaded && _currentlyPlayingId != state.studio.id) {
Future.microtask(() => _initializePlayer(state.studio));
}
return StateHandler<StudioDetailsState>(
state: state,
onRetry: () {
try {
state.getStudioDetails(state.studio.id);
} catch (e) {
state.getStudioDetails(widget.pageData['id']);
}
},
builder: (context, state) {
if (!state.isStudioLoaded) {
return Scaffold(
body: Center(
child: Image.asset(
Assets.loadingAnimation,
width: 100,
height: 100,
),
),
);
}
return WillPopScope(
onWillPop: () async {
if (MediaService.currentPodcast != null) {
state.studio = MediaService.currentPodcast!;
}
state.handleTracking(id: state.studio.id);
return true;
},
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(90.0),
child: AppBar(
scrolledUnderElevation: 0,
surfaceTintColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Center(
child: SvgPicture.asset(
'lib/assets/images/logos/logo-horizontal-light.svg',
height: 55,
),
),
IconButton(
icon: SvgPicture.asset(
'lib/assets/icons/arrow-left.svg',
color: Theme.of(context).colorScheme.caption,
height: 24,
),
onPressed: () {
Navigator.pop(context);
},
),
],
),
),
),
)),
body: SingleChildScrollView(
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: 'video-${state.studio.id}',
child: AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
children: [
(_chewieController != null &&
_chewieController!
.videoPlayerController
.value
.isInitialized)
? Chewie(controller: _chewieController!)
: Center(
child: Image.asset(
Assets.loadingAnimation,
width: 100,
height: 100,
),
),
Positioned(
top: 1,
left: 1,
child: Row(
children: [
Container(
height: 36,
decoration: BoxDecoration(
color:
Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: IconButton(
icon: SvgPicture.asset(
'lib/assets/icons/fluent_mention-32-regular.svg',
width: 28,
height: 28,
),
onPressed: () =>
Navigator.of(context).pushNamed(
Routes.mentions,
arguments: {
'id': state.studio.id,
'type': 'studio',
'title': state.studio.title,
},
),
),
),
BookmarkButton(
value: state.studio.marked,
onMarkChanged: (value) {
if (widget.pageData[
'onMarkChanged'] !=
null) {
widget.pageData['onMarkChanged'](
state.studio.id, value);
}
},
gestureSize: 35,
type: 'video',
itemId: state.studio.id,
),
],
),
),
],
),
),
),
_buildDescriptionSection(state),
_buildRelatedContentSection(state),
_buildCommentsSection(state),
],
),
),
),
),
),
);
},
);
},
);
}
Widget _buildDescriptionSection(StudioDetailsState state) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: KeyedSubtree(
key: ValueKey('description-${state.studio.id}'),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
state.studio.title,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: DesignConfig.isDark
? const Color.fromARGB(255, 0, 90, 119)
: const Color.fromARGB(255, 0, 53, 70)),
),
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
bottom:
Radius.circular(_isDescriptionExpanded ? 0 : 16.0),
),
boxShadow: _isDescriptionExpanded
? null
: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8.0,
spreadRadius: -2.0,
offset: const Offset(0, 6),
),
],
),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: ClipRRect(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(
_isDescriptionExpanded ? 0 : 16.0),
top: Radius.circular(
_isDescriptionExpanded ? 0 : 16.0),
),
child: Container(
color: Theme.of(context).colorScheme.surface,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: _isDescriptionExpanded
? double.infinity
: 150.0,
),
child: Html(
key: ValueKey(state.studio.id),
data: state.studio.description,
onAnchorTap: (href, _, __) =>
launchUrlString(href!),
style: {
'*': Style(
direction: TextDirection.rtl,
textAlign: TextAlign.right,
lineHeight: LineHeight.percent(135),
margin: const Margins(),
padding: HtmlPaddings.zero,
color: const Color.fromARGB(
255, 102, 102, 102),
fontWeight: FontWeight.normal,
),
},
),
),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: _isDescriptionExpanded ? 0.0 : 1.0,
child: Container(
height: 90,
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(16.0),
top: Radius.circular(16.0),
),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context)
.colorScheme
.surface
.withOpacity(0.0),
Theme.of(context).colorScheme.surface,
],
stops: const [0.0, 0.9],
),
),
),
),
),
],
),
),
Transform.translate(
offset: const Offset(0, -14.0),
child: InkWell(
onTap: () {
setState(() {
_isDescriptionExpanded = !_isDescriptionExpanded;
});
},
child: Padding(
padding: EdgeInsets.zero,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.focused,
shape: BoxShape.circle,
),
child: SvgPicture.asset(
_isDescriptionExpanded
? 'lib/assets/icons/arrow-up2.svg'
: 'lib/assets/icons/arrow-down.svg',
color: Theme.of(context).primaryColor,
height: 25,
),
),
],
),
),
),
),
if (state.studio.tags.isNotEmpty) const SizedBox(height: 16),
if (state.studio.tags.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0; i < state.studio.tags.length; i++)
TagItem(
tag: state.studio.tags[i],
onMarkChanged: (id, value) {
if (widget.pageData['onMarkChanged'] != null) {
widget.pageData['onMarkChanged'](id, value);
}
},
type: 'video',
),
],
),
],
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
if (state.nextStudio != null &&
state.alongSideState == AppState.idle)
StudioPreview(
isNext: true,
studio: state.nextStudio!,
),
if (state.alongSideState == AppState.busy)
StudioPreview.placeHolder,
if (state.prevStudio != null &&
state.alongSideState == AppState.idle)
StudioPreview(
isNext: false,
studio: state.prevStudio!,
),
if (state.alongSideState == AppState.busy)
StudioPreview.placeHolder,
const SizedBox(),
],
),
],
),
),
);
}
Widget _buildCommentsSection(StudioDetailsState state) {
return ChangeNotifierProvider<CommentsState>(
create: (context) => CommentsState()
..itemId = state.studio.id
..type = 'studio'
..onCommentsChanged = state.onCommentsChanged
..getComments(),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Consumer<UserProvider>(
builder: (context, userProvider, child) {
final user = userProvider.user;
final hasProfileImage =
user.photo != null && user.photo!.isNotEmpty;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// CircleAvatar(
// radius: 20,
// backgroundColor: DesignConfig.isDark
// ? Colors.transparent
// : Colors.white,
// backgroundImage:
// hasProfileImage ? NetworkImage(user.photo!) : null,
// child: !hasProfileImage
// ? Icon(
// DidvanIcons.avatar_light,
// size: 50,
// color: DesignConfig.isDark
// ? Colors.white
// : Colors.black,
// )
// : null,
// ),
// const SizedBox(width: 8),
Expanded(
child: CommentMessageBox(focusNode: _focusNode),
),
],
);
},
),
SizedBox(height: state.studio.comments == 0 ? 0 : 16),
SizedBox(
width: double.infinity,
child: state.studio.comments == 0
? const SizedBox()
: DidvanText(
'نظرات کاربران:',
style: TextStyle(
color: DesignConfig.isDark
? const Color.fromARGB(255, 0, 90, 119)
: const Color.fromARGB(255, 0, 53, 70),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
state.studio.comments == 0
? const SizedBox()
: SizedBox(
height: 400,
child: Comments(
key: _commentsKey,
pageData: const {'isPage': false},
),
),
],
),
),
),
);
}
Widget _buildRelatedContentSection(StudioDetailsState state) {
debugPrint("تعداد مطالب مرتبط: ${state.studio.relatedContents.length}");
debugPrint(
"آیا لیست مطالب مرتبط خالی است؟ ${state.studio.relatedContentsIsEmpty}");
debugPrint("تعداد tags: ${state.studio.tags.length}");
return Container(
width: double.infinity,
margin: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"مطالب مرتبط:",
style: TextStyle(
fontSize: 18,
color: DesignConfig.isDark
? const Color.fromARGB(255, 0, 90, 119)
: const Color.fromARGB(255, 0, 53, 70),
fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Builder(
builder: (context) {
if (state.studio.relatedContents.isNotEmpty) {
return AnimatedList(
key: _relatedContentKey,
initialItemCount: state.studio.relatedContents.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index, animation) {
final item = state.studio.relatedContents[index];
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.2),
end: Offset.zero,
).animate(animation),
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
String routeName;
Map<String, dynamic> arguments;
if (item.type == 'video') {
routeName = Routes.videoDetails;
arguments = {
'id': item.id,
'type': item.type,
};
} else if (item.type == 'podcast') {
routeName = Routes.studioDetails;
arguments = {
'id': item.id,
'type': item.type,
};
} else if (item.type == 'news') {
routeName = Routes.newsDetails;
arguments = {
'id': item.id,
};
} else if (item.type == 'radar') {
routeName = Routes.radarDetails;
arguments = {
'id': item.id,
};
} else {
routeName = Routes.studioDetails;
arguments = {
'id': item.id,
'type': item.type,
};
}
Navigator.pushNamed(
context,
routeName,
arguments: arguments,
);
},
child: MultitypeOverview(
item: item,
onMarkChanged: (id, value) {
if (widget.pageData['onMarkChanged'] !=
null) {
widget.pageData['onMarkChanged'](id, value);
}
},
),
),
),
),
);
},
);
} else if (state.studio.relatedContentsIsEmpty) {
return const Padding(
padding: EdgeInsets.all(32.0),
child: Center(
child: DidvanText(
'مطالب مرتبطی یافت نشد',
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
);
} else {
return Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'در حال بارگذاری مطالب مرتبط...',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
),
...List.generate(
3,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MultitypeOverview.placeholder,
)),
],
);
}
},
),
),
],
),
);
}
@override
void dispose() {
_animationController.dispose();
_videoPlayerController?.dispose();
_chewieController?.dispose();
_focusNode.dispose();
super.dispose();
}
}