home page v1

This commit is contained in:
MohammadTaha Basiri 2023-10-06 18:52:06 +03:30
parent 0349a8939b
commit 1fe0e8def5
21 changed files with 1282 additions and 167 deletions

View File

@ -0,0 +1,13 @@
class HomePageBannerType {
final String image;
final String? link;
HomePageBannerType({required this.image, required this.link});
factory HomePageBannerType.fromJson(Map<String, dynamic> json) {
return HomePageBannerType(
image: json['image'],
link: json['link'],
);
}
}

View File

@ -1,26 +1,30 @@
class Content { class HomePageContentType {
final int? id; final int id;
final String title; final String title;
final String image; final String image;
final String link; final String link;
final bool marked; final bool marked;
final List<String> subtitles; final List<String> subtitles;
final int? duration;
const Content({ const HomePageContentType({
required this.id, required this.id,
required this.title, required this.title,
required this.image, required this.image,
required this.link, required this.link,
required this.marked, required this.marked,
required this.subtitles, required this.subtitles,
this.duration,
}); });
factory Content.fromJson(Map<String, dynamic> json) => Content( factory HomePageContentType.fromJson(Map<String, dynamic> json) =>
HomePageContentType(
id: json['id'], id: json['id'],
title: json['title'], title: json['title'],
image: json['image'], image: json['image'],
link: json['link'], link: json['link'],
marked: json['marked'], marked: json['marked'],
subtitles: List<String>.from(json['subtitles']), subtitles: List<String>.from(json['subtitles']),
duration: json['duration'],
); );
} }

View File

