From ddc707bbe94cd141d7a42f2c784cbf33ad083ff0 Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Wed, 16 Mar 2022 11:08:05 +0330 Subject: [PATCH] D1APP-99 studio updates --- lib/models/overview_data.dart | 13 +- lib/models/srudio_data.dart | 37 --- lib/models/studio_details_data.dart | 61 +++++ lib/models/view/app_bar_data.dart | 9 +- lib/providers/user_provider.dart | 17 ++ lib/routes/route_generator.dart | 11 + lib/routes/routes.dart | 1 + lib/services/network/request_helper.dart | 43 ++-- lib/views/home/studio/studio.dart | 47 +++- .../studio/studio_details/studio_details.dart | 210 ++++++++++++++++++ .../studio_details/studio_details_state.dart | 141 ++++++++++++ lib/views/home/studio/studio_state.dart | 36 ++- lib/views/home/widgets/audio_slider.dart | 14 +- lib/views/home/widgets/duration_widget.dart | 47 ++++ lib/views/home/widgets/podcast_overview.dart | 103 +++++---- lib/views/home/widgets/video_overview.dart | 157 +++++++++++++ lib/views/widgets/didvan/app_bar.dart | 3 +- lib/views/widgets/didvan/scaffold.dart | 12 +- pubspec.lock | 30 ++- pubspec.yaml | 1 + 20 files changed, 870 insertions(+), 123 deletions(-) delete mode 100644 lib/models/srudio_data.dart create mode 100644 lib/models/studio_details_data.dart create mode 100644 lib/views/home/studio/studio_details/studio_details.dart create mode 100644 lib/views/home/studio/studio_details/studio_details_state.dart create mode 100644 lib/views/home/widgets/duration_widget.dart create mode 100644 lib/views/home/widgets/video_overview.dart diff --git a/lib/models/overview_data.dart b/lib/models/overview_data.dart index 4d16449..a658839 100644 --- a/lib/models/overview_data.dart +++ b/lib/models/overview_data.dart @@ -6,6 +6,7 @@ class OverviewData { final String image; final String description; final int? timeToRead; + final int? duration; final String? reference; final bool forManagers; final String createdAt; @@ -24,6 +25,7 @@ class OverviewData { required this.marked, required this.comments, required this.forManagers, + this.duration, this.timeToRead, this.reference, this.categories, @@ -39,11 +41,16 @@ class OverviewData { forManagers: json['forManagers'] ?? false, comments: json['comments'] ?? 0, createdAt: json['createdAt'], + duration: json['duration'], type: json['type'] ?? '', marked: json['marked'] ?? false, - categories: (json['categories'] as List?) - ?.map((e) => CategoryData.fromJson(e as Map)) - .toList(), + categories: json['categories'] != null + ? List.from( + json['categories'].map( + (e) => CategoryData.fromJson(e), + ), + ) + : null, ); Map toJson() => { diff --git a/lib/models/srudio_data.dart b/lib/models/srudio_data.dart deleted file mode 100644 index 1e7f4b6..0000000 --- a/lib/models/srudio_data.dart +++ /dev/null @@ -1,37 +0,0 @@ -class StudioData { - final int id; - final String title; - final String image; - final String duration; - final String createdAt; - bool marked; - - StudioData({ - required this.id, - required this.title, - required this.image, - required this.duration, - required this.createdAt, - required this.marked, - }); - - factory StudioData.fromJson(Map json) { - return StudioData( - id: json['id'], - title: json['title'], - image: json['image'], - duration: json['duration'], - createdAt: json['createdAt'], - marked: json['marked'], - ); - } - - Map toJson() => { - 'id': id, - 'title': title, - 'image': image, - 'duration': duration, - 'createdAt': createdAt, - 'marked': marked, - }; -} diff --git a/lib/models/studio_details_data.dart b/lib/models/studio_details_data.dart new file mode 100644 index 0000000..b83722e --- /dev/null +++ b/lib/models/studio_details_data.dart @@ -0,0 +1,61 @@ +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/tag.dart'; + +class StudioDetailsData { + final int id; + final int duration; + final String title; + final String description; + final String image; + final String media; + final String createdAt; + final int order; + bool marked; + int comments; + final List tags; + final List relatedContents = []; + + StudioDetailsData({ + required this.id, + required this.duration, + required this.title, + required this.description, + required this.image, + required this.media, + required this.createdAt, + required this.order, + required this.marked, + required this.comments, + required this.tags, + }); + + factory StudioDetailsData.fromJson(Map json) { + return StudioDetailsData( + id: json['id'], + duration: json['duration'], + title: json['title'], + description: json['description'], + image: json['image'], + media: json['media'], + createdAt: json['createdAt'], + order: json['order'], + marked: json['marked'], + comments: json['comments'], + tags: List.from(json['tags'].map((e) => Tag.fromJson(e))), + ); + } + + Map toJson() => { + 'id': id, + 'duration': duration, + 'title': title, + 'description': description, + 'image': image, + 'media': media, + 'createdAt': createdAt, + 'order': order, + 'marked': marked, + 'comments': comments, + 'tags': tags.map((e) => e.toJson()).toList(), + }; +} diff --git a/lib/models/view/app_bar_data.dart b/lib/models/view/app_bar_data.dart index 9e00aa5..85b6e91 100644 --- a/lib/models/view/app_bar_data.dart +++ b/lib/models/view/app_bar_data.dart @@ -5,6 +5,13 @@ class AppBarData { final String? subtitle; final bool hasBack; final Widget? trailing; + final bool isSmall; - AppBarData({this.title, this.subtitle, this.hasBack = false, this.trailing}); + AppBarData({ + this.title, + this.subtitle, + this.hasBack = false, + this.trailing, + this.isSmall = false, + }); } diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index f53d0ac..19c5192 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -14,6 +14,7 @@ class UserProvider extends CoreProvier { static final List _radarMarkQueue = []; static final List _newsMarkQueue = []; + static final List _studioMarkQueue = []; Future setAndGetToken({String? newToken}) async { if (newToken == null) { @@ -141,6 +142,22 @@ class UserProvider extends CoreProvier { }); } + static Future changeStudioMark(int id, bool value) async { + _studioMarkQueue.add(MapEntry(id, value)); + Future.delayed(const Duration(milliseconds: 500), () async { + final MapEntry? lastChange = + _studioMarkQueue.lastWhereOrNull((item) => item.key == id); + if (lastChange == null) return; + final service = RequestService(RequestHelper.markStudio(id)); + if (lastChange.value) { + await service.post(); + } else { + await service.delete(); + } + _studioMarkQueue.removeWhere((element) => element.key == id); + }); + } + static Future changeNewsMark(int id, bool value) async { _newsMarkQueue.add(MapEntry(id, value)); Future.delayed(const Duration(milliseconds: 500), () async { diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 205dcb6..2989dee 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -25,6 +25,8 @@ 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_state.dart'; import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/routes/routes.dart'; @@ -99,6 +101,15 @@ class RouteGenerator { ), ), ); + case Routes.studioDetails: + return _createRoute( + ChangeNotifierProvider( + create: (context) => StudioDetailsState(), + child: StudioDetails( + pageData: settings.arguments as Map, + ), + ), + ); case Routes.directList: return _createRoute( ChangeNotifierProvider( diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 7cbab35..fe94d78 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -8,6 +8,7 @@ class Routes { static const String generalSettings = '/general-settings'; static const String radarDetails = '/radar-details'; static const String newsDetails = '/news-details'; + static const String studioDetails = '/studio-details'; static const String directList = '/direct-list'; static const String direct = '/direct'; static const String comments = '/comments'; diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index ccb6391..e35f070 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -19,8 +19,19 @@ class RequestHelper { static const String checkUsername = _baseUserUrl + '/CheckUsername'; static const String updateProfile = _baseUserUrl + '/profile/edit'; static const String otp = _baseUserUrl + '/otp'; - static String bookmarks({String? type}) => - _baseUserUrl + '/marked/${type ?? ''}'; + static String bookmarks({ + required int page, + String? search, + String? type, + String? studioType, + }) => + _baseUserUrl + + '/marked/${type ?? ''}' + + _urlConcatGenerator([ + MapEntry('page', page), + MapEntry('type', studioType), + MapEntry('search', search), + ]); static const String directTypes = baseUrl + '/direct/types'; static String direct(int id) => _baseDirectUrl + '/$id'; @@ -36,10 +47,10 @@ class RequestHelper { baseUrl + '/tag' + _urlConcatGenerator([ - MapEntry('page', page?.toString()), - MapEntry('limit', limit?.toString() ?? '3'), + MapEntry('page', page), + MapEntry('limit', limit ?? '3'), MapEntry('type', type), - MapEntry('id', itemId?.toString() ?? '1'), + MapEntry('id', itemId ?? '1'), MapEntry('tags', _urlListConcatGenerator(ids)), ]); @@ -52,7 +63,7 @@ class RequestHelper { _baseRadarUrl + '/$id' + _urlConcatGenerator([ - MapEntry('page', args.page.toString()), + MapEntry('page', args.page), MapEntry('start', args.startDate), MapEntry('end', args.endDate), MapEntry('search', args.search), @@ -61,7 +72,7 @@ class RequestHelper { static String radarOverviews({required RadarRequestArgs args}) => _baseRadarUrl + _urlConcatGenerator([ - MapEntry('page', args.page.toString()), + MapEntry('page', args.page), MapEntry('start', args.startDate), MapEntry('end', args.endDate), MapEntry('search', args.search), @@ -77,7 +88,7 @@ class RequestHelper { _baseNewsUrl + '/$id' + _urlConcatGenerator([ - MapEntry('page', args.page.toString()), + MapEntry('page', args.page), MapEntry('start', args.startDate), MapEntry('end', args.endDate), MapEntry('search', args.search), @@ -85,7 +96,7 @@ class RequestHelper { static String newsOverviews({required NewsRequestArgs args}) => _baseNewsUrl + _urlConcatGenerator([ - MapEntry('page', args.page.toString()), + MapEntry('page', args.page), MapEntry('start', args.startDate), MapEntry('end', args.endDate), MapEntry('search', args.search), @@ -101,27 +112,29 @@ class RequestHelper { _baseStudioUrl + '/$id' + _urlConcatGenerator([ - MapEntry('page', args.page.toString()), + MapEntry('page', args.page), MapEntry('type', args.type), MapEntry('order', args.order), MapEntry('search', args.search), ]); static String studioOverviews({required StudioRequestArgs args}) => - _baseNewsUrl + + _baseStudioUrl + _urlConcatGenerator([ - MapEntry('page', args.page.toString()), + MapEntry('page', args.page), MapEntry('type', args.type), MapEntry('order', args.order), MapEntry('search', args.search), ]); - static String _urlConcatGenerator(List> additions) { + static String _urlConcatGenerator(List> additions) { String result = ''; - additions.removeWhere((element) => element.value == null); + additions.removeWhere( + (element) => element.value == null || element.value.toString().isEmpty, + ); if (additions.isNotEmpty) { result += '?'; for (var i = 0; i < additions.length; i++) { - result += (additions[i].key + '=' + additions[i].value!); + result += (additions[i].key + '=' + additions[i].value!.toString()); if (i != additions.length - 1) { result += '&'; } diff --git a/lib/views/home/studio/studio.dart b/lib/views/home/studio/studio.dart index a362ed8..d4cba73 100644 --- a/lib/views/home/studio/studio.dart +++ b/lib/views/home/studio/studio.dart @@ -1,11 +1,14 @@ 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/utils/action_sheet.dart'; import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/home/studio/widgets/slider.dart'; import 'package:didvan/views/home/studio/widgets/tab_bar.dart'; import 'package:didvan/views/home/widgets/logo_app_bar.dart'; +import 'package:didvan/views/home/widgets/podcast_overview.dart'; import 'package:didvan/views/home/widgets/search_field.dart'; +import 'package:didvan/views/home/widgets/video_overview.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'; @@ -24,6 +27,12 @@ class Studio extends StatefulWidget { class _StudioState extends State { final FocusNode _focusNode = FocusNode(); + @override + void initState() { + context.read().init(); + super.initState(); + } + @override Widget build(BuildContext context) { return CustomScrollView( @@ -86,9 +95,41 @@ class _StudioState extends State { Consumer( builder: (context, state, child) => SliverStateHandler( state: state, - builder: (context, state, index) => Container(), + itemPadding: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + placeholder: state.videosSelected + ? VideoOverview.placeHolder + : PodcastOverview.placeholder, + builder: (context, state, index) => state.videosSelected + ? VideoOverview( + onMarkChanged: state.changeMark, + hasUnmarkConfirmation: false, + video: state.studios[index], + onCommentsChanged: state.onCommentsChanged, + studioRequestArgs: StudioRequestArgs( + page: state.page, + order: state.order, + search: state.search, + type: state.type, + ), + ) + : PodcastOverview( + podcast: state.studios[index], + onMarkChanged: state.changeMark, + hasUnmarkConfirmation: false, + onCommentsChanged: state.onCommentsChanged, + studioRequestArgs: StudioRequestArgs( + page: state.page, + order: state.order, + search: state.search, + type: state.type, + ), + ), childCount: state.studios.length, - onRetry: () {}, + onRetry: () => state.getStudioOverviews(page: 1), ), ), ], @@ -132,7 +173,7 @@ class _StudioState extends State { titleIcon: DidvanIcons.sort_regular, hasDismissButton: false, confrimTitle: 'مرتب سازی', - onConfirmed: () {}, + onConfirmed: () => state.getStudioOverviews(page: 1), ), ); } diff --git a/lib/views/home/studio/studio_details/studio_details.dart b/lib/views/home/studio/studio_details/studio_details.dart new file mode 100644 index 0000000..26491e2 --- /dev/null +++ b/lib/views/home/studio/studio_details/studio_details.dart @@ -0,0 +1,210 @@ +import 'dart:io'; + +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/models/studio_details_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/widgets/audio_slider.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.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:flutter/services.dart'; +import 'package:provider/provider.dart'; +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 { + bool _isFullScreen = false; + bool _isInit = true; + + double _dwInPortrait = 0; + double _scaleInPortrait = 1; + + bool get _isVideo => widget.pageData['isVideo']; + + @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(); + 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) => state.studios.isEmpty + ? const SizedBox() + : WillPopScope( + onWillPop: () async { + if (_isFullScreen) { + await _changeFullSceen(false); + return false; + } + return true; + }, + child: DidvanScaffold( + appBarData: _isFullScreen + ? null + : AppBarData( + isSmall: true, + title: state.currentStudio.title, + ), + children: [ + if (_isVideo) + SizedBox( + width: ds.width, + height: _isFullScreen ? ds.height : ds.width * 9 / 16, + child: Stack( + children: [ + WebView( + initialUrl: Uri.dataFromString( + ''' + + + + + + + ${state.currentStudio.media} + + + ''', + 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, + ), + ), + ), + ], + ), + ), + if (!_isVideo) + AudioPlayerWidget(podcast: state.currentStudio), + ], + ), + ), + ), + ); + } +} + +class AudioPlayerWidget extends StatelessWidget { + final StudioDetailsData podcast; + const AudioPlayerWidget({Key? key, required this.podcast}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Hero( + tag: podcast.media, + child: SkeletonImage( + imageUrl: podcast.image, + aspectRatio: 1 / 1, + ), + ), + ), + const SizedBox(height: 16), + DidvanText( + podcast.title, + style: Theme.of(context).textTheme.bodyText1, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AudioSlider( + tag: podcast.media, + showTimer: true, + ), + ), + ], + ); + } +} diff --git a/lib/views/home/studio/studio_details/studio_details_state.dart b/lib/views/home/studio/studio_details/studio_details_state.dart new file mode 100644 index 0000000..4a4d9e2 --- /dev/null +++ b/lib/views/home/studio/studio_details/studio_details_state.dart @@ -0,0 +1,141 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/studio.dart'; +import 'package:didvan/models/studio_details_data.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class StudioDetailsState extends CoreProvier { + final List studios = []; + late Timer _trackingTimer; + int _trackingTimerCounter = 0; + late final int initialIndex; + late final StudioRequestArgs args; + bool isFetchingNewItem = false; + final List relatedQueue = []; + + int _currentIndex = 0; + int get currentIndex => _currentIndex; + + StudioDetailsData get currentStudio { + try { + return studios[_currentIndex]!; + } catch (e) { + return studios[_currentIndex + 1]!; + } + } + + Future getStudioDetails(int id, {bool? isForward}) async { + if (isForward == null) { + appState = AppState.busy; + } else { + isFetchingNewItem = true; + notifyListeners(); + } + final service = RequestService(RequestHelper.studioDetails(id, args)); + await service.httpGet(); + _handleTracking(sendRequest: isForward != null); + if (service.isSuccess) { + final result = service.result; + final studio = StudioDetailsData.fromJson(result['studio']); + if (args.page == 0) { + studios.add(studio); + initialIndex = 0; + appState = AppState.idle; + return; + } + + StudioDetailsData? prevStudio; + if (result['prevStudio'].isNotEmpty) { + prevStudio = StudioDetailsData.fromJson(result['prevStudio']); + } + + StudioDetailsData? nextStudio; + if (result['nextStudio'].isNotEmpty) { + nextStudio = StudioDetailsData.fromJson(result['nextStudio']); + } + + if (isForward == null) { + studios + .addAll(List.generate(max(studio.order - 2, 0), (index) => null)); + if (prevStudio != null) { + studios.add(prevStudio); + } + studios.add(studio); + if (nextStudio != null) { + studios.add(nextStudio); + } + _currentIndex = initialIndex = studio.order - 1; + } else if (isForward) { + if (!exists(nextStudio) && nextStudio != null) { + studios.add(nextStudio); + } + _currentIndex++; + } else if (!isForward) { + if (!exists(prevStudio) && prevStudio != null) { + studios[_currentIndex - 2] = prevStudio; + } + _currentIndex--; + } + isFetchingNewItem = false; + appState = AppState.idle; + return; + } + //why? total page state shouldn't die! + if (isForward == null) { + appState = AppState.failed; + } + } + + 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', + )); + await service.httpGet(); + if (service.isSuccess) { + final relateds = service.result['contents']; + for (var i = 0; i < relateds.length; i++) { + studios + .where((element) => element != null) + .firstWhere((element) => element!.id == currentStudio.id)! + .relatedContents + .add(OverviewData.fromJson(relateds[i])); + } + notifyListeners(); + } + } + + bool exists(StudioDetailsData? studio) => + studios.any((r) => studio != null && r != null && r.id == studio.id); + + void onCommentsChanged(int count) { + studios.firstWhere((studio) => studio?.id == currentStudio.id)!.comments = + count; + notifyListeners(); + } + + Future _handleTracking({bool sendRequest = true}) async { + if (!sendRequest) { + _trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + _trackingTimerCounter++; + }); + return; + } + //send request + _trackingTimerCounter = 0; + } + + @override + void dispose() { + _trackingTimer.cancel(); + super.dispose(); + } +} diff --git a/lib/views/home/studio/studio_state.dart b/lib/views/home/studio/studio_state.dart index f8e6944..18b7dc2 100644 --- a/lib/views/home/studio/studio_state.dart +++ b/lib/views/home/studio/studio_state.dart @@ -1,12 +1,13 @@ import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/studio.dart'; -import 'package:didvan/models/srudio_data.dart'; import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; class StudioState extends CoreProvier { - final List studios = []; + final List studios = []; String? search; String? lastSearch; @@ -22,52 +23,71 @@ class StudioState extends CoreProvier { set videosSelected(bool value) { if (_videosSelected == value) return; _videosSelected = value; + studios.clear(); getStudioOverviews(page: page); } void init() { search = ''; lastSearch = ''; - videosSelected = true; + _videosSelected = true; selectedSortTypeIndex = 0; Future.delayed(Duration.zero, () { getStudioOverviews(page: 1); }); } - String get _order { + String get order { if (selectedSortTypeIndex == 0) return 'date'; if (selectedSortTypeIndex == 1) return 'view'; return 'comment'; } + String get type { + if (videosSelected) return 'video'; + return 'podcast'; + } + Future getStudioOverviews({required int page}) async { this.page = page; if (page == 1) { appState = AppState.busy; } - final service = RequestService( RequestHelper.studioOverviews( args: StudioRequestArgs( page: page, - type: videosSelected ? 'video' : 'podcast', + type: type, search: search, - order: _order, + order: order, ), ), ); await service.httpGet(); if (service.isSuccess) { + if (page == 1) { + studios.clear(); + } lastPage = service.result['lastPage']; final studioItems = service.result['studios']; for (var i = 0; i < studioItems.length; i++) { - studios.add(StudioData.fromJson(studioItems[i])); + studios.add(OverviewData.fromJson(studioItems[i])); } appState = AppState.idle; return; } appState = AppState.failed; } + + Future changeMark(int id, bool value) async { + studios.firstWhere((element) => element.id == id).marked = value; + notifyListeners(); + UserProvider.changeStudioMark(id, value); + } + + void onCommentsChanged(int id, int count) { + studios.firstWhere((radar) => radar.id == id).comments = count; + notifyListeners(); + } } diff --git a/lib/views/home/widgets/audio_slider.dart b/lib/views/home/widgets/audio_slider.dart index b9edf70..dd7196f 100644 --- a/lib/views/home/widgets/audio_slider.dart +++ b/lib/views/home/widgets/audio_slider.dart @@ -1,10 +1,13 @@ import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; +import 'package:didvan/config/design_config.dart'; import 'package:didvan/services/media/media.dart'; import 'package:flutter/material.dart'; class AudioSlider extends StatelessWidget { final String tag; - const AudioSlider({Key? key, required this.tag}) : super(key: key); + final bool showTimer; + const AudioSlider({Key? key, required this.tag, this.showTimer = false}) + : super(key: key); bool get _isPlaying => MediaService.audioPlayerTag == tag; @@ -24,7 +27,14 @@ class AudioSlider extends StatelessWidget { _isPlaying ? MediaService.audioPlayer.bufferedPosition : null, thumbRadius: 6, barHeight: 3, - timeLabelTextStyle: const TextStyle(fontSize: 0), + timeLabelTextStyle: TextStyle( + fontSize: showTimer ? null : 0, + height: showTimer ? null : 0, + fontFamily: DesignConfig.fontFamily.replaceAll( + '-FA', + '', + ), + ), onSeek: (value) => _onSeek(value.inMilliseconds), ); }, diff --git a/lib/views/home/widgets/duration_widget.dart b/lib/views/home/widgets/duration_widget.dart new file mode 100644 index 0000000..025224d --- /dev/null +++ b/lib/views/home/widgets/duration_widget.dart @@ -0,0 +1,47 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class DurationWidget extends StatelessWidget { + final int duration; + const DurationWidget({Key? key, required this.duration}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.focusedBorder, + ), + borderRadius: BorderRadius.circular(5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + DidvanIcons.timer_regular, + size: 16, + color: Theme.of(context).colorScheme.focusedBorder, + ), + const SizedBox(width: 4), + DidvanText( + DateTimeUtils.normalizeTimeDuration( + Duration(seconds: duration), + ), + isEnglishFont: true, + color: Theme.of(context).colorScheme.focusedBorder, + ), + const SizedBox(width: 4), + Icon( + DidvanIcons.play_circle_regular, + size: 16, + color: Theme.of(context).colorScheme.focusedBorder, + ), + ], + ), + ); + } +} diff --git a/lib/views/home/widgets/podcast_overview.dart b/lib/views/home/widgets/podcast_overview.dart index fa626fb..2e2265d 100644 --- a/lib/views/home/widgets/podcast_overview.dart +++ b/lib/views/home/widgets/podcast_overview.dart @@ -1,38 +1,46 @@ +import 'package:didvan/config/theme_data.dart'; +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/studio.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/utils/date_time.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'; import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.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'; class PodcastOverview extends StatelessWidget { - final OverviewData news; - final NewsRequestArgs? newsRequestArgs; + final OverviewData podcast; + final void Function(int id, int count) onCommentsChanged; final void Function(int id, bool value) onMarkChanged; final bool hasUnmarkConfirmation; + final StudioRequestArgs? studioRequestArgs; const PodcastOverview({ Key? key, - required this.news, + required this.podcast, + required this.onCommentsChanged, required this.onMarkChanged, - this.newsRequestArgs, - this.hasUnmarkConfirmation = false, + required this.hasUnmarkConfirmation, + this.studioRequestArgs, }) : super(key: key); @override Widget build(BuildContext context) { return DidvanCard( onTap: () => Navigator.of(context).pushNamed( - Routes.newsDetails, + Routes.studioDetails, arguments: { 'onMarkChanged': onMarkChanged, - 'id': news.id, - 'args': newsRequestArgs, + 'onCommentsChanged': onCommentsChanged, + 'id': podcast.id, + 'args': studioRequestArgs, 'hasUnmarkConfirmation': hasUnmarkConfirmation, + 'isVideo': false, }, ), child: Column( @@ -41,47 +49,50 @@ class PodcastOverview extends StatelessWidget { Row( children: [ SkeletonImage( - imageUrl: news.image, + imageUrl: podcast.image, width: 64, height: 64, ), const SizedBox(width: 8), Expanded( - child: SizedBox( - height: 64, - child: DidvanText( - news.title, - style: Theme.of(context).textTheme.bodyText1, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + podcast.title, + style: Theme.of(context).textTheme.bodyText1, + ), + const SizedBox(height: 4), + DidvanText( + DateTimeUtils.momentGenerator(podcast.createdAt), + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + ], ), ), ], ), const SizedBox(height: 8), DidvanText( - news.description, - maxLines: 3, + podcast.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), const DidvanDivider(verticalPadding: 8), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - DidvanText( - news.reference!, - style: Theme.of(context).textTheme.caption, - ), - DidvanText( - ' - ' + DateTimeUtils.momentGenerator(news.createdAt), - style: Theme.of(context).textTheme.caption, - ), - ], + DurationWidget(duration: podcast.duration!), + const Spacer(), + DidvanIconButton( + gestureSize: 28, + icon: DidvanIcons.download_regular, + onPressed: () {}, ), + const SizedBox(width: 16), BookmarkButton( - value: news.marked, - onMarkChanged: (value) => onMarkChanged(news.id, value), - askForConfirmation: hasUnmarkConfirmation, + value: podcast.marked, + onMarkChanged: (value) => onMarkChanged(podcast.id, value), ), ], ), @@ -95,7 +106,7 @@ class PodcastOverview extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ const ShimmerPlaceholder(height: 64, width: 64), const SizedBox(width: 8), @@ -109,7 +120,7 @@ class PodcastOverview extends StatelessWidget { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), const ShimmerPlaceholder( height: 16, width: double.infinity, @@ -117,19 +128,21 @@ class PodcastOverview extends StatelessWidget { const SizedBox(height: 8), const ShimmerPlaceholder( height: 16, - width: double.infinity, - ), - const SizedBox(height: 8), - const ShimmerPlaceholder( - height: 16, - width: 100, + width: 200, ), + const SizedBox(height: 4), const DidvanDivider(verticalPadding: 8), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - ShimmerPlaceholder(height: 12, width: 150), - ShimmerPlaceholder(height: 24, width: 24), + children: [ + ShimmerPlaceholder( + height: 36, + width: 92, + borderRadius: BorderRadius.circular(5), + ), + const Spacer(), + const ShimmerPlaceholder(width: 24, height: 24), + const SizedBox(width: 16), + const ShimmerPlaceholder(width: 24, height: 24), ], ), ], diff --git a/lib/views/home/widgets/video_overview.dart b/lib/views/home/widgets/video_overview.dart new file mode 100644 index 0000000..f7e4975 --- /dev/null +++ b/lib/views/home/widgets/video_overview.dart @@ -0,0 +1,157 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +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/widgets/bookmark_button.dart'; +import 'package:didvan/views/home/widgets/duration_widget.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.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'; + +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, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + onTap: () => Navigator.of(context).pushNamed( + Routes.studioDetails, + arguments: { + 'onMarkChanged': onMarkChanged, + 'onCommentsChanged': onCommentsChanged, + 'id': video.id, + 'args': studioRequestArgs, + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + 'isVideo': true, + }, + ), + child: Row( + children: [ + Stack( + children: [ + SkeletonImage( + imageUrl: video.image, + height: 108, + width: 108, + ), + Positioned.fill( + child: Center( + child: Container( + height: 28, + width: 28, + 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, + ), + ), + ), + ), + ], + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + video.title, + style: Theme.of(context).textTheme.bodyText1, + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon( + DidvanIcons.calendar_day_regular, + size: 16, + ), + const SizedBox(width: 4), + DidvanText( + DateTimeUtils.momentGenerator(video.createdAt), + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + const DidvanDivider(verticalPadding: 8), + Row( + children: [ + DurationWidget(duration: video.duration!), + const Spacer(), + DidvanIconButton( + gestureSize: 28, + icon: DidvanIcons.download_regular, + onPressed: () {}, + ), + const SizedBox(width: 16), + BookmarkButton( + value: video.marked, + onMarkChanged: (value) => onMarkChanged(video.id, value), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + static Widget get placeHolder => DidvanCard( + child: Row( + children: [ + const ShimmerPlaceholder(height: 108, width: 108), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder(height: 20), + const SizedBox(height: 8), + const ShimmerPlaceholder(width: 100, height: 16), + const DidvanDivider(verticalPadding: 10), + Row( + children: [ + ShimmerPlaceholder( + height: 36, + width: 92, + borderRadius: BorderRadius.circular(5), + ), + const Spacer(), + const ShimmerPlaceholder(width: 24, height: 24), + const SizedBox(width: 16), + const ShimmerPlaceholder(width: 24, height: 24), + ], + ), + ], + ), + ), + ], + ), + ); +} diff --git a/lib/views/widgets/didvan/app_bar.dart b/lib/views/widgets/didvan/app_bar.dart index 1de49fe..7667dd8 100644 --- a/lib/views/widgets/didvan/app_bar.dart +++ b/lib/views/widgets/didvan/app_bar.dart @@ -18,11 +18,10 @@ class DidvanAppBar extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - height: kToolbarHeight + MediaQuery.of(context).padding.top, + height: appBarData.isSmall ? 56 : 72, width: MediaQuery.of(context).size.width, padding: const EdgeInsets.only(right: 4, left: 20), decoration: BoxDecoration( - color: backgroundColor, border: hasBorder ? Border( bottom: BorderSide( diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index db75ac9..b1d8b28 100644 --- a/lib/views/widgets/didvan/scaffold.dart +++ b/lib/views/widgets/didvan/scaffold.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; class DidvanScaffold extends StatefulWidget { final List? slivers; final List? children; - final AppBarData appBarData; + final AppBarData? appBarData; final EdgeInsets padding; final Color? backgroundColor; final bool reverse; @@ -40,14 +40,14 @@ class _DidvanScaffoldState extends State { controller: _scrollController, reverse: widget.reverse, slivers: [ - if (!widget.reverse) + if (!widget.reverse && widget.appBarData != null) SliverAppBar( - toolbarHeight: kToolbarHeight, + toolbarHeight: widget.appBarData!.isSmall ? 56 : 72, backgroundColor: widget.backgroundColor ?? Theme.of(context).colorScheme.background, automaticallyImplyLeading: false, pinned: true, - flexibleSpace: DidvanAppBar(appBarData: widget.appBarData), + flexibleSpace: DidvanAppBar(appBarData: widget.appBarData!), ), if (!widget.reverse) const SliverToBoxAdapter( @@ -79,9 +79,9 @@ class _DidvanScaffoldState extends State { ), ], ), - if (widget.reverse) + if (widget.reverse && widget.appBarData != null) _AppBar( - appBarData: widget.appBarData, + appBarData: widget.appBarData!, scrollController: _scrollController, ), ], diff --git a/pubspec.lock b/pubspec.lock index 1ae7618..825fa09 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -566,7 +566,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.2" process: dependency: transitive description: @@ -782,6 +782,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.3" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + url: "https://pub.dartlang.org" + source: hosted + version: "2.7.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c685dfc..41c210a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: image_cropper: ^1.5.0 firebase_messaging: ^11.2.8 firebase_core: ^1.13.1 + webview_flutter: ^3.0.1 dev_dependencies: