base rediesign single videocast

This commit is contained in:
mohamadmahdi jebeli 2025-10-13 16:05:51 +03:30
parent fe87fac33d
commit 2da6dc8993
15 changed files with 858 additions and 277 deletions

View File

@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 17.7298C11.3 17.7298 10.6 17.4598 10.07 16.9298L3.55002 10.4098C3.26002 10.1198 3.26002 9.63982 3.55002 9.34982C3.84002 9.05982 4.32002 9.05982 4.61002 9.34982L11.13 15.8698C11.61 16.3498 12.39 16.3498 12.87 15.8698L19.39 9.34982C19.68 9.05982 20.16 9.05982 20.45 9.34982C20.74 9.63982 20.74 10.1198 20.45 10.4098L13.93 16.9298C13.4 17.4598 12.7 17.7298 12 17.7298Z" fill="#2196F3"/>
</svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 19.92L8.48 13.4C7.71 12.63 7.71 11.37 8.48 10.6L15 4.07996" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.9201 15.0499L13.4001 8.52989C12.6301 7.75989 11.3701 7.75989 10.6001 8.52989L4.08008 15.0499" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,4 @@
<svg width="43" height="43" viewBox="0 0 43 43" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="42.5445" height="42.5445" rx="21.2723" fill="black" fill-opacity="0.6"/>
<path d="M26.0923 11.2725H16.4523C14.3223 11.2725 12.5923 13.0125 12.5923 15.1325V29.2225C12.5923 31.0225 13.8823 31.7825 15.4623 30.9125L20.3423 28.2025C20.8623 27.9125 21.7023 27.9125 22.2123 28.2025L27.0923 30.9125C28.6723 31.7925 29.9623 31.0325 29.9623 29.2225V15.1325C29.9523 13.0125 28.2223 11.2725 26.0923 11.2725Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 596 B

View File

@ -0,0 +1,4 @@
<svg width="43" height="43" viewBox="0 0 43 43" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="42.5445" height="42.5445" rx="21.2723" fill="black" fill-opacity="0.6"/>
<path d="M26.0987 11.269H16.4459C14.3243 11.269 12.5873 13.006 12.5873 15.1277V29.2222C12.5873 31.0212 13.8776 31.7905 15.4533 30.9095L20.3293 28.1924C20.8504 27.907 21.6941 27.907 22.2028 28.1924L27.0788 30.9095C28.6669 31.778 29.9573 31.0212 29.9573 29.2222V15.1277C29.9573 13.006 28.2203 11.269 26.0987 11.269Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 519 B

View File

@ -64,6 +64,7 @@ import 'package:didvan/views/home/statistic/statistic_state.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details.mobile.dart'
if (dart.library.io) 'package:didvan/views/podcasts/studio_details/studio_details.mobile.dart'
if (dart.library.html) 'package:didvan/views/podcasts/studio_details/studio_details.web.dart';
import 'package:didvan/views/home/media/video_details_page.dart';
import 'package:didvan/views/splash/splash.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/views/story_viewer/story_viewer_page.dart';
@ -313,6 +314,20 @@ class RouteGenerator {
return _errorRoute(
'Invalid arguments for ${settings.name}: Expected Map<String, dynamic>.');
case Routes.videoDetails:
if (settings.arguments is Map<String, dynamic>) {
return _createRoute(
ChangeNotifierProvider<StudioDetailsState>(
create: (context) => StudioDetailsState(),
child: VideoDetailsPage(
pageData: settings.arguments as Map<String, dynamic>,
),
),
);
}
return _errorRoute(
'Invalid arguments for ${settings.name}: Expected Map<String, dynamic>.');
case Routes.statisticDetails:
if (settings.arguments is Map<String, dynamic>) {
return _createRoute(

View File

@ -42,4 +42,5 @@ class Routes {
static const String web = '/web';
static const String storyViewer = '/story-viewer';
static const String media = '/media';
static const String videoDetails = '/video-details';
}

View File

@ -0,0 +1,507 @@
// ignore_for_file: use_build_context_synchronously, deprecated_member_use
import 'package:chewie/chewie.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/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> {
int _currentlyPlayingId = 0;
VideoPlayerController? _videoPlayerController;
ChewieController? _chewieController;
bool _isDescriptionExpanded = false;
@override
void initState() {
super.initState();
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();
}
});
}
}),
);
}
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(
backgroundColor: Colors.white,
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:
const Color.fromARGB(255, 102, 102, 102),
height: 24,
),
onPressed: () {
Navigator.pop(context);
},
),
],
),
),
),
)),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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: 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 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: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 0, 53, 70)),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: _isDescriptionExpanded ? double.infinity : 100.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,
),
},
),
),
),
InkWell(
onTap: () {
setState(() {
_isDescriptionExpanded = !_isDescriptionExpanded;
});
},
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
_isDescriptionExpanded
? 'lib/assets/icons/arrow-up2.svg'
: 'lib/assets/icons/arrow-down.svg',
color: Theme.of(context).primaryColor,
height: 20,
),
const SizedBox(width: 4),
Text(
_isDescriptionExpanded ? 'کمتر' : 'بیشتر',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.normal,
),
),
],
),
),
),
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 Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFF059669),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: const DidvanText(
'نظرات',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(
height: 400,
child: ChangeNotifierProvider<CommentsState>(
create: (context) => CommentsState(),
child: Comments(
pageData: {
'id': state.studio.id,
'type': 'studio',
'title': state.studio.title,
'onCommentsChanged': state.onCommentsChanged,
'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: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"مطالب مرتبط:",
style: TextStyle(
fontSize: 16,
color: Color.fromARGB(255, 0, 53, 70),
fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Builder(
builder: (context) {
debugPrint("Building related content section:");
debugPrint(
" - relatedContents.length: ${state.studio.relatedContents.length}");
debugPrint(
" - relatedContentsIsEmpty: ${state.studio.relatedContentsIsEmpty}");
debugPrint(" - tags.length: ${state.studio.tags.length}");
if (state.studio.relatedContents.isNotEmpty) {
debugPrint(
" - Showing ${state.studio.relatedContents.length} related items");
return Column(
children: [
...state.studio.relatedContents
.map((item) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MultitypeOverview(
item: item,
onMarkChanged: (id, value) {
if (widget.pageData['onMarkChanged'] !=
null) {
widget.pageData['onMarkChanged'](
id, value);
}
},
),
))
.toList(),
],
);
} else if (state.studio.relatedContentsIsEmpty) {
debugPrint(" - Showing empty message");
return const Padding(
padding: EdgeInsets.all(32.0),
child: Center(
child: DidvanText(
'مطالب مرتبطی یافت نشد',
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
);
} else {
debugPrint(" - Showing placeholders (loading state)");
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() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
}

View File

@ -136,7 +136,7 @@ class _VideoCastTabPageState extends State<VideoCastTabPage> {
onTap: () {
Navigator.pushNamed(
context,
Routes.studioDetails,
Routes.videoDetails,
arguments: {
'id': state.studios[_currentFeaturedIndex].id,
'type': state.studios[_currentFeaturedIndex].type,
@ -198,7 +198,7 @@ class _VideoCastTabPageState extends State<VideoCastTabPage> {
onTap: () {
Navigator.pushNamed(
context,
Routes.studioDetails,
Routes.videoDetails,
arguments: {
'id': videocast.id,
'type': videocast.type,

View File

@ -25,7 +25,7 @@ class FeaturedVideoCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.40,
height: 380,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
@ -112,6 +112,7 @@ class FeaturedVideoCard extends StatelessWidget {
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: const Color.fromARGB(255, 200, 224, 244),
fontWeight: FontWeight.bold,
fontSize: 18
),
maxLines: 2,
overflow: TextOverflow.ellipsis,

View File

@ -1,7 +1,5 @@
import 'dart:math';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/views/comments/comments.dart';
@ -16,6 +14,8 @@ import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -37,7 +37,7 @@ class StudioDetailsWidget extends StatelessWidget {
bool isVideo = state.studio.iframe != null;
double topOffset = isVideo
? ds.width * 9 / 16
: 400; // برای ویدیو aspect ratio، برای پادکست ارتفاع AudioPlayerWidget
: 400;
return Container(
height: max(
ds.height - topOffset - 72 - MediaQuery.of(context).padding.top,
@ -101,20 +101,20 @@ class StudioDetailsWidget extends StatelessWidget {
const SizedBox(),
if (state.nextStudio != null &&
state.alongSideState == AppState.idle)
_StudioPreview(
StudioPreview(
isNext: true,
studio: state.nextStudio!,
),
if (state.alongSideState == AppState.busy)
_StudioPreview.placeHolder,
StudioPreview.placeHolder,
if (state.prevStudio != null &&
state.alongSideState == AppState.idle)
_StudioPreview(
StudioPreview(
isNext: false,
studio: state.prevStudio!,
),
if (state.alongSideState == AppState.busy)
_StudioPreview.placeHolder,
StudioPreview.placeHolder,
const SizedBox(),
],
),
@ -200,10 +200,10 @@ class StudioDetailsWidget extends StatelessWidget {
}
}
class _StudioPreview extends StatelessWidget {
class StudioPreview extends StatelessWidget {
final bool isNext;
final StudioDetailsData studio;
const _StudioPreview({
const StudioPreview({
Key? key,
required this.isNext,
required this.studio,
@ -211,11 +211,18 @@ class _StudioPreview extends StatelessWidget {
String get _previewTitle {
if (studio.type == 'video') {
return 'ویدیوی ${isNext ? 'بعدی' : 'قبلی'} ';
return 'ویدیوکست ${isNext ? 'بعدی' : 'قبلی'} ';
}
return 'پادکست ${isNext ? 'بعدی' : 'قبلی'} ';
}
String _formatDuration(int? duration) {
if (duration == null) return '';
final minutes = duration ~/ 60;
final seconds = duration % 60;
return '${seconds.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return GestureDetector(
@ -227,39 +234,95 @@ class _StudioPreview extends StatelessWidget {
isForward: isNext,
);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 88,
height: 216,
color: Colors.transparent,
child: Column(
children: [
SkeletonImage(
imageUrl: studio.image,
aspectRatio: 1 / 1,
),
const SizedBox(height: 8),
Icon(
isNext
? DidvanIcons.angle_right_regular
: DidvanIcons.angle_left_regular,
),
const SizedBox(height: 8),
DidvanText(
_previewTitle,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
DidvanText(
studio.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
color: Theme.of(context).colorScheme.caption,
width: 170,
height: 235,
decoration: BoxDecoration(
color: const Color.fromRGBO(235, 235, 235, 1),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(6.0),
child: AspectRatio(
aspectRatio: 17 / 14,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SkeletonImage(
imageUrl: studio.image,
width: double.infinity,
height: double.infinity,
borderRadius: BorderRadius.circular(20),
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 4, 8, 3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
DidvanText(
_previewTitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.normal,
color: const Color.fromARGB(255, 102, 102, 102),
fontSize: 12
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
DidvanText(
studio.title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.normal,
color: Colors.black87,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
SvgPicture.asset(
'lib/assets/icons/clock.svg',
color: const Color.fromARGB(255, 102, 102, 102),
),
const SizedBox(width: 4),
DidvanText(
_formatDuration(studio.duration).toPersianDigit(),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: const Color.fromARGB(255, 102, 102, 102),
),
),
],
),
],
),
),
),
],
),
),
),
);
}

View File

@ -3,9 +3,9 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class BookmarkButton extends StatefulWidget {
final bool value;
@ -15,6 +15,9 @@ class BookmarkButton extends StatefulWidget {
final double gestureSize;
final String type;
final int itemId;
final String? svgIconOn;
final String? svgIconOff;
const BookmarkButton({
Key? key,
required this.value,
@ -24,6 +27,8 @@ class BookmarkButton extends StatefulWidget {
required this.itemId,
this.askForConfirmation = false,
this.color,
this.svgIconOn,
this.svgIconOff,
}) : super(key: key);
@override
@ -45,16 +50,7 @@ class _BookmarkButtonState extends State<BookmarkButton> {
super.initState();
}
@override
Widget build(BuildContext context) {
return DidvanIconButton(
gestureSize: widget.gestureSize,
color: widget.color ??
(DesignConfig.isDark || !_value
? null
: Theme.of(context).colorScheme.primary),
icon: _value ? DidvanIcons.bookmark_solid : DidvanIcons.bookmark_regular,
onPressed: () async {
void _handleTap() async {
bool confirm = false;
if (widget.askForConfirmation) {
await ActionSheetUtils(context).openDialog(
@ -76,7 +72,27 @@ class _BookmarkButtonState extends State<BookmarkButton> {
widget.onMarkChanged(_value);
UserProvider.changeItemMark(widget.type, widget.itemId, _value);
}
}
@override
Widget build(BuildContext context) {
print("BookmarkButton build - value: $_value");
return IconButton(
iconSize: widget.gestureSize,
onPressed: _handleTap,
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: SvgPicture.asset(
_value ? 'lib/assets/icons/bookmark_on.svg' : 'lib/assets/icons/bookmark_off.svg',
key: ValueKey('bookmark_$_value'),
width: widget.gestureSize,
height: widget.gestureSize,
),
),
);
}
}

View File

@ -35,6 +35,7 @@ class DidvanIconButton extends StatelessWidget {
size: size,
color: color,
),
),
);
}

View File

@ -6,10 +6,7 @@ import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:didvan/views/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
@ -17,10 +14,8 @@ import 'package:didvan/views/widgets/didvan/text_field.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
import 'package:didvan/services/app_initalizer.dart';
import 'package:url_launcher/url_launcher_string.dart';
class MultitypeOverview extends StatelessWidget {
final OverviewData item;
@ -64,67 +59,29 @@ class MultitypeOverview extends StatelessWidget {
return null;
}
IconData get _icon {
String get _icon {
switch (item.type) {
case 'radar':
return DidvanIcons.scanning_light;
return 'DidvanIcons.scanning_light';
case 'news':
return DidvanIcons.foolad_light;
return 'DidvanIcons.foolad_light';
case 'video':
return DidvanIcons.video_light;
return 'DidvanIcons.video_light';
case 'podcast':
return DidvanIcons.podcast_light;
return 'DidvanIcons.podcast_light';
case 'delphi':
case 'survey':
return DidvanIcons.saha_light;
return 'DidvanIcons.saha_light';
case 'infography':
return DidvanIcons.infography_regular;
return 'DidvanIcons.infography_regular';
default:
return DidvanIcons.radar_light;
return 'DidvanIcons.radar_light';
}
}
@override
Widget build(BuildContext context) {
return DidvanCard(
onTap: () async {
if (item.type == 'infography') {
return;
}
if (item.type == 'podcast') {
final state = context.read<StudioDetailsState>();
await state.getStudioDetails(
item.id,
args: const StudioRequestArgs(page: 0, type: 'podcast'),
);
MediaService.currentPodcast = state.studio;
MediaService.handleAudioPlayback(
audioSource: item.link,
id: item.id,
isNetworkAudio: true,
isVoiceMessage: false,
);
return;
}
if (_targetPageRouteName == null && item.link != null) {
AppInitializer.openWebLink(
context,
'${item.link!}?accessToken=${RequestService.token}',
mode: LaunchMode.inAppWebView,
);
return;
}
Navigator.of(context).pushNamed(
_targetPageRouteName!,
arguments: {
'onMarkChanged': onMarkChanged,
'id': item.id,
'args': _targetPageArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
},
);
},
child: Column(
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
@ -138,13 +95,13 @@ class MultitypeOverview extends StatelessWidget {
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(10),
left: Radius.circular(20),
),
),
child: Icon(
child: SvgPicture.asset(
_icon,
color: Theme.of(context).colorScheme.white,
size: 18,
height: 18,
),
),
],
@ -283,8 +240,9 @@ class MultitypeOverview extends StatelessWidget {
],
],
),
const SizedBox(height: 10,),
Divider(color: Theme.of(context).colorScheme.border),
],
),
);
}

View File

@ -1,5 +1,4 @@
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/tag.dart';
import 'package:didvan/routes/routes.dart';
@ -35,22 +34,25 @@ class TagItem extends StatelessWidget {
horizontal: 8,
),
decoration: BoxDecoration(
borderRadius: DesignConfig.lowBorderRadius,
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
border: Border.all(
color: Theme.of(context).colorScheme.focusedBorder,
color: const Color.fromARGB(255, 184, 184, 184),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
const Icon(
DidvanIcons.hashtag_regular,
color: Theme.of(context).colorScheme.focusedBorder,
color: Color.fromARGB(255, 102, 102, 102),
size: 17,
),
DidvanText(
tag.label,
color: Theme.of(context).colorScheme.focusedBorder,
style: Theme.of(context).textTheme.bodyLarge,
color: const Color.fromARGB(255, 102, 102, 102),
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
],
),