From 5dd6c4205fab36874c2e3fc5fda4a26b629439b3 Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Wed, 23 Mar 2022 13:58:58 +0430 Subject: [PATCH] D1APP-99 studio (beta 2) --- lib/main.dart | 4 + lib/models/overview_data.dart | 2 +- lib/models/slider_data.dart | 27 ++ lib/providers/user_provider.dart | 6 +- lib/routes/route_generator.dart | 22 +- lib/services/network/request_helper.dart | 28 +- lib/views/home/comments/comments.dart | 23 +- lib/views/home/comments/comments_state.dart | 26 +- .../home/settings/bookmarks/bookmarks.dart | 10 +- .../filtered_bookmark/filtered_bookmark.dart | 29 +- .../filtered_bookmarks_state.dart | 14 +- lib/views/home/studio/studio.dart | 52 +++- ...etails.dart => studio_details.mobile.dart} | 122 ++++---- .../studio_details/studio_details.web.dart | 210 ++++++++++++++ .../studio_details/studio_details_state.dart | 35 ++- .../widgets/studio_details.dart | 97 ------- .../widgets/studio_details_widget.dart | 272 ++++++++++++++++++ lib/views/home/studio/studio_state.dart | 57 +++- lib/views/home/studio/widgets/slider.dart | 80 +++++- .../widgets/audio/audio_player_widget.dart | 39 ++- lib/views/home/widgets/bnb.dart | 89 +++--- .../home/widgets/floating_navigation_bar.dart | 2 +- .../home/widgets/overview/multitype.dart | 69 ++++- lib/views/home/widgets/overview/podcast.dart | 3 + lib/views/home/widgets/overview/radar.dart | 2 +- lib/views/home/widgets/overview/video.dart | 7 +- lib/views/widgets/didvan/scaffold.dart | 14 +- 27 files changed, 1015 insertions(+), 326 deletions(-) create mode 100644 lib/models/slider_data.dart rename lib/views/home/studio/studio_details/{studio_details.dart => studio_details.mobile.dart} (61%) create mode 100644 lib/views/home/studio/studio_details/studio_details.web.dart delete mode 100644 lib/views/home/studio/studio_details/widgets/studio_details.dart create mode 100644 lib/views/home/studio/studio_details/widgets/studio_details_widget.dart diff --git a/lib/main.dart b/lib/main.dart index 3ed3728..d663c2d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/providers/theme_provider.dart'; import 'package:didvan/providers/user_provider.dart'; import 'package:didvan/routes/route_generator.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; @@ -26,6 +27,9 @@ class Didvan extends StatelessWidget { ChangeNotifierProvider( create: (context) => ThemeProvider(), ), + ChangeNotifierProvider( + create: (context) => StudioDetailsState(), + ), ], child: Consumer( builder: (context, themeProvider, child) => MaterialApp( diff --git a/lib/models/overview_data.dart b/lib/models/overview_data.dart index b72995b..6a20c71 100644 --- a/lib/models/overview_data.dart +++ b/lib/models/overview_data.dart @@ -45,7 +45,7 @@ class OverviewData { createdAt: json['createdAt'], duration: json['duration'], type: json['type'] ?? '', - marked: json['marked'] ?? false, + marked: json['marked'] ?? true, media: json['media'], categories: json['categories'] != null ? List.from( diff --git a/lib/models/slider_data.dart b/lib/models/slider_data.dart new file mode 100644 index 0000000..a99c52d --- /dev/null +++ b/lib/models/slider_data.dart @@ -0,0 +1,27 @@ +class SliderData { + final int id; + final String title; + final String image; + final String media; + + const SliderData({ + required this.id, + required this.title, + required this.image, + required this.media, + }); + + factory SliderData.fromJson(Map json) => SliderData( + id: json['id'], + title: json['title'], + image: json['image'], + media: json['media'], + ); + + Map toJson() => { + 'id': id, + 'title': title, + 'image': image, + 'media': media, + }; +} diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index 19c5192..570bb06 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -132,7 +132,7 @@ class UserProvider extends CoreProvier { final MapEntry? lastChange = _radarMarkQueue.lastWhereOrNull((item) => item.key == id); if (lastChange == null) return; - final service = RequestService(RequestHelper.markRadar(id)); + final service = RequestService(RequestHelper.mark(id, 'radar')); if (lastChange.value) { await service.post(); } else { @@ -148,7 +148,7 @@ class UserProvider extends CoreProvier { final MapEntry? lastChange = _studioMarkQueue.lastWhereOrNull((item) => item.key == id); if (lastChange == null) return; - final service = RequestService(RequestHelper.markStudio(id)); + final service = RequestService(RequestHelper.mark(id, 'studio')); if (lastChange.value) { await service.post(); } else { @@ -164,7 +164,7 @@ class UserProvider extends CoreProvier { final MapEntry? lastChange = _newsMarkQueue.lastWhereOrNull((item) => item.key == id); if (lastChange == null) return; - final service = RequestService(RequestHelper.markNews(id)); + final service = RequestService(RequestHelper.mark(id, 'news')); if (lastChange.value) { await service.post(); } else { diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 7514e7c..a4f2222 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -25,8 +25,9 @@ import 'package:didvan/views/home/settings/direct_list/direct_list_state.dart'; import 'package:didvan/views/home/settings/general_settings/settings.dart'; import 'package:didvan/views/home/settings/general_settings/settings_state.dart'; import 'package:didvan/views/home/settings/profile/profile.dart'; -import 'package:didvan/views/home/studio/studio_details/studio_details.dart'; -import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details.mobile.dart' + if (dart.library.io) 'package:didvan/views/home/studio/studio_details/studio_details.mobile.dart' + if (dart.library.html) 'package:didvan/views/home/studio/studio_details/studio_details.web.dart'; import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/routes/routes.dart'; @@ -64,9 +65,6 @@ class RouteGenerator { ChangeNotifierProvider( create: (context) => StudioState(), ), - ChangeNotifierProvider( - create: (context) => StudioDetailsState(), - ), ], child: const Home(), ), @@ -106,11 +104,8 @@ class RouteGenerator { ); case Routes.studioDetails: return _createRoute( - ChangeNotifierProvider.value( - value: (settings.arguments as Map)['state'], - child: StudioDetails( - pageData: settings.arguments as Map, - ), + StudioDetails( + pageData: settings.arguments as Map, ), ); case Routes.directList: @@ -150,14 +145,15 @@ class RouteGenerator { child: Hashtag(tag: settings.arguments as Tag), ), ); - case Routes.filteredBookmarks: return _createRoute( ChangeNotifierProvider( create: (context) => FilteredBookmarksState( - settings.arguments as String, + (settings.arguments as Map)['type'], ), - child: const FilteredBookmarks(), + child: FilteredBookmarks( + onDeleted: + (settings.arguments as Map)['onDeleted']), ), ); default: diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index e35f070..84e2a0f 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -54,11 +54,6 @@ class RequestHelper { MapEntry('tags', _urlListConcatGenerator(ids)), ]); - static String markRadar(int id) => _baseRadarUrl + '/$id/mark'; - static String radarComments(int id) => _baseRadarUrl + '/$id/comments'; - static String addRadarComment(int id) => _baseRadarUrl + '/$id/comments/add'; - static String feedbackRadarComment(int radarId, int id) => - _baseRadarUrl + '/$radarId/comments/$id/feedback'; static String radarDetails(int id, RadarRequestArgs args) => _baseRadarUrl + '/$id' + @@ -79,11 +74,6 @@ class RequestHelper { MapEntry('categories', _urlListConcatGenerator(args.categories)), ]); - static String markNews(int id) => _baseNewsUrl + '/$id/mark'; - static String newsComments(int id) => _baseNewsUrl + '/$id/comments'; - static String addNewsComment(int id) => _baseNewsUrl + '/$id/comments/add'; - static String feedbackNewsComment(int radarId, int id) => - _baseNewsUrl + '/$radarId/comments/$id/feedback'; static String newsDetails(int id, NewsRequestArgs args) => _baseNewsUrl + '/$id' + @@ -102,12 +92,10 @@ class RequestHelper { MapEntry('search', args.search), ]); - static String markStudio(int id) => _baseStudioUrl + '/$id/mark'; - static String studioComments(int id) => _baseStudioUrl + '/$id/comments'; - static String addStudioComment(int id) => - _baseStudioUrl + '/$id/comments/add'; - static String feedbackStudioComment(int studioId, int id) => - _baseStudioUrl + '/$studioId/comments/$id/feedback'; + static String sudioSlider(String type) => + _baseStudioUrl + + '/slider' + + _urlConcatGenerator([MapEntry('type', type)]); static String studioDetails(int id, StudioRequestArgs args) => _baseStudioUrl + '/$id' + @@ -126,6 +114,14 @@ class RequestHelper { MapEntry('search', args.search), ]); + static String mark(int id, String type) => baseUrl + '/$type/$id/mark'; + static String comments(int id, String type) => + baseUrl + '/$type/$id/comments'; + static String feedback(int id, int commentId, String type) => + baseUrl + '/$type/$id/comments/$commentId/feedback'; + static String addComment(int id, String type) => + baseUrl + '/$type/$id/comments/add'; + static String _urlConcatGenerator(List> additions) { String result = ''; additions.removeWhere( diff --git a/lib/views/home/comments/comments.dart b/lib/views/home/comments/comments.dart index 0bf0c72..baf7d3e 100644 --- a/lib/views/home/comments/comments.dart +++ b/lib/views/home/comments/comments.dart @@ -1,6 +1,7 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/views/home/comments/comments_state.dart'; import 'package:didvan/views/home/comments/widgets/comment_item.dart'; @@ -9,6 +10,7 @@ import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -32,7 +34,7 @@ class _CommentsState extends State { void initState() { final state = context.read(); state.itemId = widget.pageData['id']; - state.isRadar = widget.pageData['isRadar']; + state.type = widget.pageData['type']; state.onCommentsChanged = widget.pageData['onCommentsChanged']; Future.delayed( Duration.zero, @@ -41,6 +43,8 @@ class _CommentsState extends State { super.initState(); } + bool get _isPage => widget.pageData['isPage'] != false; + @override Widget build(BuildContext context) { final bottomViewInset = MediaQuery.of(context).viewInsets.bottom; @@ -56,11 +60,13 @@ class _CommentsState extends State { children: [ DidvanScaffold( backgroundColor: Theme.of(context).colorScheme.surface, - appBarData: AppBarData( - hasBack: true, - title: 'نظرات', - subtitle: widget.pageData['title'], - ), + appBarData: _isPage + ? AppBarData( + hasBack: true, + title: 'نظرات', + subtitle: widget.pageData['title'], + ) + : null, padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92), slivers: [ Consumer( @@ -71,6 +77,11 @@ class _CommentsState extends State { itemPadding: const EdgeInsets.symmetric(vertical: 16), childCount: state.comments.length, placeholder: const _CommentPlaceholder(), + enableEmptyState: state.comments.isEmpty, + emptyState: EmptyState( + asset: Assets.emptyChat, + title: 'اولین نظر را بنویسید...', + ), builder: (context, state, index) => Comment( focusNode: _focusNode, comment: state.comments[index], diff --git a/lib/views/home/comments/comments_state.dart b/lib/views/home/comments/comments_state.dart index ead8409..cb52186 100644 --- a/lib/views/home/comments/comments_state.dart +++ b/lib/views/home/comments/comments_state.dart @@ -17,19 +17,17 @@ class CommentsState extends CoreProvier { bool showReplyBox = false; late void Function(int count) onCommentsChanged; int _count = 0; + late String type; final List comments = []; final Map> _feedbackQueue = {}; - bool isRadar = true; int itemId = 0; Future getComments() async { appState = AppState.busy; final service = RequestService( - isRadar - ? RequestHelper.radarComments(itemId) - : RequestHelper.newsComments(itemId), + RequestHelper.comments(itemId, type), ); await service.httpGet(); if (service.isSuccess) { @@ -52,13 +50,12 @@ class CommentsState extends CoreProvier { Future.delayed(const Duration(milliseconds: 500), () async { if (!_feedbackQueue.containsKey(id)) return; final service = RequestService( - isRadar - ? RequestHelper.feedbackRadarComment(itemId, id) - : RequestHelper.feedbackNewsComment(itemId, id), - body: { - 'like': _feedbackQueue[id]!.key, - 'dislike': _feedbackQueue[id]!.value, - }); + RequestHelper.feedback(itemId, id, type), + body: { + 'like': _feedbackQueue[id]!.key, + 'dislike': _feedbackQueue[id]!.value, + }, + ); await service.put(); _feedbackQueue.remove(id); }); @@ -119,10 +116,9 @@ class CommentsState extends CoreProvier { update(); body.addAll({'text': text}); final service = RequestService( - isRadar - ? RequestHelper.addRadarComment(itemId) - : RequestHelper.addNewsComment(itemId), - body: body); + RequestHelper.addComment(itemId, type), + body: body, + ); await service.post(); if (service.isSuccess) { diff --git a/lib/views/home/settings/bookmarks/bookmarks.dart b/lib/views/home/settings/bookmarks/bookmarks.dart index 520d880..81ddeab 100644 --- a/lib/views/home/settings/bookmarks/bookmarks.dart +++ b/lib/views/home/settings/bookmarks/bookmarks.dart @@ -131,7 +131,15 @@ class _BookmarksState extends State { void _onCategorySelected(String type) { FocusScope.of(context).unfocus(); - Navigator.of(context).pushNamed(Routes.filteredBookmarks, arguments: type); + Navigator.of(context).pushNamed(Routes.filteredBookmarks, arguments: { + 'type': type, + 'onDeleted': (int id) { + final state = context.read(); + state.bookmarks + .removeWhere((element) => element.id == id && element.type == type); + state.update(); + }, + }); } void _onChanged(String value) { diff --git a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart index 2291e1a..bc553e4 100644 --- a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart +++ b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart @@ -1,7 +1,10 @@ +import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart'; import 'package:didvan/views/home/widgets/overview/news.dart'; +import 'package:didvan/views/home/widgets/overview/podcast.dart'; import 'package:didvan/views/home/widgets/overview/radar.dart'; +import 'package:didvan/views/home/widgets/overview/video.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/state_handlers/empty_list.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; @@ -9,7 +12,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class FilteredBookmarks extends StatefulWidget { - const FilteredBookmarks({Key? key}) : super(key: key); + final void Function(int id)? onDeleted; + const FilteredBookmarks({Key? key, this.onDeleted}) : super(key: key); @override _FilteredBookmarksState createState() => _FilteredBookmarksState(); @@ -67,8 +71,26 @@ class _FilteredBookmarksState extends State { hasUnmarkConfirmation: true, ); } - return NewsOverview( - news: state.bookmarks[index], + if (state.type == 'news') { + return NewsOverview( + news: state.bookmarks[index], + onMarkChanged: _onBookmarkChanged, + hasUnmarkConfirmation: true, + ); + } + if (state.type == 'podcast') { + return PodcastOverview( + studioRequestArgs: + const StudioRequestArgs(page: 0, type: 'podcast'), + podcast: state.bookmarks[index], + onMarkChanged: _onBookmarkChanged, + hasUnmarkConfirmation: true, + ); + } + return VideoOverview( + studioRequestArgs: + const StudioRequestArgs(page: 0, type: 'video'), + video: state.bookmarks[index], onMarkChanged: _onBookmarkChanged, hasUnmarkConfirmation: true, ); @@ -85,5 +107,6 @@ class _FilteredBookmarksState extends State { if (value) return; final state = context.read(); state.onMarkChanged(id, false); + widget.onDeleted?.call(id); } } diff --git a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart index 7aa0463..9cfc4d1 100644 --- a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart +++ b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart @@ -56,14 +56,12 @@ class FilteredBookmarksState extends CoreProvier { } void onMarkChanged(int id, bool value) { - switch (type) { - case 'radar': - UserProvider.changeRadarMark(id, value); - break; - case 'news': - UserProvider.changeNewsMark(id, value); - break; - default: + if (type == 'radar') { + UserProvider.changeRadarMark(id, value); + } else if (type == 'news') { + UserProvider.changeNewsMark(id, value); + } else { + UserProvider.changeStudioMark(id, value); } bookmarks.removeWhere((element) => element.id == id); notifyListeners(); diff --git a/lib/views/home/studio/studio.dart b/lib/views/home/studio/studio.dart index 1ffca83..79744c4 100644 --- a/lib/views/home/studio/studio.dart +++ b/lib/views/home/studio/studio.dart @@ -1,6 +1,10 @@ +import 'dart:async'; + +import 'package:didvan/config/design_config.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/routes/routes.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/home/studio/widgets/slider.dart'; @@ -9,6 +13,7 @@ import 'package:didvan/views/home/widgets/logo_app_bar.dart'; import 'package:didvan/views/home/widgets/overview/podcast.dart'; import 'package:didvan/views/home/widgets/overview/video.dart'; import 'package:didvan/views/home/widgets/search_field.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/radial_button.dart'; @@ -26,6 +31,7 @@ class Studio extends StatefulWidget { class _StudioState extends State { final _focusNode = FocusNode(); + Timer? _timer; @override void initState() { @@ -46,7 +52,10 @@ class _StudioState extends State { EdgeInsets.only(top: MediaQuery.of(context).padding.top), child: DidvanIconButton( icon: DidvanIcons.bookmark_regular, - onPressed: () {}, + onPressed: () => Navigator.of(context).pushNamed( + Routes.filteredBookmarks, + arguments: context.read().type, + ), ), ), ], @@ -59,14 +68,20 @@ class _StudioState extends State { child: Padding( padding: const EdgeInsets.all(16.0), child: SearchField( - title: 'جستجو در استودیو', - onChanged: (value) {}, + title: 'استودیو', + onChanged: _onChanged, focusNode: _focusNode, ), ), ), - const SliverToBoxAdapter( - child: StudioSlider(), + SliverToBoxAdapter( + child: Consumer( + builder: (context, state, child) => AnimatedVisibility( + isVisible: !state.searching, + duration: DesignConfig.lowAnimationDuration, + child: const StudioSlider(), + ), + ), ), const SliverPadding( padding: EdgeInsets.symmetric(horizontal: 16), @@ -82,7 +97,13 @@ class _StudioState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const ItemTitle(title: 'تازه‌ترین‌ها'), + Consumer( + builder: (context, state, child) => AnimatedVisibility( + isVisible: !state.searching, + duration: DesignConfig.lowAnimationDuration, + child: ItemTitle(title: state.orderString), + ), + ), DidvanIconButton( gestureSize: 36, icon: DidvanIcons.sort_regular, @@ -108,7 +129,6 @@ class _StudioState extends State { onMarkChanged: state.changeMark, hasUnmarkConfirmation: false, video: state.studios[index], - onCommentsChanged: state.onCommentsChanged, studioRequestArgs: StudioRequestArgs( page: state.page, order: state.order, @@ -127,13 +147,25 @@ class _StudioState extends State { ), ), childCount: state.studios.length, - onRetry: () => state.getStudioOverviews(page: 1), + onRetry: () => state.getStudios(page: 1), ), ), ], ); } + void _onChanged(String value) { + final state = context.read(); + if (value.length < 4 && value.isNotEmpty || state.lastSearch == value) { + return; + } + _timer?.cancel(); + _timer = Timer(const Duration(seconds: 1), () { + state.search = value; + state.getStudios(page: 1); + }); + } + void _showSortDialog() { final state = context.read(); ActionSheetUtils.showBottomSheet( @@ -142,7 +174,7 @@ class _StudioState extends State { builder: (context, setState) => Column( children: [ DidvanRadialButton( - title: 'جدیدترین‌ها', + title: 'تازه‌ترین‌ها', onSelected: () => setState( () => state.selectedSortTypeIndex = 0, ), @@ -171,7 +203,7 @@ class _StudioState extends State { titleIcon: DidvanIcons.sort_regular, hasDismissButton: false, confrimTitle: 'مرتب سازی', - onConfirmed: () => state.getStudioOverviews(page: 1), + onConfirmed: () => state.getStudios(page: 1), ), ); } diff --git a/lib/views/home/studio/studio_details/studio_details.dart b/lib/views/home/studio/studio_details/studio_details.mobile.dart similarity index 61% rename from lib/views/home/studio/studio_details/studio_details.dart rename to lib/views/home/studio/studio_details/studio_details.mobile.dart index 20ce528..4756477 100644 --- a/lib/views/home/studio/studio_details/studio_details.dart +++ b/lib/views/home/studio/studio_details/studio_details.mobile.dart @@ -1,13 +1,17 @@ import 'dart:io'; import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; +import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_widget.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; + import 'package:webview_flutter/webview_flutter.dart'; class StudioDetails extends StatefulWidget { @@ -20,6 +24,7 @@ class StudioDetails extends StatefulWidget { } class _StudioDetailsState extends State { + final _scrollController = ScrollController(); bool _isFullScreen = false; bool _isInit = true; @@ -29,14 +34,12 @@ class _StudioDetailsState extends State { @override void initState() { final state = context.read(); - Future.delayed( Duration.zero, () => state.getStudioDetails(widget.pageData['id']), ); - state.args = widget.pageData['args']; - if (Platform.isAndroid) WebView.platform = AndroidWebView(); + if (!kIsWeb && Platform.isAndroid) WebView.platform = AndroidWebView(); super.initState(); } @@ -77,39 +80,44 @@ class _StudioDetailsState extends State { _scaleInPortrait = _dwInPortrait / 576; _isInit = false; } + return Consumer( builder: (context, state, child) => StateHandler( state: state, onRetry: () => state.getStudioDetails(state.currentStudio.id), - builder: (context, state) => state.studios.isEmpty - ? const SizedBox() - : WillPopScope( - onWillPop: () async { - if (_isFullScreen) { - await _changeFullSceen(false); - return false; - } - return true; - }, - child: DidvanScaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - padding: EdgeInsets.zero, - appBarData: _isFullScreen - ? null - : AppBarData( - isSmall: true, - title: state.currentStudio.title, - ), - children: [ - SizedBox( - width: ds.width, - height: _isFullScreen ? ds.height : ds.width * 9 / 16, - child: Stack( - children: [ - WebView( - allowsInlineMediaPlayback: true, - initialUrl: Uri.dataFromString( - ''' + builder: (context, state) { + if (state.studios.isEmpty) { + return const SizedBox(); + } + return WillPopScope( + onWillPop: () async { + if (_isFullScreen) { + await _changeFullSceen(false); + return false; + } + return true; + }, + child: DidvanScaffold( + scrollController: _scrollController, + backgroundColor: Theme.of(context).colorScheme.surface, + padding: EdgeInsets.zero, + appBarData: _isFullScreen + ? null + : AppBarData( + isSmall: true, + title: state.currentStudio.title, + ), + children: [ + SizedBox( + width: ds.width, + height: _isFullScreen ? ds.height : ds.width * 9 / 16, + child: Stack( + children: [ + WebView( + backgroundColor: Theme.of(context).colorScheme.black, + allowsInlineMediaPlayback: true, + initialUrl: Uri.dataFromString( + ''' { ''', - mimeType: 'text/html', - ).toString(), - javascriptMode: JavascriptMode.unrestricted, - ), - Positioned( - right: 42, - bottom: 8, - child: GestureDetector( - onTap: () => _changeFullSceen(!_isFullScreen), - child: Container( - color: Colors.transparent, - width: 24, - height: 30, - ), - ), - ), - ], + mimeType: 'text/html', + ).toString(), + javascriptMode: JavascriptMode.unrestricted, ), - ), - ], + Positioned( + right: 42, + bottom: 8, + child: GestureDetector( + onTap: () => _changeFullSceen(!_isFullScreen), + child: Container( + color: Colors.transparent, + width: 24, + height: 30, + ), + ), + ), + ], + ), ), - ), + const SizedBox(height: 20), + StudioDetailsWidget( + onCommentsTabSelected: () => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ), + studio: state.currentStudio, + ), + ], + ), + ); + }, ), ); } diff --git a/lib/views/home/studio/studio_details/studio_details.web.dart b/lib/views/home/studio/studio_details/studio_details.web.dart new file mode 100644 index 0000000..c186998 --- /dev/null +++ b/lib/views/home/studio/studio_details/studio_details.web.dart @@ -0,0 +1,210 @@ +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; +import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_widget.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:webview_flutter/webview_flutter.dart'; + +class StudioDetails extends StatefulWidget { + final Map pageData; + + const StudioDetails({Key? key, required this.pageData}) : super(key: key); + + @override + State createState() => _StudioDetailsState(); +} + +class _StudioDetailsState extends State { + final _scrollController = ScrollController(); + + bool _isFullScreen = false; + bool _isInit = true; + + double _dwInPortrait = 0; + double _scaleInPortrait = 1; + + @override + void initState() { + final state = context.read(); + Future.delayed( + Duration.zero, + () => state.getStudioDetails(widget.pageData['id']), + ); + state.args = widget.pageData['args']; + if (!kIsWeb && Platform.isAndroid) WebView.platform = AndroidWebView(); + super.initState(); + } + + Future _changeFullSceen(bool value) async { + if (value) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [], + ); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + systemNavigationBarColor: Colors.black, + ), + ); + await SystemChrome.setPreferredOrientations( + [DeviceOrientation.landscapeLeft], + ); + } else { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top], + ); + await SystemChrome.setPreferredOrientations( + [DeviceOrientation.portraitUp], + ); + DesignConfig.updateSystemUiOverlayStyle(); + } + setState(() { + _isFullScreen = value; + }); + } + + @override + Widget build(BuildContext context) { + final ds = MediaQuery.of(context).size; + if (_isInit) { + _dwInPortrait = MediaQuery.of(context).size.width; + _scaleInPortrait = _dwInPortrait / 576; + _isInit = false; + } + + return Consumer( + builder: (context, state, child) => StateHandler( + state: state, + onRetry: () => state.getStudioDetails(state.currentStudio.id), + builder: (context, state) { + if (state.studios.isEmpty) { + return const SizedBox(); + } + if (kIsWeb) { + // ignore: undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory( + "video", + (int viewId) => html.IFrameElement() + ..allowFullscreen = true + ..src = Uri.dataFromString( + '' + + state.currentStudio.media, + mimeType: 'text/html', + ).toString() + ..style.border = 'none', + ); + } + return WillPopScope( + onWillPop: () async { + if (_isFullScreen) { + await _changeFullSceen(false); + return false; + } + return true; + }, + child: DidvanScaffold( + scrollController: _scrollController, + padding: EdgeInsets.zero, + appBarData: _isFullScreen + ? null + : AppBarData( + isSmall: true, + title: state.currentStudio.title, + ), + children: [ + if (kIsWeb) + const AspectRatio( + aspectRatio: 16 / 9, + child: HtmlElementView(viewType: 'video'), + ), + if (!kIsWeb) + SizedBox( + width: ds.width, + height: _isFullScreen ? ds.height : ds.width * 9 / 16, + child: Stack( + children: [ + WebView( + backgroundColor: Theme.of(context).colorScheme.black, + allowsInlineMediaPlayback: true, + initialUrl: Uri.dataFromString( + ''' + + + + + + + ${state.currentStudio.media} + + + ''', + mimeType: 'text/html', + ).toString(), + javascriptMode: JavascriptMode.unrestricted, + ), + if (!kIsWeb) + Positioned( + right: 42, + bottom: 8, + child: GestureDetector( + onTap: () => _changeFullSceen(!_isFullScreen), + child: Container( + color: Colors.transparent, + width: 24, + height: 30, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + StudioDetailsWidget( + onCommentsTabSelected: () => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ), + studio: state.currentStudio, + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/views/home/studio/studio_details/studio_details_state.dart b/lib/views/home/studio/studio_details/studio_details_state.dart index 0a6205b..7ef097c 100644 --- a/lib/views/home/studio/studio_details/studio_details_state.dart +++ b/lib/views/home/studio/studio_details/studio_details_state.dart @@ -17,6 +17,7 @@ class StudioDetailsState extends CoreProvier { int _selectedDetailsIndex = 0; bool isFetchingNewItem = false; final List relatedQueue = []; + bool currentTypeIsVideo = true; int _currentIndex = 0; int get currentIndex => _currentIndex; @@ -24,6 +25,9 @@ class StudioDetailsState extends CoreProvier { int get selectedDetailsIndex => _selectedDetailsIndex; set selectedDetailsIndex(int value) { _selectedDetailsIndex = value; + if (value == 2) { + getRelatedContents(); + } notifyListeners(); } @@ -35,12 +39,16 @@ class StudioDetailsState extends CoreProvier { } } - Future getStudioDetails(int id, - {bool? isForward, StudioRequestArgs? args}) async { + Future getStudioDetails( + int id, { + bool? isForward, + StudioRequestArgs? args, + }) async { if (args != null) { this.args = args; } if (isForward == null) { + _selectedDetailsIndex = 0; appState = AppState.busy; } else { isFetchingNewItem = true; @@ -49,22 +57,16 @@ class StudioDetailsState extends CoreProvier { final service = RequestService(RequestHelper.studioDetails(id, this.args)); await service.httpGet(); if (service.isSuccess) { + studios.clear(); final result = service.result; final studio = StudioDetailsData.fromJson(result['studio']); + await _handlePodcastPlayback(studio); if (this.args.page == 0) { studios.add(studio); initialIndex = 0; appState = AppState.idle; return; } - if (this.args.type == 'podcast') { - MediaService.currentPodcast = studio; - MediaService.podcastPlaylistArgs = args; - await MediaService.handleAudioPlayback( - audioSource: studio.media, - isVoiceMessage: false, - ); - } StudioDetailsData? prevStudio; if (result['prevStudio'].isNotEmpty) { @@ -108,13 +110,24 @@ class StudioDetailsState extends CoreProvier { } } + Future _handlePodcastPlayback(StudioDetailsData studio) async { + if (args.type == 'podcast') { + MediaService.currentPodcast = studio; + MediaService.podcastPlaylistArgs = args; + await MediaService.handleAudioPlayback( + audioSource: studio.media, + isVoiceMessage: false, + ); + } + } + Future getRelatedContents() async { if (currentStudio.relatedContents.isNotEmpty) return; relatedQueue.add(currentStudio.id); final service = RequestService(RequestHelper.tag( ids: currentStudio.tags.map((tag) => tag.id).toList(), itemId: currentStudio.id, - type: 'studio', + type: currentStudio.media.contains('iframe') ? 'video' : 'podcast', )); await service.httpGet(); if (service.isSuccess) { diff --git a/lib/views/home/studio/studio_details/widgets/studio_details.dart b/lib/views/home/studio/studio_details/widgets/studio_details.dart deleted file mode 100644 index 059b4cc..0000000 --- a/lib/views/home/studio/studio_details/widgets/studio_details.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; -import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; -import 'package:didvan/views/widgets/didvan/text.dart'; -import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class StudioDetailsWidget extends StatelessWidget { - const StudioDetailsWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, state, child) => StateHandler( - onRetry: () {}, - state: state, - builder: (context, state) => Container( - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _TabItem( - icon: DidvanIcons.description_solid, - title: 'توضیحات', - onTap: () => state.selectedDetailsIndex = 0, - isSelected: state.selectedDetailsIndex == 0, - ), - _TabItem( - icon: DidvanIcons.chats_solid, - title: 'نظرات', - onTap: () => state.selectedDetailsIndex = 1, - isSelected: state.selectedDetailsIndex == 1, - ), - _TabItem( - icon: DidvanIcons.puzzle_solid, - title: 'مطالب مرتبط', - onTap: () => state.selectedDetailsIndex = 2, - isSelected: state.selectedDetailsIndex == 2, - ), - ], - ), - const SizedBox(height: 16), - ], - ), - ), - ), - ); - } -} - -class _TabItem extends StatelessWidget { - final IconData icon; - final String title; - final VoidCallback onTap; - final bool isSelected; - const _TabItem({ - Key? key, - required this.icon, - required this.title, - required this.onTap, - required this.isSelected, - }) : super(key: key); - - Color? _color(context) => - isSelected ? Theme.of(context).colorScheme.focusedBorder : null; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - color: Colors.transparent, - child: Column( - children: [ - Icon( - icon, - color: _color(context), - ), - Container( - width: 64, - height: 1, - color: _color(context), - ), - DidvanText( - title, - color: _color(context), - style: Theme.of(context).textTheme.caption, - ) - ], - ), - ), - ); - } -} diff --git a/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart b/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart new file mode 100644 index 0000000..1844c5d --- /dev/null +++ b/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart @@ -0,0 +1,272 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/studio_details_data.dart'; +import 'package:didvan/views/home/comments/comments.dart'; +import 'package:didvan/views/home/comments/comments_state.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; +import 'package:didvan/views/home/widgets/overview/multitype.dart'; +import 'package:didvan/views/home/widgets/tag_item.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'; + +class StudioDetailsWidget extends StatelessWidget { + final StudioDetailsData studio; + final VoidCallback onCommentsTabSelected; + const StudioDetailsWidget({ + Key? key, + required this.studio, + required this.onCommentsTabSelected, + }) : super(key: key); + + bool get _isVideo => studio.media.contains('ifram'); + + @override + Widget build(BuildContext context) { + final ds = MediaQuery.of(context).size; + return Consumer( + builder: (context, state, child) => Container( + color: Theme.of(context).colorScheme.surface, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(), + _TabItem( + icon: DidvanIcons.description_solid, + title: 'توضیحات', + onTap: () => state.selectedDetailsIndex = 0, + isSelected: state.selectedDetailsIndex == 0, + isVideo: _isVideo, + ), + _TabItem( + icon: DidvanIcons.chats_solid, + title: 'نظرات', + onTap: () { + state.selectedDetailsIndex = 1; + onCommentsTabSelected(); + }, + isSelected: state.selectedDetailsIndex == 1, + isVideo: _isVideo, + ), + _TabItem( + icon: DidvanIcons.puzzle_solid, + title: 'مطالب مرتبط', + onTap: () => state.selectedDetailsIndex = 2, + isSelected: state.selectedDetailsIndex == 2, + isVideo: _isVideo, + ), + const SizedBox(), + ], + ), + const SizedBox(height: 24), + StateHandler( + onRetry: () {}, + state: state, + builder: (context, state) { + if (state.selectedDetailsIndex == 0) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + DidvanText(state.currentStudio.description), + if (studio.tags.isNotEmpty) const SizedBox(height: 20), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var i = 0; i < studio.tags.length; i++) + TagItem(tag: studio.tags[i]), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + if (state.studios.length != state.currentIndex + 1) + _StudioPreview( + isNext: true, + studio: state.studios[state.currentIndex + 1]!, + ), + if (state.currentIndex != 0) + _StudioPreview( + isNext: false, + studio: state.studios[state.currentIndex - 1]!, + ), + const SizedBox(), + ], + ) + ], + ), + ); + } + if (state.selectedDetailsIndex == 1) { + return ChangeNotifierProvider( + create: (context) => CommentsState(), + child: SizedBox( + height: ds.height - 180, + child: Comments( + pageData: { + 'id': studio.id, + 'type': 'studio', + 'title': studio.title, + 'onCommentsChanged': state.onCommentsChanged, + 'isPage': false, + }, + ), + ), + ); + } + return Column( + children: [ + if (studio.relatedContents.isEmpty) + for (var i = 0; i < 3; i++) + Padding( + padding: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + child: MultitypeOverview.placeholder, + ), + for (var i = 0; i < studio.relatedContents.length; i++) + Padding( + padding: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + child: MultitypeOverview( + item: studio.relatedContents[i], + onMarkChanged: (id, value) {}, + ), + ), + ], + ); + }, + ), + ], + ), + ), + ); + } +} + +class _TabItem extends StatelessWidget { + final IconData icon; + final String title; + final VoidCallback onTap; + final bool isSelected; + final bool isVideo; + const _TabItem({ + Key? key, + required this.icon, + required this.title, + required this.onTap, + required this.isSelected, + required this.isVideo, + }) : super(key: key); + + Color? _color(context) { + if (isSelected) { + if (isVideo) { + return Theme.of(context).colorScheme.secondary; + } + return Theme.of(context).colorScheme.focusedBorder; + } + return Theme.of(context).colorScheme.border; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + Icon( + icon, + color: _color(context), + ), + if (isSelected) const SizedBox(height: 8), + if (isSelected) + Container( + width: 64, + height: 1, + color: _color(context), + ), + if (isSelected) + DidvanText( + title, + color: _color(context), + style: Theme.of(context).textTheme.caption, + ) + ], + ), + ), + ); + } +} + +class _StudioPreview extends StatelessWidget { + final bool isNext; + final StudioDetailsData studio; + const _StudioPreview({Key? key, required this.isNext, required this.studio}) + : super(key: key); + + String get _previewTitle { + if (studio.media.contains('iframe')) { + return 'ویدئو ${isNext ? 'بعدی' : 'قبلی'} '; + } + return 'پادکست ${isNext ? 'بعدی' : 'قبلی'} '; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + final state = context.read(); + state.getStudioDetails(studio.id, args: state.args); + }, + child: Container( + width: 88, + color: Colors.transparent, + child: Column( + children: [ + SkeletonImage( + imageUrl: studio.image, + aspectRatio: 1 / 1, + ), + const SizedBox(height: 8), + Icon( + isNext + ? DidvanIcons.angle_right_regular + : DidvanIcons.angle_left_regular, + ), + const SizedBox(height: 8), + DidvanText( + _previewTitle, + style: Theme.of(context).textTheme.caption, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + DidvanText( + studio.title, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/home/studio/studio_state.dart b/lib/views/home/studio/studio_state.dart index 18b7dc2..115534a 100644 --- a/lib/views/home/studio/studio_state.dart +++ b/lib/views/home/studio/studio_state.dart @@ -1,6 +1,7 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/studio.dart'; +import 'package:didvan/models/slider_data.dart'; import 'package:didvan/providers/core_provider.dart'; import 'package:didvan/providers/user_provider.dart'; import 'package:didvan/services/network/request.dart'; @@ -8,9 +9,10 @@ import 'package:didvan/services/network/request_helper.dart'; class StudioState extends CoreProvier { final List studios = []; + final List sliders = []; - String? search; - String? lastSearch; + String search = ''; + String lastSearch = ''; int page = 1; int lastPage = 1; @@ -20,21 +22,13 @@ class StudioState extends CoreProvier { bool get videosSelected => _videosSelected; + bool get searching => search.isNotEmpty; + set videosSelected(bool value) { if (_videosSelected == value) return; _videosSelected = value; - studios.clear(); - getStudioOverviews(page: page); - } - - void init() { - search = ''; - lastSearch = ''; - _videosSelected = true; - selectedSortTypeIndex = 0; - Future.delayed(Duration.zero, () { - getStudioOverviews(page: 1); - }); + _getSliders(); + getStudios(page: page); } String get order { @@ -43,13 +37,46 @@ class StudioState extends CoreProvier { return 'comment'; } + String get orderString { + if (selectedSortTypeIndex == 0) return 'تازه‌ترین‌ها'; + if (selectedSortTypeIndex == 1) return 'پربازدیدترین‌ها'; + return 'پربحث‌نرین‌ها'; + } + String get type { if (videosSelected) return 'video'; return 'podcast'; } - Future getStudioOverviews({required int page}) async { + void init() { + search = ''; + lastSearch = ''; + _videosSelected = true; + selectedSortTypeIndex = 0; + Future.delayed(Duration.zero, () { + _getSliders(); + getStudios(page: 1); + }); + } + + Future _getSliders() async { + final service = RequestService( + RequestHelper.sudioSlider(type), + ); + await service.httpGet(); + if (service.isSuccess) { + sliders.clear(); + final sliderItems = service.result['studios']; + for (var i = 0; i < sliderItems.length; i++) { + sliders.add(SliderData.fromJson(sliderItems[i])); + } + } + notifyListeners(); + } + + Future getStudios({required int page}) async { this.page = page; + lastSearch = search; if (page == 1) { appState = AppState.busy; } diff --git a/lib/views/home/studio/widgets/slider.dart b/lib/views/home/studio/widgets/slider.dart index d312b2b..39e5bc4 100644 --- a/lib/views/home/studio/widgets/slider.dart +++ b/lib/views/home/studio/widgets/slider.dart @@ -1,28 +1,92 @@ import 'package:carousel_slider/carousel_slider.dart'; +import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; -class StudioSlider extends StatelessWidget { +class StudioSlider extends StatefulWidget { const StudioSlider({Key? key}) : super(key: key); + @override + State createState() => _StudioSliderState(); +} + +class _StudioSliderState extends State { + int selectedIndex = 0; + @override Widget build(BuildContext context) { + final state = context.watch(); return Column( children: [ CarouselSlider( items: [ - Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), - Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), - Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), - Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), + for (var i = 0; i < state.sliders.length; i++) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: state.appState == AppState.busy + ? const ShimmerPlaceholder() + : Stack( + children: [ + SkeletonImage( + borderRadius: DesignConfig.mediumBorderRadius, + imageUrl: state.sliders[i].image, + width: double.infinity, + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + decoration: BoxDecoration( + color: (state.videosSelected + ? Theme.of(context) + .colorScheme + .secondaryDisabled + : Theme.of(context).colorScheme.focused) + .withOpacity(0.9), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(10), + ), + ), + child: DidvanText( + state.sliders[i].title, + color: Theme.of(context).colorScheme.title, + style: Theme.of(context).textTheme.caption, + ), + ), + ), + ], + ), + ), ], options: CarouselOptions( + onPageChanged: (index, reason) => setState( + () => selectedIndex = index, + ), viewportFraction: 0.94, aspectRatio: 16 / 9, autoPlay: true, ), ), - Row(), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (var i = 0; i < state.sliders.length; i++) + _SliderIndicator(isCurrentIndex: selectedIndex == i), + ], + ), + const SizedBox(height: 16), ], ); } @@ -35,9 +99,11 @@ class _SliderIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return AnimatedContainer( + duration: DesignConfig.lowAnimationDuration, height: 8, width: 8, + margin: const EdgeInsets.only(left: 4), decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.focusedBorder, diff --git a/lib/views/home/widgets/audio/audio_player_widget.dart b/lib/views/home/widgets/audio/audio_player_widget.dart index d369b10..dbdc64f 100644 --- a/lib/views/home/widgets/audio/audio_player_widget.dart +++ b/lib/views/home/widgets/audio/audio_player_widget.dart @@ -1,8 +1,11 @@ +import 'dart:math'; + 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/studio_details_data.dart'; import 'package:didvan/services/media/media.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/home/widgets/audio/audio_slider.dart'; import 'package:didvan/views/home/widgets/bookmark_button.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart'; @@ -10,6 +13,7 @@ import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/ink_wrapper.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class AudioPlayerWidget extends StatelessWidget { final StudioDetailsData podcast; @@ -77,8 +81,13 @@ class AudioPlayerWidget extends StatelessWidget { const DidvanText('30', isEnglishFont: true), ], ), - _PlayPouseAnimatedIcon( - audioSource: podcast.media, + StreamBuilder( + stream: MediaService.audioPlayer.playingStream, + builder: (context, snapshot) { + return _PlayPouseAnimatedIcon( + audioSource: podcast.media, + ); + }, ), Column( children: [ @@ -88,8 +97,10 @@ class AudioPlayerWidget extends StatelessWidget { onPressed: () { MediaService.audioPlayer.seek( Duration( - seconds: - MediaService.audioPlayer.position.inSeconds - 10, + seconds: max( + 0, + MediaService.audioPlayer.position.inSeconds - 10, + ), ), ); }, @@ -100,7 +111,8 @@ class AudioPlayerWidget extends StatelessWidget { BookmarkButton( gestureSize: 48, value: podcast.marked, - onMarkChanged: (value) {}, + onMarkChanged: (value) => + context.read().changeMark(podcast.id, value), ), ], ), @@ -123,6 +135,12 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> with SingleTickerProviderStateMixin { late final AnimationController _animationController; + @override + void didUpdateWidget(covariant _PlayPouseAnimatedIcon oldWidget) { + _handleAnimation(); + super.didUpdateWidget(oldWidget); + } + @override void initState() { super.initState(); @@ -130,8 +148,13 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> vsync: this, duration: DesignConfig.lowAnimationDuration, ); + } + + void _handleAnimation() { if (MediaService.audioPlayer.playing) { _animationController.forward(); + } else { + _animationController.reverse(); } } @@ -144,11 +167,7 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> audioSource: widget.audioSource, isVoiceMessage: false, ); - if (MediaService.audioPlayer.playing) { - _animationController.forward(); - } else { - _animationController.reverse(); - } + _handleAnimation(); }, child: Container( padding: const EdgeInsets.all(8), diff --git a/lib/views/home/widgets/bnb.dart b/lib/views/home/widgets/bnb.dart index a7651be..2a9d722 100644 --- a/lib/views/home/widgets/bnb.dart +++ b/lib/views/home/widgets/bnb.dart @@ -1,9 +1,11 @@ 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/services/media/media.dart'; import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; -import 'package:didvan/views/home/studio/studio_details/widgets/studio_details.dart'; +import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_widget.dart'; +import 'package:didvan/views/home/studio/studio_state.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/widgets/didvan/icon_button.dart'; @@ -179,46 +181,65 @@ class DidvanBNB extends StatelessWidget { final sheetKey = GlobalKey(); bool isExpanded = false; final detailsState = context.read(); + final state = context.read(); showModalBottomSheet( backgroundColor: Colors.transparent, context: context, isScrollControlled: true, - builder: (context) => ChangeNotifierProvider.value( - value: detailsState, - child: ExpandableBottomSheet( - key: sheetKey, - background: const SizedBox(), - persistentHeader: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AudioPlayerWidget( - podcast: MediaService.currentPodcast!, - ), - Container( - width: MediaQuery.of(context).size.width, + builder: (context) => ChangeNotifierProvider.value( + value: state, + child: Consumer( + builder: (context, state, 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, - child: Column( - children: [ - DidvanIconButton( - size: 32, - icon: DidvanIcons.angle_down_regular, - onPressed: () { - if (!isExpanded) { - sheetKey.currentState?.expand(); - isExpanded = true; - return; - } - isExpanded = false; - sheetKey.currentState?.contract(); - }, - ), - const SizedBox(height: 16), - ], - ), ), - ], + ), + persistentHeader: 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_down_regular, + onPressed: () { + if (!isExpanded) { + sheetKey.currentState?.expand(); + isExpanded = true; + return; + } + isExpanded = false; + sheetKey.currentState?.contract(); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ], + ), + expandableContent: state.appState == AppState.busy + ? const SizedBox() + : StudioDetailsWidget( + studio: detailsState.currentStudio, + onCommentsTabSelected: () { + Future.delayed( + const Duration(milliseconds: 100), + sheetKey.currentState?.expand, + ); + }, + ), ), - expandableContent: const StudioDetailsWidget(), ), ), ); diff --git a/lib/views/home/widgets/floating_navigation_bar.dart b/lib/views/home/widgets/floating_navigation_bar.dart index 28a88e4..dd453a9 100644 --- a/lib/views/home/widgets/floating_navigation_bar.dart +++ b/lib/views/home/widgets/floating_navigation_bar.dart @@ -130,7 +130,7 @@ class _FloatingNavigationBarState extends State { Routes.comments, arguments: { 'id': widget.item.id, - 'isRadar': widget.isRadar, + 'type': widget.isRadar ? 'radar' : 'news', 'title': widget.item.title, 'onCommentsChanged': widget.onCommentsChanged, }, diff --git a/lib/views/home/widgets/overview/multitype.dart b/lib/views/home/widgets/overview/multitype.dart index 0076a05..6e5a5aa 100644 --- a/lib/views/home/widgets/overview/multitype.dart +++ b/lib/views/home/widgets/overview/multitype.dart @@ -3,13 +3,16 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:provider/provider.dart'; class MultitypeOverview extends StatelessWidget { final OverviewData item; @@ -23,20 +26,60 @@ class MultitypeOverview extends StatelessWidget { this.hasUnmarkConfirmation = false, }) : super(key: key); + get _targetPageArgs { + if (item.type == 'radar') { + return const RadarRequestArgs(page: 0); + } + if (item.type == 'news') { + return const NewsRequestArgs(page: 0); + } + return StudioRequestArgs(page: 0, type: item.type); + } + + String get _targetPageRouteName { + if (item.type == 'radar') { + return Routes.radarDetails; + } + if (item.type == 'news') { + return Routes.newsDetails; + } + return Routes.studioDetails; + } + + IconData get _icon { + if (item.type == 'radar') { + return DidvanIcons.radar_light; + } + if (item.type == 'news') { + return DidvanIcons.news_light; + } + if (item.type == 'video') { + return DidvanIcons.video_light; + } + return DidvanIcons.podcast_light; + } + @override Widget build(BuildContext context) { return DidvanCard( - onTap: () => Navigator.of(context).pushNamed( - item.type == 'radar' ? Routes.radarDetails : Routes.newsDetails, - arguments: { - 'onMarkChanged': onMarkChanged, - 'id': item.id, - 'args': item.type == 'radar' - ? const RadarRequestArgs(page: 0) - : const NewsRequestArgs(page: 0), - 'hasUnmarkConfirmation': hasUnmarkConfirmation, - }, - ), + onTap: () { + if (item.type == 'podcast') { + context.read().getStudioDetails( + item.id, + args: StudioRequestArgs(page: 0, type: item.type), + ); + return; + } + Navigator.of(context).pushNamed( + _targetPageRouteName, + arguments: { + 'onMarkChanged': onMarkChanged, + 'id': item.id, + 'args': _targetPageArgs, + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + }, + ); + }, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -52,9 +95,7 @@ class MultitypeOverview extends StatelessWidget { ), ), child: Icon( - item.type == 'radar' - ? DidvanIcons.radar_light - : DidvanIcons.news_light, + _icon, color: Theme.of(context).colorScheme.white, size: 18, ), diff --git a/lib/views/home/widgets/overview/podcast.dart b/lib/views/home/widgets/overview/podcast.dart index 1a243df..5476de4 100644 --- a/lib/views/home/widgets/overview/podcast.dart +++ b/lib/views/home/widgets/overview/podcast.dart @@ -19,11 +19,13 @@ class PodcastOverview extends StatelessWidget { final OverviewData podcast; final void Function(int id, bool value) onMarkChanged; final StudioRequestArgs? studioRequestArgs; + final bool hasUnmarkConfirmation; const PodcastOverview({ Key? key, required this.podcast, required this.onMarkChanged, this.studioRequestArgs, + this.hasUnmarkConfirmation = false, }) : super(key: key); @override @@ -82,6 +84,7 @@ class PodcastOverview extends StatelessWidget { ), const SizedBox(width: 16), BookmarkButton( + askForConfirmation: hasUnmarkConfirmation, gestureSize: 24, value: podcast.marked, onMarkChanged: (value) => onMarkChanged(podcast.id, value), diff --git a/lib/views/home/widgets/overview/radar.dart b/lib/views/home/widgets/overview/radar.dart index cd88ff3..d74f422 100644 --- a/lib/views/home/widgets/overview/radar.dart +++ b/lib/views/home/widgets/overview/radar.dart @@ -117,7 +117,7 @@ class RadarOverview extends StatelessWidget { onPressed: () => Navigator.of(context).pushNamed( Routes.comments, arguments: { - 'isRadar': true, + 'type': 'radar', 'title': radar.title, 'id': radar.id, 'onCommentsChanged': (count) => diff --git a/lib/views/home/widgets/overview/video.dart b/lib/views/home/widgets/overview/video.dart index 3150b34..2deaf2b 100644 --- a/lib/views/home/widgets/overview/video.dart +++ b/lib/views/home/widgets/overview/video.dart @@ -4,7 +4,6 @@ import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/utils/date_time.dart'; -import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; import 'package:didvan/views/home/widgets/bookmark_button.dart'; import 'package:didvan/views/home/widgets/duration_widget.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; @@ -14,18 +13,15 @@ import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class VideoOverview extends StatelessWidget { final OverviewData video; - final void Function(int id, int count) onCommentsChanged; final void Function(int id, bool value) onMarkChanged; final bool hasUnmarkConfirmation; final StudioRequestArgs? studioRequestArgs; const VideoOverview({ Key? key, required this.video, - required this.onCommentsChanged, required this.onMarkChanged, required this.hasUnmarkConfirmation, this.studioRequestArgs, @@ -38,12 +34,10 @@ class VideoOverview extends StatelessWidget { Routes.studioDetails, arguments: { 'onMarkChanged': onMarkChanged, - 'onCommentsChanged': onCommentsChanged, 'id': video.id, 'args': studioRequestArgs, 'hasUnmarkConfirmation': hasUnmarkConfirmation, 'isVideo': true, - 'state': context.read(), }, ), child: Row( @@ -115,6 +109,7 @@ class VideoOverview extends StatelessWidget { gestureSize: 24, value: video.marked, onMarkChanged: (value) => onMarkChanged(video.id, value), + askForConfirmation: hasUnmarkConfirmation, ), ], ), diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index 0dabcf2..bca06ac 100644 --- a/lib/views/widgets/didvan/scaffold.dart +++ b/lib/views/widgets/didvan/scaffold.dart @@ -9,6 +9,7 @@ class DidvanScaffold extends StatefulWidget { final EdgeInsets padding; final Color? backgroundColor; final bool reverse; + final ScrollController? scrollController; const DidvanScaffold({ Key? key, @@ -18,6 +19,7 @@ class DidvanScaffold extends StatefulWidget { this.padding = const EdgeInsets.symmetric(horizontal: 16), this.backgroundColor, this.reverse = false, + this.scrollController, }) : super(key: key); @override @@ -25,7 +27,13 @@ class DidvanScaffold extends StatefulWidget { } class _DidvanScaffoldState extends State { - final _scrollController = ScrollController(); + late final ScrollController _scrollController; + + @override + void initState() { + _scrollController = widget.scrollController ?? ScrollController(); + super.initState(); + } @override Widget build(BuildContext context) { @@ -33,7 +41,9 @@ class _DidvanScaffoldState extends State { return Scaffold( backgroundColor: widget.backgroundColor, body: Padding( - padding: EdgeInsets.only(top: statusBarHeight), + padding: widget.appBarData == null + ? EdgeInsets.zero + : EdgeInsets.only(top: statusBarHeight), child: Stack( children: [ CustomScrollView(