redesign videocast tab

This commit is contained in:
mohamadmahdi jebeli 2025-10-12 16:41:15 +03:30
parent f90d7eff6a
commit 37767c912d
12 changed files with 575 additions and 78 deletions

View File

@ -0,0 +1,12 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 1.5V3.75" stroke="#666666" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 1.5V3.75" stroke="#666666" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.125 6.81738H15.875" stroke="#666666" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.25 6.375V12.75C16.25 15 15.125 16.5 12.5 16.5H6.5C3.875 16.5 2.75 15 2.75 12.75V6.375C2.75 4.125 3.875 2.625 6.5 2.625H12.5C15.125 2.625 16.25 4.125 16.25 6.375Z" stroke="#666666" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.271 10.2754H12.2778" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.271 12.5254H12.2778" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.49661 10.2754H9.50335" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.49661 12.5254H9.50335" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.72073 10.2754H6.72747" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.72073 12.5254H6.72747" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 17.4625C4.5525 17.4625 0.9375 13.8475 0.9375 9.40002C0.9375 4.95252 4.5525 1.33752 9 1.33752C13.4475 1.33752 17.0625 4.95252 17.0625 9.40002C17.0625 13.8475 13.4475 17.4625 9 17.4625ZM9 2.46252C5.175 2.46252 2.0625 5.57502 2.0625 9.40002C2.0625 13.225 5.175 16.3375 9 16.3375C12.825 16.3375 15.9375 13.225 15.9375 9.40002C15.9375 5.57502 12.825 2.46252 9 2.46252Z" fill="#292D32"/>
<path d="M11.7822 12.3475C11.6847 12.3475 11.5872 12.325 11.4972 12.265L9.17224 10.8775C8.59474 10.5325 8.16724 9.77503 8.16724 9.10753V6.03253C8.16724 5.72503 8.42224 5.47003 8.72974 5.47003C9.03724 5.47003 9.29224 5.72503 9.29224 6.03253V9.10753C9.29224 9.37753 9.51724 9.77503 9.74974 9.91003L12.0747 11.2975C12.3447 11.455 12.4272 11.8 12.2697 12.07C12.1572 12.25 11.9697 12.3475 11.7822 12.3475Z" fill="#292D32"/>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View File

@ -0,0 +1,4 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.47 22.5C17.9928 22.5 22.47 18.0228 22.47 12.5C22.47 6.97715 17.9928 2.5 12.47 2.5C6.94712 2.5 2.46997 6.97715 2.46997 12.5C2.46997 18.0228 6.94712 22.5 12.47 22.5Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.23999 12.7301V11.0601C9.23999 8.98012 10.71 8.13012 12.51 9.17012L13.96 10.0101L15.41 10.8501C17.21 11.8901 17.21 13.5901 15.41 14.6301L13.96 15.4701L12.51 16.3101C10.71 17.3501 9.23999 16.5001 9.23999 14.4201V12.7301Z" stroke="white" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@ -0,0 +1,4 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.6667 9.50016C15.6667 13.1802 12.68 16.1668 9.00004 16.1668C5.32004 16.1668 2.33337 13.1802 2.33337 9.50016C2.33337 5.82016 5.32004 2.8335 9.00004 2.8335C12.68 2.8335 15.6667 5.82016 15.6667 9.50016Z" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.4733 11.6202L9.40663 10.3868C9.04663 10.1735 8.7533 9.66017 8.7533 9.24017V6.50684" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@ -1,5 +1,3 @@
// lib/routes/route_generator.dart
// ignore_for_file: avoid_print
import 'package:didvan/models/ai/ai_chat_args.dart';
@ -30,6 +28,7 @@ import 'package:didvan/views/home/infography/infography_screen.dart';
import 'package:didvan/views/home/infography/infography_screen_state.dart';
import 'package:didvan/views/home/main/main_page_state.dart';
import 'package:didvan/views/home/home_state.dart';
import 'package:didvan/views/home/media/media_page.dart';
import 'package:didvan/views/home/new_statistic/new_statistics_state.dart';
import 'package:didvan/views/home/new_statistic/statistics_details/stat_cats_general_screen.dart';
import 'package:didvan/views/home/new_statistic/statistics_details/stat_cats_general_state.dart';
@ -43,6 +42,7 @@ import 'package:didvan/views/news/news_details/news_details_state.dart';
import 'package:didvan/views/news/news_state.dart';
import 'package:didvan/views/notification_time/notification_time_state.dart';
import 'package:didvan/views/podcasts/podcasts.dart';
import 'package:didvan/views/podcasts/podcasts_state.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:didvan/views/profile/profile.dart';
import 'package:didvan/views/radar/radar.dart';
@ -496,6 +496,14 @@ class RouteGenerator {
return _errorRoute(
'Invalid arguments for ${settings.name}: Expected Map with stories and tappedIndex.');
case Routes.media:
return MaterialPageRoute(
builder: (_) => ChangeNotifierProvider(
create: (context) => PodcastsState(),
child: const MediaPage(),
),
);
default:
return _errorRoute(settings.name ?? 'Unknown route');
}