@ -1,7 +1,9 @@
import 'package:didvan/models/home_page_content/banner.dart';
import 'home_page_list.dart'; import 'home_page_list.dart';
class HomePageContent { class HomePageContent {
final List<dynamic> banners; final List<HomePageBannerType> banners;
final List<HomePageList> lists; final List<HomePageList> lists;
final int unread; final int unread;
@ -10,7 +12,9 @@ class HomePageContent {
factory HomePageContent.fromJson(Map<String, dynamic> json) { factory HomePageContent.fromJson(Map<String, dynamic> json) {
return HomePageContent( return HomePageContent(
banners: json['banners'], banners: List<HomePageBannerType>.from(json['banners'].map(
(x) => HomePageBannerType.fromJson(x),
)),
lists: List<HomePageList>.from( lists: List<HomePageList>.from(
json['lists'].map( json['lists'].map(
(x) => HomePageList.fromJson(x), (x) => HomePageList.fromJson(x),

View File

@ -5,7 +5,7 @@ class HomePageList {
final String header; final String header;
final String more; final String more;
final String link; final String link;
final List<Content> contents; final List<HomePageContentType> contents;
const HomePageList({ const HomePageList({
required this.type, required this.type,
@ -20,7 +20,7 @@ class HomePageList {
header: json['header'], header: json['header'],
more: json['more'], more: json['more'],
link: json['link'], link: json['link'],
contents: List<Content>.from(json['contents'].map( contents: List<HomePageContentType>.from(json['contents'].map(
(x) => Content.fromJson(x), (x) => HomePageContentType.fromJson(x),
))); )));
} }

View File

@ -68,6 +68,9 @@ class RouteGenerator {
ChangeNotifierProvider<StatisticState>( ChangeNotifierProvider<StatisticState>(
create: (context) => StatisticState(), create: (context) => StatisticState(),
), ),
ChangeNotifierProvider<PodcastsState>(
create: (context) => PodcastsState(),
),
], ],
child: const Home(), child: const Home(),
), ),

View File

@ -3,11 +3,14 @@ import 'dart:io';
import 'package:assets_audio_player/assets_audio_player.dart'; import 'package:assets_audio_player/assets_audio_player.dart';
import 'package:didvan/config/theme_data.dart'; import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/studio_details_data.dart'; import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/home/widgets/audio/audio_slider.dart'; import 'package:didvan/views/home/widgets/audio/audio_slider.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AudioWidget extends StatelessWidget { class AudioWidget extends StatelessWidget {
final String? audioUrl; final String? audioUrl;
@ -34,7 +37,9 @@ class AudioWidget extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
child: AudioSlider( child: AudioSlider(
tag: audioMetaData != null ? 'radar-$id' : 'message-$id', tag: audioMetaData != null
? '${audioMetaData!.type}-$id'
: 'message-$id',
duration: audioMetaData?.duration, duration: audioMetaData?.duration,
showTimer: true, showTimer: true,
), ),
@ -70,7 +75,7 @@ class _AudioControllerButton extends StatelessWidget {
bool get _nowPlaying => bool get _nowPlaying =>
MediaService.audioPlayerTag == MediaService.audioPlayerTag ==
(audioMetaData != null ? 'radar-$id' : 'message-$id'); (audioMetaData != null ? '${audioMetaData!.type}-$id' : 'message-$id');
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -84,6 +89,15 @@ class _AudioControllerButton extends StatelessWidget {
gestureSize: 36, gestureSize: 36,
color: Theme.of(context).colorScheme.focusedBorder, color: Theme.of(context).colorScheme.focusedBorder,
onPressed: () async { onPressed: () async {
if (audioMetaData?.type == 'podcast') {
final state = context.read<StudioDetailsState>();
if (MediaService.currentPodcast == null) {
await state.getStudioDetails(
id,
args: const StudioRequestArgs(page: 0, type: 'podcast'),
);
}
}
if (snapshot.data == null && _nowPlaying) { if (snapshot.data == null && _nowPlaying) {
return; return;
} }

View File

@ -1,11 +1,17 @@
import 'package:carousel_slider/carousel_slider.dart'; import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart'; import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/home_page_content/home_page_list.dart'; import 'package:didvan/models/home_page_content/home_page_list.dart';
import 'package:didvan/views/home/home/home_page_state.dart'; import 'package:didvan/views/home/home/home_page_state.dart';
import 'package:didvan/views/widgets/didvan/card.dart'; import 'package:didvan/views/home/home/widgets/banner.dart';
import 'package:didvan/views/home/home/widgets/general_item.dart';
import 'package:didvan/views/home/home/widgets/main_content.dart';
import 'package:didvan/views/home/home/widgets/news_item.dart';
import 'package:didvan/views/home/home/widgets/podcast_item.dart';
import 'package:didvan/views/home/home/widgets/radar_item.dart';
import 'package:didvan/views/home/home/widgets/videocast_item.dart';
import 'package:didvan/views/widgets/didvan/slider.dart';
import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -21,10 +27,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
@override @override
void initState() { void initState() {
Future.delayed( context.read<HomePageState>().init();
Duration.zero,
context.read<HomePageState>().getMainPageContent,
);
super.initState(); super.initState();
} }
@ -33,37 +36,42 @@ class _HomePageState extends State<HomePage> {
return StateHandler<HomePageState>( return StateHandler<HomePageState>(
onRetry: () {}, onRetry: () {},
state: context.watch<HomePageState>(), state: context.watch<HomePageState>(),
builder: (context, state) => builder: (context, state) => ListView.builder(
ListView.builder(itemBuilder: (context, index) { padding: const EdgeInsets.symmetric(vertical: 16),
if (index == 0) { itemBuilder: (context, index) {
return CarouselSlider( if (index == 0) {
items: state.content.banners return const HomePageMainContent();
.map((e) => SkeletonImage( }
imageUrl: e.imageUrl, index--;
)) if (index == 4) {
.toList(), return const Padding(
options: CarouselOptions( padding: EdgeInsets.only(top: 32),
viewportFraction: 1.0, child: HomePageBanner(
), isFirst: false,
),
);
}
if (index > 3) {
index--;
}
final list = state.content.lists[index];
return _HomePageSection(
list: list,
isLast: index == state.content.lists.length - 1,
); );
} },
return const SizedBox(); itemCount: state.content.lists.length + 2,
// state.content.banners.map((e) => const SizedBox()).toList(), ),
// for (int i = 0; i < state.content.lists.length; i += 2)
// ...state.content.lists
// .sublist(i, i + 2)
// .map((e) => _HomePageItem(list: e))
// .toList(),
}),
); );
} }
} }
class _HomePageItem extends StatelessWidget { class _HomePageSection extends StatelessWidget {
final HomePageList list; final HomePageList list;
const _HomePageItem({required this.list}); final bool isLast;
const _HomePageSection({required this.list, required this.isLast});
void moreHandler(BuildContext context) { void _moreHandler(BuildContext context) {
if (list.link.startsWith('http')) { if (list.link.startsWith('http')) {
launchUrl(Uri.parse(list.link)); launchUrl(Uri.parse(list.link));
return; return;
@ -71,8 +79,34 @@ class _HomePageItem extends StatelessWidget {
Navigator.of(context).pushNamed(list.link); Navigator.of(context).pushNamed(list.link);
} }
IconData? _generateIcon() {
switch (list.type) {
case 'news':
return DidvanIcons.news_solid;
case 'radar':
return DidvanIcons.radar_solid;
case 'video':
return DidvanIcons.video_solid;
case 'podcast':
return DidvanIcons.podcast_solid;
default:
return null;
}
}
int _maxSublistCount() {
int max = 1;
for (var i = 0; i < list.contents.length; i++) {
if (list.contents[i].subtitles.length > max) {
max = list.contents[i].subtitles.length;
}
}
return max - 1;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final icon = _generateIcon();
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -85,13 +119,19 @@ class _HomePageItem extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
DidvanText( Row(
list.header, children: [
style: Theme.of(context).textTheme.titleMedium, if (icon != null) Icon(icon),
color: Theme.of(context).colorScheme.title, const SizedBox(width: 4),
DidvanText(
list.header,
style: Theme.of(context).textTheme.titleMedium,
color: Theme.of(context).colorScheme.title,
),
],
), ),
GestureDetector( GestureDetector(
onTap: () => moreHandler(context), onTap: () => _moreHandler(context),
child: Row( child: Row(
children: [ children: [
DidvanText( DidvanText(
@ -108,63 +148,97 @@ class _HomePageItem extends StatelessWidget {
], ],
), ),
), ),
CarouselSlider.builder( if (list.type == 'news')
itemCount: list.contents.length, DidvanSlider(
itemBuilder: (context, index, realIndex) => DidvanCard( height: 232,
margin: const EdgeInsets.only(left: 8), itemCount: list.contents.length,
padding: EdgeInsets.zero, viewportFraction: 0.45,
child: Column( itemBuilder: (context, index, realIndex) => Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 4),
SkeletonImage( child: HomePageNewsItem(
imageUrl: list.contents[index].image, content: list.contents[index],
height: MediaQuery.of(context).size.height / 6, ),
width: double.infinity,
),
Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
list.contents[index].title,
style: Theme.of(context).textTheme.bodyLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
const Icon(
DidvanIcons.puzzle_light,
size: 16,
),
...list.contents[index].subtitles.map(
(e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: DidvanText(
e,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
),
],
)
],
),
),
],
), ),
), ),
options: CarouselOptions( if (list.type == 'radar')
autoPlay: true, DidvanSlider(
padEnds: false, height: 148,
viewportFraction: 0.7, itemCount: list.contents.length,
viewportFraction: 0.6,
itemBuilder: (context, index, realIndex) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: HomePageRadarItem(
content: list.contents[index],
),
),
), ),
), if (list.type == 'video')
DidvanSlider(
height: 180,
itemCount: list.contents.length,
viewportFraction: 0.6,
itemBuilder: (context, index, realIndex) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: HomePageVideocastItem(
content: list.contents[index],
),
),
),
if (list.type == 'podcast')
Column(
children: list.contents
.map(
(e) => Padding(
padding: const EdgeInsets.only(
bottom: 12,
left: 28,
right: 28,
),
child: HomePagePodcastItem(
content: e,
),
),
)
.toList(),
),
if (list.type != 'news' &&
list.type != 'radar' &&
list.type != 'video' &&
list.type != 'podcast')
DidvanSlider(
itemBuilder: (context, index, realIndex) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: HomePageGeneralItem(
content: list.contents[index],
),
),
itemCount: list.contents.length,
viewportFraction: 0.7,
height: 196 + _maxSublistCount() * 20,
),
if (!isLast) const _HomePageDivider(),
], ],
); );
} }
} }
class _HomePageDivider extends StatelessWidget {
const _HomePageDivider();
@override
Widget build(BuildContext context) {
return Container(
height: 2,
margin: const EdgeInsets.only(
top: 8,
left: 20,
right: 20,
),
width: double.infinity,
decoration: BoxDecoration(
borderRadius: DesignConfig.highBorderRadius,
color: Theme.of(context).colorScheme.border,
),
);
}
}

View File

@ -1,3 +1,5 @@
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/category.dart';
import 'package:didvan/models/enums.dart'; import 'package:didvan/models/enums.dart';
import 'package:didvan/models/home_page_content/home_page_content.dart'; import 'package:didvan/models/home_page_content/home_page_content.dart';
import 'package:didvan/providers/core.dart'; import 'package:didvan/providers/core.dart';
@ -5,8 +7,9 @@ import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
class HomePageState extends CoreProvier { class HomePageState extends CoreProvier {
late final HomePageContent content; late HomePageContent content;
Future<void> getMainPageContent() async { List<CategoryData> categories = [];
Future<void> _getMainPageContent() async {
final service = RequestService(RequestHelper.mainPageContent); final service = RequestService(RequestHelper.mainPageContent);
await service.httpGet(); await service.httpGet();
if (service.isSuccess) { if (service.isSuccess) {
@ -16,4 +19,64 @@ class HomePageState extends CoreProvier {
} }
appState = AppState.failed; appState = AppState.failed;
} }
void init() {
if (categories.isEmpty) {
categories = [
CategoryData(
id: 1,
label: 'اقتصادی',
asset: Assets.economicCategoryIcon,
),
CategoryData(
id: 2,
label: 'سیاسی',
asset: Assets.politicalCategoryIcon,
),
CategoryData(
id: 3,
label: 'فناوری',
asset: Assets.techCategoryIcon,
),
CategoryData(
id: 4,
label: 'کسب و کار',
asset: Assets.businessCategoryIcon,
),
CategoryData(
id: 5,
label: 'زیست محیطی',
asset: Assets.enviromentalCategoryIcon,
),
CategoryData(
id: 6,
label: 'اجتماعی',
asset: Assets.socialCategoryIcon,
),
CategoryData(
id: 1,
label: 'اقتصادی',
asset: Assets.economicCategoryIcon,
),
CategoryData(
id: 2,
label: 'سیاسی',
asset: Assets.politicalCategoryIcon,
),
CategoryData(
id: 3,
label: 'فناوری',
asset: Assets.techCategoryIcon,
),
CategoryData(
id: 4,
label: 'کسب و کار',
asset: Assets.businessCategoryIcon,
),
];
}
Future.delayed(Duration.zero, () {
_getMainPageContent();
});
}
} }

View File

@ -0,0 +1,25 @@
import 'package:didvan/views/widgets/didvan/slider.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
class HomePageBanner extends StatelessWidget {
final bool isFirst;
const HomePageBanner({super.key, required this.isFirst});
@override
Widget build(BuildContext context) {
// final state = context.read<HomePageState>();
final banners = [1, 2];
return DidvanSlider(
itemBuilder: (context, index, realIndex) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: SkeletonImage(
imageUrl: 'https://wallpapercave.com/fwp/wp12963122.jpg',
),
),
itemCount: banners.length,
viewportFraction: 1,
enableIndicator: true,
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/home_page_content/content.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
class HomePageGeneralItem extends StatelessWidget {
final HomePageContentType content;
const HomePageGeneralItem({super.key, required this.content});
@override
Widget build(BuildContext context) {
return DidvanCard(
padding: EdgeInsets.zero,
child: Column(
children: [
SkeletonImage(
imageUrl: content.image,
height: 124,
width: double.infinity,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
content.title,
style: Theme.of(context).textTheme.bodyLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Column(
children: content.subtitles
.map(
(e) => Row(
children: [
const Icon(
DidvanIcons.puzzle_light,
size: 16,
),
const SizedBox(width: 4),
Expanded(
child: DidvanText(
e,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
)
.toList(),
)
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,90 @@
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/views/home/home/home_page_state.dart';
import 'package:didvan/views/home/home/widgets/banner.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
class HomePageMainContent extends StatelessWidget {
const HomePageMainContent({super.key});
@override
Widget build(BuildContext context) {
final state = context.read<HomePageState>();
return Column(
children: [
const HomePageBanner(
isFirst: true,
),
const SizedBox(height: 8),
Stack(
children: [
Positioned(
bottom: 13,
child: Container(
width: MediaQuery.of(context).size.width,
height: 2,
color: Theme.of(context).colorScheme.border,
),
),
Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
color: Theme.of(context).colorScheme.background,
child: DidvanText(
'دیدوان در یک نگاه',
color: Theme.of(context).colorScheme.title,
style: Theme.of(context).textTheme.titleMedium,
),
),
),
],
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 16,
),
child: Wrap(
alignment: WrapAlignment.center,
children: state.categories
.map(
(e) => SizedBox(
width: (MediaQuery.of(context).size.width - 40) / 4,
child: Column(
children: [
Container(
width: 56,
height: 56,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: DesignConfig.lowBorderRadius,
border: Border.all(
color:
Theme.of(context).colorScheme.focusedBorder,
),
),
child: SvgPicture.asset(e.asset!),
),
const SizedBox(height: 4),
DidvanText(
e.label,
color: Theme.of(context).colorScheme.title,
style: Theme.of(context).textTheme.labelSmall,
fontWeight: FontWeight.w600,
),
const SizedBox(height: 12),
],
),
),
)
.toList(),
),
),
],
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/home_page_content/content.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class HomePageNewsItem extends StatelessWidget {
final HomePageContentType content;
const HomePageNewsItem({super.key, required this.content});
@override
Widget build(BuildContext context) {
return DidvanCard(
padding: EdgeInsets.zero,
child: Column(
children: [
SkeletonImage(
imageUrl: content.image,
width: double.infinity,
height: 160,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
content.title,
style: Theme.of(context).textTheme.bodyLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
DidvanIcons.calendar_day_light,
size: 16,
),
DidvanText(
DateTime.parse(content.subtitles[0]).toPersianDateStr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
)
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,105 @@
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/home_page_content/content.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/views/home/direct/widgets/audio_widget.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class HomePagePodcastItem extends StatelessWidget {
final HomePageContentType content;
const HomePagePodcastItem({super.key, required this.content});
@override
Widget build(BuildContext context) {
return Stack(
children: [
const SizedBox(
height: 180,
width: double.infinity,
),
Positioned.fill(
child: Center(
child: DidvanCard(
child: Row(
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 3,
),
SizedBox(
width: MediaQuery.of(context).size.width * 2 / 3 - 90,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 2 / 3 - 90,
child: AudioWidget(
id: content.id,
audioUrl: content.link,
audioMetaData: StudioDetailsData(
id: content.id,
duration: content.duration!,
title: content.title,
description: '',
image: content.image,
link: content.link,
iframe: null,
createdAt: '',
order: 1,
marked: content.marked,
comments: 0,
tags: [],
type: 'podcast',
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
content.title,
style: Theme.of(context).textTheme.bodyLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
DidvanIcons.calendar_day_light,
size: 16,
),
DidvanText(
DateTime.parse(content.subtitles[0])
.toPersianDateStr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.bodySmall,
),
],
),
)
],
),
],
),
),
],
),
),
),
),
SkeletonImage(
width: MediaQuery.of(context).size.width / 3,
height: 180,
imageUrl: content.image,
),
],
);
}
}

View File

@ -0,0 +1,112 @@
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/home_page_content/content.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/home/home/home_page_state.dart';
import 'package:didvan/views/widgets/didvan/card.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/svg.dart';
import 'package:provider/provider.dart';
class HomePageRadarItem extends StatelessWidget {
final HomePageContentType content;
const HomePageRadarItem({super.key, required this.content});
@override
Widget build(BuildContext context) {
return DidvanCard(
padding: EdgeInsets.zero,
child: Stack(
children: [
Column(
children: [
SkeletonImage(
imageUrl: content.image,
width: double.infinity,
height: 76,
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
content.title,
style: Theme.of(context).textTheme.bodyLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
DidvanIcons.calendar_day_light,
size: 16,
),
DidvanText(
DateTimeUtils.momentGenerator(
content.subtitles[0]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
DidvanIcons.timer_light,
size: 16,
),
DidvanText(
'${content.subtitles[1]} دقیقه',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
),
)
],
),
),
],
),
Center(
child: Container(
margin: EdgeInsets.only(
right: MediaQuery.of(context).size.width * 0.4,
),
width: 36,
height: 36,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surface,
),
child: SvgPicture.asset(
context
.read<HomePageState>()
.categories
.firstWhere((element) =>
element.id.toString() == content.subtitles[2])
.asset ??
'',
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,90 @@
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/home_page_content/content.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class HomePageVideocastItem extends StatelessWidget {
final HomePageContentType content;
const HomePageVideocastItem({super.key, required this.content});
@override
Widget build(BuildContext context) {
return DidvanCard(
padding: EdgeInsets.zero,
child: Stack(
children: [
SkeletonImage(
width: double.infinity,
height: double.infinity,
imageUrl: content.image,
),
Center(
child: Container(
height: 36,
width: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.secondary.withOpacity(0.7),
),
child: Icon(
DidvanIcons.play_solid,
color: Theme.of(context).colorScheme.white,
),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
boxShadow: DesignConfig.defaultShadow,
color: Theme.of(context).colorScheme.surface,
),
width: MediaQuery.of(context).size.width / 3,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
content.title,
style: Theme.of(context).textTheme.bodyLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
DidvanIcons.calendar_day_light,
size: 16,
),
DidvanText(
DateTime.parse(content.subtitles[0])
.toPersianDateStr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
)
],
),
),
),
],
),
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:didvan/providers/core.dart'; import 'package:didvan/providers/core.dart';
class HomeState extends CoreProvier { class HomeState extends CoreProvier {
int _currentPageIndex = 2; int _currentPageIndex = 0;
set currentPageIndex(int value) { set currentPageIndex(int value) {
_currentPageIndex = value; _currentPageIndex = value;

View File

@ -9,7 +9,6 @@ import 'package:didvan/views/home/statistic/statistic_state.dart';
import 'package:didvan/views/home/statistic/widgets/statistic_overview.dart'; import 'package:didvan/views/home/statistic/widgets/statistic_overview.dart';
import 'package:didvan/views/home/widgets/categories_gird.dart'; import 'package:didvan/views/home/widgets/categories_gird.dart';
import 'package:didvan/views/home/widgets/categories_list.dart'; import 'package:didvan/views/home/widgets/categories_list.dart';
import 'package:didvan/views/home/widgets/logo_app_bar.dart';
import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/didvan/text.dart';
@ -57,10 +56,9 @@ class _StatisticState extends State<Statistic> {
: const ClampingScrollPhysics(), : const ClampingScrollPhysics(),
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [
const SliverToBoxAdapter(child: LogoAppBar()),
if (state.appState != AppState.failed) if (state.appState != AppState.failed)
const SliverToBoxAdapter( const SliverToBoxAdapter(
child: SizedBox(height: 180), child: SizedBox(height: 120),
), ),
if (state.appState != AppState.failed && if (state.appState != AppState.failed &&
state.markedStatistics.isNotEmpty) state.markedStatistics.isNotEmpty)
@ -135,7 +133,7 @@ class _StatisticState extends State<Statistic> {
onSelected: _onCategorySelected, onSelected: _onCategorySelected,
categories: List.from(state.categories)..removeAt(0), categories: List.from(state.categories)..removeAt(0),
isColapsed: state.isColapsed, isColapsed: state.isColapsed,
topPadding: 144, topPadding: 20,
rightPadding: 300, rightPadding: 300,
), ),
if (state.appState != AppState.failed) if (state.appState != AppState.failed)
@ -177,21 +175,20 @@ class _StatisticState extends State<Statistic> {
_isAnimating = true; _isAnimating = true;
setState(() {}); setState(() {});
await _scrollController.animateTo( await _scrollController.animateTo(
228, 60,
duration: DesignConfig.mediumAnimationDuration, duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn, curve: Curves.easeIn,
); );
_isAnimating = false; _isAnimating = false;
setState(() {}); setState(() {});
} else if (position < } else if (position < min(_scrollController.position.maxScrollExtent, 40) &&
min(_scrollController.position.maxScrollExtent, 228) &&
state.isColapsed) { state.isColapsed) {
state.isScrolled = false; state.isScrolled = false;
_isAnimating = true; _isAnimating = true;
setState(() {}); setState(() {});
await _scrollController.animateTo( await _scrollController.animateTo(
0, 0,
duration: DesignConfig.mediumAnimationDuration, duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn, curve: Curves.easeIn,
); );
_isAnimating = false; _isAnimating = false;

View File

@ -37,7 +37,8 @@ class AudioSlider extends StatelessWidget {
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
baseBarColor: Theme.of(context).colorScheme.border, baseBarColor: Theme.of(context).colorScheme.border,
bufferedBarColor: Theme.of(context).colorScheme.splash, bufferedBarColor: Theme.of(context).colorScheme.splash,
total: MediaService.duration ?? Duration(seconds: duration ?? 0), total: Duration(
seconds: duration ?? MediaService.duration?.inSeconds ?? 0),
progress: snapshot.data ?? Duration.zero, progress: snapshot.data ?? Duration.zero,
thumbRadius: disableThumb ? 0 : 6, thumbRadius: disableThumb ? 0 : 6,
barHeight: 3, barHeight: 3,

View File

@ -1,6 +1,26 @@
import 'package:assets_audio_player/assets_audio_player.dart';
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/enums.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/home/widgets/audio/audio_player_widget.dart';
import 'package:didvan/views/home/widgets/audio/audio_slider.dart';
import 'package:didvan/views/podcasts/podcasts_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/didvan/app_bar.dart'; import 'package:didvan/views/widgets/didvan/app_bar.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:provider/provider.dart';
class DidvanScaffold extends StatefulWidget { class DidvanScaffold extends StatefulWidget {
final List<Widget>? slivers; final List<Widget>? slivers;
@ -42,75 +62,88 @@ class _DidvanScaffoldState extends State<DidvanScaffold> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top; final double statusBarHeight = MediaQuery.of(context).padding.top;
final double systemNavigationBarHeight =
MediaQuery.of(context).padding.bottom;
return Scaffold( return Scaffold(
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
body: Padding( body: Padding(
padding: widget.appBarData == null padding: widget.appBarData == null
? EdgeInsets.zero ? EdgeInsets.zero
: EdgeInsets.only(top: statusBarHeight), : EdgeInsets.only(top: statusBarHeight),
child: Stack( child: SizedBox(
children: [ height: MediaQuery.of(context).size.height -
CustomScrollView( statusBarHeight -
physics: widget.physics, systemNavigationBarHeight,
controller: _scrollController, child: Stack(
reverse: widget.reverse, children: [
slivers: [ CustomScrollView(
if (!widget.reverse && widget.appBarData != null) physics: widget.physics,
SliverAppBar( controller: _scrollController,
toolbarHeight: (widget.appBarData!.isSmall ? 56 : 72) - reverse: widget.reverse,
statusBarHeight, slivers: [
automaticallyImplyLeading: false, if (!widget.reverse && widget.appBarData != null)
pinned: true, SliverAppBar(
backgroundColor: widget.backgroundColor ?? toolbarHeight: (widget.appBarData!.isSmall ? 56 : 72) -
Theme.of(context).colorScheme.background, statusBarHeight,
flexibleSpace: DidvanAppBar( automaticallyImplyLeading: false,
appBarData: widget.appBarData!, pinned: true,
backgroundColor: widget.backgroundColor ?? backgroundColor: widget.backgroundColor ??
Theme.of(context).colorScheme.background, Theme.of(context).colorScheme.background,
), flexibleSpace: DidvanAppBar(
), appBarData: widget.appBarData!,
if (widget.children != null && !widget.showSliversFirst) backgroundColor: widget.backgroundColor ??
SliverPadding( Theme.of(context).colorScheme.background,
padding: widget.padding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => widget.children![index],
childCount: widget.children!.length,
), ),
), ),
), if (widget.children != null && !widget.showSliversFirst)
if (widget.slivers != null)
for (var i = 0; i < widget.slivers!.length; i++)
SliverPadding( SliverPadding(
padding: widget.padding, padding: widget.padding,
sliver: widget.slivers![i], sliver: SliverList(
), delegate: SliverChildBuilderDelegate(
if (widget.children != null && widget.showSliversFirst) (context, index) => widget.children![index],
SliverPadding( childCount: widget.children!.length,
padding: widget.padding, ),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => widget.children![index],
childCount: widget.children!.length,
), ),
), ),
), if (widget.slivers != null)
if (widget.reverse) for (var i = 0; i < widget.slivers!.length; i++)
SliverToBoxAdapter( SliverPadding(
child: SizedBox( padding: widget.padding,
height: kToolbarHeight + sliver: widget.slivers![i],
MediaQuery.of(context).padding.top + ),
12, if (widget.children != null && widget.showSliversFirst)
SliverPadding(
padding: widget.padding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => widget.children![index],
childCount: widget.children!.length,
),
),
), ),
), if (widget.reverse)
], SliverToBoxAdapter(
), child: SizedBox(
if (widget.reverse && widget.appBarData != null) height: kToolbarHeight +
_AppBar( MediaQuery.of(context).padding.top +
appBarData: widget.appBarData!, 12,
scrollController: _scrollController, ),
),
],
), ),
], if (widget.reverse && widget.appBarData != null)
_AppBar(
appBarData: widget.appBarData!,
scrollController: _scrollController,
),
const Positioned(
bottom: 20,
left: 0,
right: 0,
child: _PlayerNavBar(),
),
],
),
), ),
), ),
); );
@ -160,3 +193,272 @@ class __AppBarState extends State<_AppBar> {
); );
} }
} }
class _PlayerNavBar extends StatelessWidget {
const _PlayerNavBar({Key? key}) : super(key: key);
bool _enablePlayerController(StudioDetailsState state) =>
MediaService.currentPodcast != null ||
(MediaService.audioPlayerTag?.contains('podcast') ?? false);
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: MediaService.audioPlayer.isPlaying,
builder: (context, snapshot) => GestureDetector(
onTap: () => MediaService.currentPodcast == null ||
MediaService.currentPodcast?.description == 'radar'
? Navigator.of(context).pushNamed(
Routes.radarDetails,
arguments: {
'onMarkChanged': (id, value) {},
'onCommentsChanged': (id, value) {},
'id': MediaService.currentPodcast?.id,
'args': const RadarRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
},
)
: _showPlayerBottomSheet(context),
child: Consumer<StudioDetailsState>(
builder: (context, state, child) => AnimatedContainer(
padding: const EdgeInsets.only(top: 12),
duration: DesignConfig.lowAnimationDuration,
height: _enablePlayerController(state) ? 60 : 0,
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focused
: Theme.of(context).colorScheme.navigation,
borderRadius: BorderRadius.circular(200),
),
alignment: Alignment.topCenter,
child: Builder(builder: (context) {
if (!_enablePlayerController(state)) return const SizedBox();
if (state.appState == AppState.failed) {
Future.delayed(const Duration(seconds: 2), () {
MediaService.resetAudioPlayer();
});
return DidvanText(
'اتصال اینترنت برقرار نمی‌باشد',
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
);
}
if (MediaService.currentPodcast == null) {
return SizedBox(
height: 32,
child: Center(
child: SpinKitThreeBounce(
size: 18,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
),
);
}
return SizedBox(
height: 56,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 8,
),
child: DidvanIconButton(
icon: DidvanIcons.close_regular,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
gestureSize: 32,
onPressed: MediaService.resetAudioPlayer,
),
),
SkeletonImage(
imageUrl: MediaService.currentPodcast!.image,
width: 32,
height: 32,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
MediaService.currentPodcast!.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
),
AudioSlider(
disableThumb: true,
tag: MediaService.audioPlayerTag!,
),
],
),
),
StreamBuilder<PlayingAudio?>(
stream: MediaService.audioPlayer.onReadyToPlay,
builder: (context, snapshot) {
if (snapshot.data == null ||
state.appState == AppState.busy &&
MediaService.currentPodcast?.description !=
'radar') {
return Padding(
padding: const EdgeInsets.only(
top: 4,
left: 16,
right: 16,
),
child: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
),
);
}
return const SizedBox();
},
),
if (state.appState != AppState.busy &&
snapshot.data != null ||
MediaService.currentPodcast?.description == 'radar')
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 12,
),
child: DidvanIconButton(
gestureSize: 32,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
icon: snapshot.data!
? DidvanIcons.pause_solid
: DidvanIcons.play_solid,
onPressed: () {
if (state.args?.type == 'video') {
state.getStudioDetails(
MediaService.currentPodcast!.id,
args: state.podcastArgs,
fetchOnly: true,
);
}
MediaService.handleAudioPlayback(
audioSource: MediaService.currentPodcast!.link,
id: MediaService.currentPodcast!.id,
isVoiceMessage: false,
);
},
),
),
],
),
);
}),
),
),
),
);
}
void _showPlayerBottomSheet(BuildContext context) {
final sheetKey = GlobalKey<ExpandableBottomSheetState>();
bool isExpanded = false;
final detailsState = context.read<StudioDetailsState>();
if (detailsState.args?.type == 'video') {
detailsState.getStudioDetails(
MediaService.currentPodcast!.id,
args: detailsState.podcastArgs,
fetchOnly: true,
);
}
final state = context.read<PodcastsState>();
showModalBottomSheet(
constraints: BoxConstraints(
maxWidth: ActionSheetUtils.mediaQueryData.size.width,
),
backgroundColor: Colors.transparent,
context: context,
isScrollControlled: true,
builder: (context) => ChangeNotifierProvider<PodcastsState>.value(
value: state,
child: Consumer<StudioDetailsState>(
builder: (context, state, child) => MediaQuery(
data: ActionSheetUtils.mediaQueryData,
child: ExpandableBottomSheet(
key: sheetKey,
background: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: MediaQuery.of(context).size.height * 0.7,
color: Theme.of(context).colorScheme.surface,
),
),
persistentHeader: GestureDetector(
onVerticalDragUpdate: (details) {
if (details.delta.dy > 10) {
Navigator.of(context).pop();
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AudioPlayerWidget(
podcast: MediaService.currentPodcast!,
),
Container(
width: MediaQuery.of(context).size.width,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
DidvanIconButton(
size: 32,
icon: DidvanIcons.angle_up_regular,
onPressed: () {
if (!isExpanded) {
sheetKey.currentState?.expand();
isExpanded = true;
} else {
isExpanded = false;
sheetKey.currentState?.contract();
}
},
),
const SizedBox(height: 16),
],
),
),
],
),
),
expandableContent: state.appState == AppState.busy
? Container(
height: MediaQuery.of(context).size.height / 2,
alignment: Alignment.center,
child: SpinKitSpinningLines(
color: Theme.of(context).colorScheme.primary,
),
)
: StudioDetailsWidget(
onMarkChanged: (id, value) => context
.read<PodcastsState>()
.changeMark(id, value, true),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
class DidvanSlider extends StatefulWidget {
final Widget Function(BuildContext context, int index, int realIndex)
itemBuilder;
final int itemCount;
final double viewportFraction;
final bool? enableIndicator;
final double? height;
final void Function(int, CarouselPageChangedReason)? onPageChanged;
const DidvanSlider({
super.key,
required this.itemBuilder,
required this.itemCount,
required this.viewportFraction,
this.onPageChanged,
this.enableIndicator,
this.height,
});
@override
State<DidvanSlider> createState() => _DidvanSliderState();
}
class _DidvanSliderState extends State<DidvanSlider> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
CarouselSlider.builder(
itemCount: widget.itemCount,
itemBuilder: widget.itemBuilder,
options: CarouselOptions(
height: widget.height,
autoPlay: true,
padEnds: false,
viewportFraction: widget.viewportFraction,
onPageChanged: (index, reason) {
widget.onPageChanged?.call(index, reason);
if (widget.enableIndicator == true) {
setState(() {
_currentIndex = index;
});
}
},
),
),
if (widget.enableIndicator == true) ...[
const SizedBox(height: 8),
_CarouselIndicator(
count: widget.itemCount,
currentIndex: _currentIndex,
),
],
],
);
}
}
class _CarouselIndicator extends StatelessWidget {
final int count;
final int currentIndex;
const _CarouselIndicator({required this.count, required this.currentIndex});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < count; i++)
Container(
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 1,
color: Theme.of(context).colorScheme.primary,
),
color: currentIndex == i
? Theme.of(context).colorScheme.primary
: Colors.transparent,
),
)
],
);
}
}

View File

@ -33,7 +33,9 @@ class SkeletonImage extends StatelessWidget {
httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'}, httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'},
width: width, width: width,
height: height, height: height,
imageUrl: RequestHelper.baseUrl + imageUrl, imageUrl: imageUrl.startsWith('http')
? imageUrl
: RequestHelper.baseUrl + imageUrl,
placeholder: (context, _) => const ShimmerPlaceholder(), placeholder: (context, _) => const ShimmerPlaceholder(),
), ),
), ),