From 1fe0e8def5a7fe38081a07989ed47de4089a45a6 Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Fri, 6 Oct 2023 18:52:06 +0330 Subject: [PATCH] home page v1 --- lib/models/home_page_content/banner.dart | 13 + lib/models/home_page_content/content.dart | 12 +- .../home_page_content/home_page_content.dart | 8 +- .../home_page_content/home_page_list.dart | 6 +- lib/routes/route_generator.dart | 3 + .../home/direct/widgets/audio_widget.dart | 18 +- lib/views/home/home/home_page.dart | 252 +++++++---- lib/views/home/home/home_page_state.dart | 67 ++- lib/views/home/home/widgets/banner.dart | 25 ++ lib/views/home/home/widgets/general_item.dart | 64 +++ lib/views/home/home/widgets/main_content.dart | 90 ++++ lib/views/home/home/widgets/news_item.dart | 60 +++ lib/views/home/home/widgets/podcast_item.dart | 105 +++++ lib/views/home/home/widgets/radar_item.dart | 112 +++++ .../home/home/widgets/videocast_item.dart | 90 ++++ lib/views/home/home_state.dart | 2 +- lib/views/home/statistic/statistic.dart | 15 +- .../home/widgets/audio/audio_slider.dart | 3 +- lib/views/widgets/didvan/scaffold.dart | 408 +++++++++++++++--- lib/views/widgets/didvan/slider.dart | 92 ++++ lib/views/widgets/skeleton_image.dart | 4 +- 21 files changed, 1282 insertions(+), 167 deletions(-) create mode 100644 lib/models/home_page_content/banner.dart create mode 100644 lib/views/home/home/widgets/banner.dart create mode 100644 lib/views/home/home/widgets/general_item.dart create mode 100644 lib/views/home/home/widgets/main_content.dart create mode 100644 lib/views/home/home/widgets/news_item.dart create mode 100644 lib/views/home/home/widgets/podcast_item.dart create mode 100644 lib/views/home/home/widgets/radar_item.dart create mode 100644 lib/views/home/home/widgets/videocast_item.dart create mode 100644 lib/views/widgets/didvan/slider.dart diff --git a/lib/models/home_page_content/banner.dart b/lib/models/home_page_content/banner.dart new file mode 100644 index 0000000..743104e --- /dev/null +++ b/lib/models/home_page_content/banner.dart @@ -0,0 +1,13 @@ +class HomePageBannerType { + final String image; + final String? link; + + HomePageBannerType({required this.image, required this.link}); + + factory HomePageBannerType.fromJson(Map json) { + return HomePageBannerType( + image: json['image'], + link: json['link'], + ); + } +} diff --git a/lib/models/home_page_content/content.dart b/lib/models/home_page_content/content.dart index 010245a..8799c39 100644 --- a/lib/models/home_page_content/content.dart +++ b/lib/models/home_page_content/content.dart @@ -1,26 +1,30 @@ -class Content { - final int? id; +class HomePageContentType { + final int id; final String title; final String image; final String link; final bool marked; final List subtitles; + final int? duration; - const Content({ + const HomePageContentType({ required this.id, required this.title, required this.image, required this.link, required this.marked, required this.subtitles, + this.duration, }); - factory Content.fromJson(Map json) => Content( + factory HomePageContentType.fromJson(Map json) => + HomePageContentType( id: json['id'], title: json['title'], image: json['image'], link: json['link'], marked: json['marked'], subtitles: List.from(json['subtitles']), + duration: json['duration'], ); } diff --git a/lib/models/home_page_content/home_page_content.dart b/lib/models/home_page_content/home_page_content.dart index b270668..ea345c7 100644 --- a/lib/models/home_page_content/home_page_content.dart +++ b/lib/models/home_page_content/home_page_content.dart @@ -1,7 +1,9 @@ +import 'package:didvan/models/home_page_content/banner.dart'; + import 'home_page_list.dart'; class HomePageContent { - final List banners; + final List banners; final List lists; final int unread; @@ -10,7 +12,9 @@ class HomePageContent { factory HomePageContent.fromJson(Map json) { return HomePageContent( - banners: json['banners'], + banners: List.from(json['banners'].map( + (x) => HomePageBannerType.fromJson(x), + )), lists: List.from( json['lists'].map( (x) => HomePageList.fromJson(x), diff --git a/lib/models/home_page_content/home_page_list.dart b/lib/models/home_page_content/home_page_list.dart index 9efa979..af29fe3 100644 --- a/lib/models/home_page_content/home_page_list.dart +++ b/lib/models/home_page_content/home_page_list.dart @@ -5,7 +5,7 @@ class HomePageList { final String header; final String more; final String link; - final List contents; + final List contents; const HomePageList({ required this.type, @@ -20,7 +20,7 @@ class HomePageList { header: json['header'], more: json['more'], link: json['link'], - contents: List.from(json['contents'].map( - (x) => Content.fromJson(x), + contents: List.from(json['contents'].map( + (x) => HomePageContentType.fromJson(x), ))); } diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 1c363cc..4ef7dfb 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -68,6 +68,9 @@ class RouteGenerator { ChangeNotifierProvider( create: (context) => StatisticState(), ), + ChangeNotifierProvider( + create: (context) => PodcastsState(), + ), ], child: const Home(), ), diff --git a/lib/views/home/direct/widgets/audio_widget.dart b/lib/views/home/direct/widgets/audio_widget.dart index 95c5ba6..a0c4cf4 100644 --- a/lib/views/home/direct/widgets/audio_widget.dart +++ b/lib/views/home/direct/widgets/audio_widget.dart @@ -3,11 +3,14 @@ import 'dart:io'; import 'package:assets_audio_player/assets_audio_player.dart'; import 'package:didvan/config/theme_data.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/services/media/media.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:flutter/material.dart'; +import 'package:provider/provider.dart'; class AudioWidget extends StatelessWidget { final String? audioUrl; @@ -34,7 +37,9 @@ class AudioWidget extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(top: 12), child: AudioSlider( - tag: audioMetaData != null ? 'radar-$id' : 'message-$id', + tag: audioMetaData != null + ? '${audioMetaData!.type}-$id' + : 'message-$id', duration: audioMetaData?.duration, showTimer: true, ), @@ -70,7 +75,7 @@ class _AudioControllerButton extends StatelessWidget { bool get _nowPlaying => MediaService.audioPlayerTag == - (audioMetaData != null ? 'radar-$id' : 'message-$id'); + (audioMetaData != null ? '${audioMetaData!.type}-$id' : 'message-$id'); @override Widget build(BuildContext context) { @@ -84,6 +89,15 @@ class _AudioControllerButton extends StatelessWidget { gestureSize: 36, color: Theme.of(context).colorScheme.focusedBorder, onPressed: () async { + if (audioMetaData?.type == 'podcast') { + final state = context.read(); + if (MediaService.currentPodcast == null) { + await state.getStudioDetails( + id, + args: const StudioRequestArgs(page: 0, type: 'podcast'), + ); + } + } if (snapshot.data == null && _nowPlaying) { return; } diff --git a/lib/views/home/home/home_page.dart b/lib/views/home/home/home_page.dart index 728ef6c..f6eff1f 100644 --- a/lib/views/home/home/home_page.dart +++ b/lib/views/home/home/home_page.dart @@ -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/constants/app_icons.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/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/skeleton_image.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -21,10 +27,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { @override void initState() { - Future.delayed( - Duration.zero, - context.read().getMainPageContent, - ); + context.read().init(); super.initState(); } @@ -33,37 +36,42 @@ class _HomePageState extends State { return StateHandler( onRetry: () {}, state: context.watch(), - builder: (context, state) => - ListView.builder(itemBuilder: (context, index) { - if (index == 0) { - return CarouselSlider( - items: state.content.banners - .map((e) => SkeletonImage( - imageUrl: e.imageUrl, - )) - .toList(), - options: CarouselOptions( - viewportFraction: 1.0, - ), + builder: (context, state) => ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 16), + itemBuilder: (context, index) { + if (index == 0) { + return const HomePageMainContent(); + } + index--; + if (index == 4) { + return const Padding( + padding: EdgeInsets.only(top: 32), + 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(); - // 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(), - }), + }, + itemCount: state.content.lists.length + 2, + ), ); } } -class _HomePageItem extends StatelessWidget { +class _HomePageSection extends StatelessWidget { 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')) { launchUrl(Uri.parse(list.link)); return; @@ -71,8 +79,34 @@ class _HomePageItem extends StatelessWidget { 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 Widget build(BuildContext context) { + final icon = _generateIcon(); return Column( children: [ Padding( @@ -85,13 +119,19 @@ class _HomePageItem extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - DidvanText( - list.header, - style: Theme.of(context).textTheme.titleMedium, - color: Theme.of(context).colorScheme.title, + Row( + children: [ + if (icon != null) Icon(icon), + const SizedBox(width: 4), + DidvanText( + list.header, + style: Theme.of(context).textTheme.titleMedium, + color: Theme.of(context).colorScheme.title, + ), + ], ), GestureDetector( - onTap: () => moreHandler(context), + onTap: () => _moreHandler(context), child: Row( children: [ DidvanText( @@ -108,63 +148,97 @@ class _HomePageItem extends StatelessWidget { ], ), ), - CarouselSlider.builder( - itemCount: list.contents.length, - itemBuilder: (context, index, realIndex) => DidvanCard( - margin: const EdgeInsets.only(left: 8), - padding: EdgeInsets.zero, - child: Column( - children: [ - SkeletonImage( - imageUrl: list.contents[index].image, - 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, - ), - ), - ), - ], - ) - ], - ), - ), - ], + if (list.type == 'news') + DidvanSlider( + height: 232, + itemCount: list.contents.length, + viewportFraction: 0.45, + itemBuilder: (context, index, realIndex) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: HomePageNewsItem( + content: list.contents[index], + ), ), ), - options: CarouselOptions( - autoPlay: true, - padEnds: false, - viewportFraction: 0.7, + if (list.type == 'radar') + DidvanSlider( + height: 148, + 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, + ), + ); + } +} diff --git a/lib/views/home/home/home_page_state.dart b/lib/views/home/home/home_page_state.dart index 544cb0b..99c111b 100644 --- a/lib/views/home/home/home_page_state.dart +++ b/lib/views/home/home/home_page_state.dart @@ -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/home_page_content/home_page_content.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'; class HomePageState extends CoreProvier { - late final HomePageContent content; - Future getMainPageContent() async { + late HomePageContent content; + List categories = []; + Future _getMainPageContent() async { final service = RequestService(RequestHelper.mainPageContent); await service.httpGet(); if (service.isSuccess) { @@ -16,4 +19,64 @@ class HomePageState extends CoreProvier { } 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(); + }); + } } diff --git a/lib/views/home/home/widgets/banner.dart b/lib/views/home/home/widgets/banner.dart new file mode 100644 index 0000000..4893081 --- /dev/null +++ b/lib/views/home/home/widgets/banner.dart @@ -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(); + 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, + ); + } +} diff --git a/lib/views/home/home/widgets/general_item.dart b/lib/views/home/home/widgets/general_item.dart new file mode 100644 index 0000000..e63eb4f --- /dev/null +++ b/lib/views/home/home/widgets/general_item.dart @@ -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(), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/home/home/widgets/main_content.dart b/lib/views/home/home/widgets/main_content.dart new file mode 100644 index 0000000..4a99379 --- /dev/null +++ b/lib/views/home/home/widgets/main_content.dart @@ -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(); + 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(), + ), + ), + ], + ); + } +} diff --git a/lib/views/home/home/widgets/news_item.dart b/lib/views/home/home/widgets/news_item.dart new file mode 100644 index 0000000..b18f5a2 --- /dev/null +++ b/lib/views/home/home/widgets/news_item.dart @@ -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, + ), + ], + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/home/home/widgets/podcast_item.dart b/lib/views/home/home/widgets/podcast_item.dart new file mode 100644 index 0000000..6464b9e --- /dev/null +++ b/lib/views/home/home/widgets/podcast_item.dart @@ -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, + ), + ], + ); + } +} diff --git a/lib/views/home/home/widgets/radar_item.dart b/lib/views/home/home/widgets/radar_item.dart new file mode 100644 index 0000000..806b73f --- /dev/null +++ b/lib/views/home/home/widgets/radar_item.dart @@ -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() + .categories + .firstWhere((element) => + element.id.toString() == content.subtitles[2]) + .asset ?? + '', + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/home/home/widgets/videocast_item.dart b/lib/views/home/home/widgets/videocast_item.dart new file mode 100644 index 0000000..1779187 --- /dev/null +++ b/lib/views/home/home/widgets/videocast_item.dart @@ -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, + ), + ], + ), + ) + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/home/home_state.dart b/lib/views/home/home_state.dart index c866e0e..fef71a4 100644 --- a/lib/views/home/home_state.dart +++ b/lib/views/home/home_state.dart @@ -1,7 +1,7 @@ import 'package:didvan/providers/core.dart'; class HomeState extends CoreProvier { - int _currentPageIndex = 2; + int _currentPageIndex = 0; set currentPageIndex(int value) { _currentPageIndex = value; diff --git a/lib/views/home/statistic/statistic.dart b/lib/views/home/statistic/statistic.dart index 527ad87..cde52a3 100644 --- a/lib/views/home/statistic/statistic.dart +++ b/lib/views/home/statistic/statistic.dart @@ -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/widgets/categories_gird.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/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; @@ -57,10 +56,9 @@ class _StatisticState extends State { : const ClampingScrollPhysics(), controller: _scrollController, slivers: [ - const SliverToBoxAdapter(child: LogoAppBar()), if (state.appState != AppState.failed) const SliverToBoxAdapter( - child: SizedBox(height: 180), + child: SizedBox(height: 120), ), if (state.appState != AppState.failed && state.markedStatistics.isNotEmpty) @@ -135,7 +133,7 @@ class _StatisticState extends State { onSelected: _onCategorySelected, categories: List.from(state.categories)..removeAt(0), isColapsed: state.isColapsed, - topPadding: 144, + topPadding: 20, rightPadding: 300, ), if (state.appState != AppState.failed) @@ -177,21 +175,20 @@ class _StatisticState extends State { _isAnimating = true; setState(() {}); await _scrollController.animateTo( - 228, - duration: DesignConfig.mediumAnimationDuration, + 60, + duration: DesignConfig.lowAnimationDuration, curve: Curves.easeIn, ); _isAnimating = false; setState(() {}); - } else if (position < - min(_scrollController.position.maxScrollExtent, 228) && + } else if (position < min(_scrollController.position.maxScrollExtent, 40) && state.isColapsed) { state.isScrolled = false; _isAnimating = true; setState(() {}); await _scrollController.animateTo( 0, - duration: DesignConfig.mediumAnimationDuration, + duration: DesignConfig.lowAnimationDuration, curve: Curves.easeIn, ); _isAnimating = false; diff --git a/lib/views/home/widgets/audio/audio_slider.dart b/lib/views/home/widgets/audio/audio_slider.dart index bf8969e..e479c0c 100644 --- a/lib/views/home/widgets/audio/audio_slider.dart +++ b/lib/views/home/widgets/audio/audio_slider.dart @@ -37,7 +37,8 @@ class AudioSlider extends StatelessWidget { : Theme.of(context).colorScheme.primary, baseBarColor: Theme.of(context).colorScheme.border, 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, thumbRadius: disableThumb ? 0 : 6, barHeight: 3, diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index 879e240..d436e6f 100644 --- a/lib/views/widgets/didvan/scaffold.dart +++ b/lib/views/widgets/didvan/scaffold.dart @@ -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/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/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_spinkit/flutter_spinkit.dart'; +import 'package:provider/provider.dart'; class DidvanScaffold extends StatefulWidget { final List? slivers; @@ -42,75 +62,88 @@ class _DidvanScaffoldState extends State { @override Widget build(BuildContext context) { final double statusBarHeight = MediaQuery.of(context).padding.top; + final double systemNavigationBarHeight = + MediaQuery.of(context).padding.bottom; return Scaffold( backgroundColor: widget.backgroundColor, body: Padding( padding: widget.appBarData == null ? EdgeInsets.zero : EdgeInsets.only(top: statusBarHeight), - child: Stack( - children: [ - CustomScrollView( - physics: widget.physics, - controller: _scrollController, - reverse: widget.reverse, - slivers: [ - if (!widget.reverse && widget.appBarData != null) - SliverAppBar( - toolbarHeight: (widget.appBarData!.isSmall ? 56 : 72) - - statusBarHeight, - automaticallyImplyLeading: false, - pinned: true, - backgroundColor: widget.backgroundColor ?? - Theme.of(context).colorScheme.background, - flexibleSpace: DidvanAppBar( - appBarData: widget.appBarData!, + child: SizedBox( + height: MediaQuery.of(context).size.height - + statusBarHeight - + systemNavigationBarHeight, + child: Stack( + children: [ + CustomScrollView( + physics: widget.physics, + controller: _scrollController, + reverse: widget.reverse, + slivers: [ + if (!widget.reverse && widget.appBarData != null) + SliverAppBar( + toolbarHeight: (widget.appBarData!.isSmall ? 56 : 72) - + statusBarHeight, + automaticallyImplyLeading: false, + pinned: true, backgroundColor: widget.backgroundColor ?? Theme.of(context).colorScheme.background, - ), - ), - if (widget.children != null && !widget.showSliversFirst) - SliverPadding( - padding: widget.padding, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => widget.children![index], - childCount: widget.children!.length, + flexibleSpace: DidvanAppBar( + appBarData: widget.appBarData!, + backgroundColor: widget.backgroundColor ?? + Theme.of(context).colorScheme.background, ), ), - ), - if (widget.slivers != null) - for (var i = 0; i < widget.slivers!.length; i++) + if (widget.children != null && !widget.showSliversFirst) SliverPadding( padding: widget.padding, - sliver: widget.slivers![i], - ), - if (widget.children != null && widget.showSliversFirst) - SliverPadding( - padding: widget.padding, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => widget.children![index], - childCount: widget.children!.length, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.children![index], + childCount: widget.children!.length, + ), ), ), - ), - if (widget.reverse) - SliverToBoxAdapter( - child: SizedBox( - height: kToolbarHeight + - MediaQuery.of(context).padding.top + - 12, + if (widget.slivers != null) + for (var i = 0; i < widget.slivers!.length; i++) + SliverPadding( + padding: widget.padding, + sliver: widget.slivers![i], + ), + 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 && widget.appBarData != null) - _AppBar( - appBarData: widget.appBarData!, - scrollController: _scrollController, + if (widget.reverse) + SliverToBoxAdapter( + child: SizedBox( + height: kToolbarHeight + + MediaQuery.of(context).padding.top + + 12, + ), + ), + ], ), - ], + 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( + 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( + 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( + 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(); + bool isExpanded = false; + final detailsState = context.read(); + if (detailsState.args?.type == 'video') { + detailsState.getStudioDetails( + MediaService.currentPodcast!.id, + args: detailsState.podcastArgs, + fetchOnly: true, + ); + } + final state = context.read(); + showModalBottomSheet( + constraints: BoxConstraints( + maxWidth: ActionSheetUtils.mediaQueryData.size.width, + ), + backgroundColor: Colors.transparent, + context: context, + isScrollControlled: true, + builder: (context) => ChangeNotifierProvider.value( + value: state, + child: Consumer( + 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() + .changeMark(id, value, true), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/slider.dart b/lib/views/widgets/didvan/slider.dart new file mode 100644 index 0000000..93fa533 --- /dev/null +++ b/lib/views/widgets/didvan/slider.dart @@ -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 createState() => _DidvanSliderState(); +} + +class _DidvanSliderState extends State { + 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, + ), + ) + ], + ); + } +} diff --git a/lib/views/widgets/skeleton_image.dart b/lib/views/widgets/skeleton_image.dart index 68724b2..e07c73a 100644 --- a/lib/views/widgets/skeleton_image.dart +++ b/lib/views/widgets/skeleton_image.dart @@ -33,7 +33,9 @@ class SkeletonImage extends StatelessWidget { httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'}, width: width, height: height, - imageUrl: RequestHelper.baseUrl + imageUrl, + imageUrl: imageUrl.startsWith('http') + ? imageUrl + : RequestHelper.baseUrl + imageUrl, placeholder: (context, _) => const ShimmerPlaceholder(), ), ),