View File

@ -41,4 +41,5 @@ class Routes {
static const String newStatic = '/new-static';
static const String web = '/web';
static const String storyViewer = '/story-viewer';
static const String media = '/media';
}

View File

@ -65,13 +65,16 @@ class _MainPageState extends State<MainPage> {
onRetry: () => context.read<MainPageState>().init(),
state: context.watch<MainPageState>(),
builder: (context, state) {
return ListView(
padding: const EdgeInsets.only(top: 0, bottom: 16),
return Column(
children: [
const HomeAppBar(
showBackButton: false,
showSearchField: true,
),
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 0, bottom: 16),
children: [
if (state.stories.isNotEmpty) ...[
const TextDivider(text: 'دیده‌بان')
.animate()
@ -92,7 +95,8 @@ class _MainPageState extends State<MainPage> {
padding: EdgeInsets.symmetric(horizontal: 16),
child: MainPageMainContent(),
).animate().fadeIn(delay: 800.ms, duration: 500.ms),
if (state.content != null && state.content!.lists.isNotEmpty) ...[
if (state.content != null &&
state.content!.lists.isNotEmpty) ...[
const _ExploreLatestTitle()
.animate()
.fadeIn(delay: 900.ms, duration: 500.ms),
@ -108,6 +112,9 @@ class _MainPageState extends State<MainPage> {
.animate()
.fadeIn(delay: 1200.ms, duration: 500.ms),
],
),
),
],
);
},
);

View File

@ -1,3 +1,5 @@
import 'package:didvan/views/home/media/podcast_tab_page.dart';
import 'package:didvan/views/home/media/videocast_tab_page.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/home/main/widgets/banner.dart';
import 'package:didvan/views/widgets/home_app_bar.dart';
@ -66,7 +68,8 @@ class _MediaPageState extends State<MediaPage> {
),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 7), // 10% فاصله از هر طرف برای 80%
padding:
const EdgeInsets.symmetric(horizontal: 7),
child: Container(
height: 2,
color: const Color.fromRGBO(184, 184, 184, 1),
@ -81,44 +84,12 @@ class _MediaPageState extends State<MediaPage> {
});
},
children: [
// محتوای تب پادکستها
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.podcasts,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 8),
DidvanText(
'صفحه پادکست‌ها',
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.video_library,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 8),
DidvanText(
'صفحه ویدئوها',
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
const PodcastTabPage(),
const VideoCastTabPage(),
const SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
padding: EdgeInsets.symmetric(
horizontal: 16.0, vertical: 16.0),
child: MainPageBanner(isFirst: true),
),
),

View File

@ -0,0 +1,66 @@
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/views/podcasts/podcasts_state.dart';
import 'package:didvan/views/widgets/overview/podcast.dart';
import 'package:didvan/views/widgets/state_handlers/empty_result.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class PodcastTabPage extends StatefulWidget {
const PodcastTabPage({super.key});
@override
State<PodcastTabPage> createState() => _PodcastTabPageState();
}
class _PodcastTabPageState extends State<PodcastTabPage> {
@override
void initState() {
super.initState();
Future.microtask(() {
context.read<PodcastsState>().init(true);
context.read<PodcastsState>().getStudios(page: 1);
});
}
@override
Widget build(BuildContext context) {
final state = context.watch<PodcastsState>();
return StateHandler<PodcastsState>(
state: state,
emptyState: EmptyResult(
onNewSearch: () {},
),
enableEmptyState: state.studios.isEmpty,
placeholder: PodcastOverview.placeholder,
builder: (context, state) {
return ListView.builder(
itemCount: state.studios.length,
itemBuilder: (context, index) {
final podcast = state.studios[index];
return Padding(
padding: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
child: PodcastOverview(
podcast: podcast,
onMarkChanged: state.changeMark,
studioRequestArgs: StudioRequestArgs(
page: state.page,
order: state.order,
search: state.search,
type: state.type,
asc: state.selectedSortTypeIndex == 1,
),
),
);
},
);
},
onRetry: () => context.read<PodcastsState>().getStudios(page: 1),
);
}
}

View File

@ -0,0 +1,99 @@
import 'package:didvan/routes/routes.dart';
import 'package:didvan/views/home/media/widgets/featured_video_card.dart';
import 'package:didvan/views/home/media/widgets/videocast_grid_card.dart';
import 'package:didvan/views/podcasts/podcasts_state.dart';
import 'package:didvan/views/widgets/overview/podcast.dart';
import 'package:didvan/views/widgets/state_handlers/empty_result.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class VideoCastTabPage extends StatefulWidget {
const VideoCastTabPage({super.key});
@override
State<VideoCastTabPage> createState() => _VideoCastTabPageState();
}
class _VideoCastTabPageState extends State<VideoCastTabPage> {
@override
void initState() {
super.initState();
Future.microtask(() {
context.read<PodcastsState>().init(false);
context.read<PodcastsState>().getStudios(page: 1);
});
}
@override
Widget build(BuildContext context) {
final state = context.watch<PodcastsState>();
return StateHandler<PodcastsState>(
state: state,
emptyState: EmptyResult(
onNewSearch: () {},
),
enableEmptyState: state.studios.isEmpty,
placeholder: PodcastOverview.placeholder,
builder: (context, state) {
if (state.studios.isEmpty) {
return const SizedBox.shrink();
}
return SingleChildScrollView(
child: Column(
children: [
FeaturedVideoCard(
videocast: state.studios.first,
onTap: () {
Navigator.pushNamed(
context,
Routes.studioDetails,
arguments: {
'id': state.studios.first.id,
'type': state.studios.first.type,
},
);
},
),
if (state.studios.length > 1)
Padding(
padding: const EdgeInsets.all(16),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: state.studios.length - 1,
itemBuilder: (context, index) {
final videocast = state.studios[index + 1];
return VideocastGridCard(
videocast: videocast,
onTap: () {
Navigator.pushNamed(
context,
Routes.studioDetails,
arguments: {
'id': videocast.id,
'type': videocast.type,
},
);
},
);
},
),
),
],
),
);
},
onRetry: () => context.read<PodcastsState>().getStudios(page: 1),
);
}
}

View File

@ -0,0 +1,189 @@
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class FeaturedVideoCard extends StatelessWidget {
final OverviewData videocast;
final VoidCallback onTap;
const FeaturedVideoCard({
super.key,
required this.videocast,
required this.onTap,
});
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 Container(
height: MediaQuery.of(context).size.height * 0.40,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
Positioned.fill(
child: ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0, 0, 0, 1, 0,
]),
child: SkeletonImage(
imageUrl: videocast.image,
width: double.infinity,
height: double.infinity,
borderRadius: BorderRadius.circular(16),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0.4),
Colors.transparent,
],
),
),
),
),
Positioned(
bottom: 16,
left: 16,
right: 16,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DidvanText(
videocast.title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: const Color.fromARGB(255, 200, 224, 244),
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Container(
height: 2,
color: const Color.fromARGB(255, 200, 224, 244),
),
],
),
),
const SizedBox(height: 12),
DidvanText(
videocast.description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
height: 1.4,
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 5),
Align(
alignment: AlignmentDirectional.centerEnd,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset("lib/assets/icons/timer.svg"),
const SizedBox(width: 6),
DidvanText(
_formatDuration(videocast.duration).toPersianDigit(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: onTap,
icon: SvgPicture.asset('lib/assets/icons/play-circle.svg'),
label: const DidvanText(
'مشاهده ویدیو',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 178, 4, 54),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
),
const SizedBox(width: 16),
Padding(
padding: const EdgeInsets.only(bottom: 60.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SkeletonImage(
imageUrl: videocast.image,
width: 115,
height: 155,
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,132 @@
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class VideocastGridCard extends StatelessWidget {
final OverviewData videocast;
final VoidCallback onTap;
const VideocastGridCard({
super.key,
required this.videocast,
required this.onTap,
});
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')}';
}
String _formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return date.toPersianDateStr();
} catch (e) {
return dateStr;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
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: [
// Video cover
Padding(
padding: const EdgeInsets.all(6.0),
child: AspectRatio(
aspectRatio: 17 / 14,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SkeletonImage(
imageUrl: videocast.image,
width: double.infinity,
height: double.infinity,
borderRadius: BorderRadius.circular(20),
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 4, 12, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
DidvanText(
videocast.title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Colors.black87,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
SvgPicture.asset('lib/assets/icons/calendar.svg'),
const SizedBox(width: 4),
DidvanText(
_formatDate(videocast.createdAt),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: const Color.fromARGB(255, 102, 102, 102),
),
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(videocast.duration).toPersianDigit(),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: const Color.fromARGB(255, 102, 102, 102),
),
),
],
),
],
),
),
),
],
),
),
);
}
}