diff --git a/.fandogh/fandogh.yaml b/.fandogh/fandogh.yaml index 1d5bac1..ebd7abc 100644 --- a/.fandogh/fandogh.yaml +++ b/.fandogh/fandogh.yaml @@ -1,13 +1,13 @@ kind: ExternalService -name: app-test +name: app-dev spec: allow_http: false disable_default_domains: true - image: app:1.1.1 + image: app-dev:1.1.4 image_pull_policy: IfNotPresent path: / replicas: 1 resources: - memory: 150Mi + memory: 100Mi domains: - - name: web.didvan.app \ No newline at end of file + - name: dev.didvan.app \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 55fa4cf..89b36ba 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -88,6 +88,8 @@ PODS: - TOCropViewController (2.6.1) - url_launcher_ios (0.0.1): - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) @@ -103,6 +105,7 @@ DEPENDENCIES: - record (from `.symlinks/plugins/record/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: @@ -145,6 +148,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: audio_session: 4f3e461722055d21515cf3261b64c973c062f345 @@ -171,6 +176,7 @@ SPEC CHECKSUMS: sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af + webview_flutter_wkwebview: 005fbd90c888a42c5690919a1527ecc6649e1162 PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea diff --git a/lib/config/design_config.dart b/lib/config/design_config.dart index 13cf836..bfb0fb8 100644 --- a/lib/config/design_config.dart +++ b/lib/config/design_config.dart @@ -43,8 +43,7 @@ class DesignConfig { static SystemUiOverlayStyle get systemUiOverlayStyle => SystemUiOverlayStyle( statusBarIconBrightness: brightness == Brightness.dark ? Brightness.light : Brightness.dark, - statusBarColor: - Theme.of(context!).colorScheme.background.withOpacity(0.5), + statusBarColor: Theme.of(context!).colorScheme.background, systemNavigationBarColor: Theme.of(context!).colorScheme.surface, systemNavigationBarDividerColor: Colors.transparent, systemNavigationBarIconBrightness: diff --git a/lib/models/overview_data.dart b/lib/models/overview_data.dart index 4d16449..b72995b 100644 --- a/lib/models/overview_data.dart +++ b/lib/models/overview_data.dart @@ -6,7 +6,9 @@ class OverviewData { final String image; final String description; final int? timeToRead; + final int? duration; final String? reference; + final String? media; final bool forManagers; final String createdAt; final String type; @@ -24,6 +26,8 @@ class OverviewData { required this.marked, required this.comments, required this.forManagers, + this.media, + this.duration, this.timeToRead, this.reference, this.categories, @@ -39,11 +43,17 @@ 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(), + media: json['media'], + categories: json['categories'] != null + ? List.from( + json['categories'].map( + (e) => CategoryData.fromJson(e), + ), + ) + : null, ); Map toJson() => { diff --git a/lib/models/requests/studio.dart b/lib/models/requests/studio.dart new file mode 100644 index 0000000..74feeaa --- /dev/null +++ b/lib/models/requests/studio.dart @@ -0,0 +1,13 @@ +class StudioRequestArgs { + final int page; + final String? search; + final String? order; + final String? type; + + const StudioRequestArgs({ + required this.page, + this.search, + this.order, + this.type, + }); +} 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/action_sheet_data.dart b/lib/models/view/action_sheet_data.dart index 6166bb6..d3e6b3c 100644 --- a/lib/models/view/action_sheet_data.dart +++ b/lib/models/view/action_sheet_data.dart @@ -6,7 +6,8 @@ class ActionSheetData { final String? dismissTitle; final VoidCallback? onConfirmed; final VoidCallback? onDismissed; - final String title; + final String? title; + final bool hasPadding; final IconData? titleIcon; final Color? titleColor; final bool hasDismissButton; @@ -16,10 +17,11 @@ class ActionSheetData { const ActionSheetData({ required this.content, - required this.title, + this.title, this.confrimTitle, this.onConfirmed, this.titleColor, + this.hasPadding = true, this.hasDismissButton = true, this.hasConfirmButton = true, this.titleIcon, 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/server_data_provider.dart b/lib/providers/server_data_provider.dart index 3b76c8f..532bb5e 100644 --- a/lib/providers/server_data_provider.dart +++ b/lib/providers/server_data_provider.dart @@ -8,7 +8,7 @@ class ServerDataProvider { await _getDirectTypes(); } - static int labelToTypeId(String? label) => label == null + static int labelToTypeId(String label) => label.contains('پشتیبانی') ? 7 : directTypes.firstWhere((element) => element.value.contains(label)).key; diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index 65aeacf..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) { @@ -24,13 +25,16 @@ class UserProvider extends CoreProvier { return null; } - Future getUserInfo() async { + Future getUserInfo() async { isAuthenticated = true; final RequestService service = RequestService(RequestHelper.userInfo); await service.httpGet(); if (service.isSuccess) { user = User.fromJson(service.result['user']); - return; + return true; + } + if (service.statusCode == 401) { + return false; } throw 'Getting user from API failed!'; } @@ -138,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 205d9b7..7514e7c 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'; @@ -62,6 +64,9 @@ class RouteGenerator { ChangeNotifierProvider( create: (context) => StudioState(), ), + ChangeNotifierProvider( + create: (context) => StudioDetailsState(), + ), ], child: const Home(), ), @@ -99,6 +104,15 @@ class RouteGenerator { ), ), ); + case Routes.studioDetails: + return _createRoute( + ChangeNotifierProvider.value( + value: (settings.arguments as Map)['state'], + child: StudioDetails( + pageData: settings.arguments as Map, + ), + ), + ); case Routes.directList: return _createRoute( ChangeNotifierProvider( @@ -170,7 +184,11 @@ class RouteGenerator { final shortestSide = MediaQuery.of(context).size.shortestSide; final bool useMobileLayout = shortestSide < 600; if (kIsWeb && !useMobileLayout) { - return Center(child: AspectRatio(aspectRatio: 9 / 16, child: page)); + return Container( + color: Theme.of(context).colorScheme.background, + alignment: Alignment.center, + child: AspectRatio(aspectRatio: 9 / 16, child: page), + ); } return Container( color: Theme.of(context).colorScheme.surface, 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/media/media.dart b/lib/services/media/media.dart index fb0fe74..353b309 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -1,3 +1,5 @@ +import 'package:didvan/models/requests/studio.dart'; +import 'package:didvan/models/studio_details_data.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:flutter/foundation.dart'; @@ -7,8 +9,8 @@ import 'package:just_audio/just_audio.dart'; class MediaService { static final AudioPlayer audioPlayer = AudioPlayer(); static String? audioPlayerTag; - static String? audioPlayerTitle; - static String? audioPlayerCover; + static StudioDetailsData? currentPodcast; + static StudioRequestArgs? podcastPlaylistArgs; static void init() { audioPlayer.positionStream.listen((event) { @@ -21,6 +23,7 @@ class MediaService { static Future handleAudioPlayback({ required dynamic audioSource, + bool isVoiceMessage = true, }) async { bool isNetworkAudio = audioSource.runtimeType == String; String tag; @@ -40,9 +43,11 @@ class MediaService { audioPlayerTag = tag; if (isNetworkAudio) { await audioPlayer.setUrl( - RequestHelper.baseUrl + - audioSource + - '?accessToken=${RequestService.token}', + isVoiceMessage + ? (RequestHelper.baseUrl + + audioSource + + '?accessToken=${RequestService.token}') + : audioSource, ); } else { if (kIsWeb) { @@ -58,6 +63,8 @@ class MediaService { static Future resetAudioPlayer() async { audioPlayerTag = null; + currentPodcast = null; + podcastPlaylistArgs = null; MediaService.audioPlayer.stop(); } diff --git a/lib/services/network/request.dart b/lib/services/network/request.dart index 1614cbc..67ac373 100644 --- a/lib/services/network/request.dart +++ b/lib/services/network/request.dart @@ -5,6 +5,7 @@ import 'package:http_parser/http_parser.dart' as parser; class RequestService { static late String token; + int? statusCode; Map get result => _body?['result'] ?? const {}; Map get errors => _body?['errors'] ?? const {}; @@ -162,6 +163,7 @@ class RequestService { } void _handleResponse(http.Response? response) { + statusCode = response?.statusCode; if (_handleError(response)) { if (response!.body.isNotEmpty) { _body = json.decode(response.body); diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index 9ae260c..bfddb9a 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -1,5 +1,6 @@ import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/models/requests/studio.dart'; class RequestHelper { static const String baseUrl = 'https://api.didvan.app'; @@ -18,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'; @@ -35,10 +47,11 @@ class RequestHelper { baseUrl + '/tag' + _urlConcatGenerator([ - MapEntry('limit', limit?.toString() ?? '3'), + MapEntry('page', page), + MapEntry('limit', limit ?? '3'), MapEntry('type', type), - MapEntry('id', itemId?.toString() ?? '1'), - MapEntry('tags', _urlListConcatGenerator(ids)) + MapEntry('id', itemId ?? '1'), + MapEntry('tags', _urlListConcatGenerator(ids)), ]); static String markRadar(int id) => _baseRadarUrl + '/$id/mark'; @@ -50,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), @@ -59,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), @@ -75,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), @@ -83,19 +96,45 @@ 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), ]); - static String _urlConcatGenerator(List> additions) { + 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 studioDetails(int id, StudioRequestArgs args) => + _baseStudioUrl + + '/$id' + + _urlConcatGenerator([ + MapEntry('page', args.page), + MapEntry('type', args.type), + MapEntry('order', args.order), + MapEntry('search', args.search), + ]); + static String studioOverviews({required StudioRequestArgs args}) => + _baseStudioUrl + + _urlConcatGenerator([ + MapEntry('page', args.page), + MapEntry('type', args.type), + MapEntry('order', args.order), + MapEntry('search', args.search), + ]); + + 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/utils/action_sheet.dart b/lib/utils/action_sheet.dart index 0bdb571..15ea262 100644 --- a/lib/utils/action_sheet.dart +++ b/lib/utils/action_sheet.dart @@ -79,7 +79,7 @@ class ActionSheetUtils { isScrollControlled: true, context: context, builder: (context) => Container( - padding: const EdgeInsets.all(20), + padding: data.hasPadding ? const EdgeInsets.all(20) : EdgeInsets.zero, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.vertical( @@ -91,6 +91,7 @@ class ActionSheetUtils { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 20), Center( child: Container( height: 3, @@ -99,23 +100,24 @@ class ActionSheetUtils { ), ), const SizedBox(height: 8), - Row( - children: [ - if (data.titleIcon != null) - Icon( - data.titleIcon, + if (data.title != null) + Row( + children: [ + if (data.titleIcon != null) + Icon( + data.titleIcon, + color: data.titleColor ?? + Theme.of(context).colorScheme.title, + ), + if (data.titleIcon != null) const SizedBox(width: 8), + DidvanText( + data.title!, + style: Theme.of(context).textTheme.subtitle1, color: data.titleColor ?? Theme.of(context).colorScheme.title, - ), - if (data.titleIcon != null) const SizedBox(width: 8), - DidvanText( - data.title, - style: Theme.of(context).textTheme.subtitle1, - color: - data.titleColor ?? Theme.of(context).colorScheme.title, - ) - ], - ), + ) + ], + ), const SizedBox(height: 28), data.content, const SizedBox(height: 28), @@ -169,30 +171,31 @@ class ActionSheetUtils { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Icon( - data.titleIcon, - size: 20, - color: data.titleColor, + if (data.title != null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Icon( + data.titleIcon, + size: 20, + color: data.titleColor, + ), ), - ), - const SizedBox( - width: 8, - ), - Expanded( - child: DidvanText( - data.title, - style: Theme.of(context).textTheme.headline3, - color: data.titleColor, - fontWeight: FontWeight.bold, + const SizedBox( + width: 8, ), - ), - ], - ), + Expanded( + child: DidvanText( + data.title!, + style: Theme.of(context).textTheme.headline3, + color: data.titleColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), const SizedBox( height: 12, ), diff --git a/lib/views/authentication/screens/password.dart b/lib/views/authentication/screens/password.dart index 391556b..69cdc42 100644 --- a/lib/views/authentication/screens/password.dart +++ b/lib/views/authentication/screens/password.dart @@ -1,8 +1,10 @@ import 'dart:developer'; +import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/providers/server_data_provider.dart'; import 'package:didvan/providers/user_provider.dart'; import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/authentication/authentication_state.dart'; import 'package:didvan/views/authentication/widgets/authentication_layout.dart'; import 'package:didvan/views/widgets/didvan/button.dart'; @@ -76,6 +78,25 @@ class _PasswordInputState extends State { log(token); await ServerDataProvider.getData(); Navigator.of(context).pushReplacementNamed(Routes.home); + _showResetPasswordDialog(); } } + + void _showResetPasswordDialog() { + ActionSheetUtils.openDialog( + data: ActionSheetData( + content: const DidvanText( + 'خوش آمدید!\nبرای امنیت بیشتر، رمز عبور خود را تغییر دهید.', + ), + title: 'تغییر رمز عبور', + onConfirmed: () => Navigator.of(ActionSheetUtils.context).pushNamed( + Routes.authenticaion, + arguments: true, + ), + confrimTitle: 'تغییر رمز عبور', + onDismissed: Navigator.of(ActionSheetUtils.context).pop, + dismissTitle: 'بعدا', + ), + ); + } } diff --git a/lib/views/authentication/screens/username.dart b/lib/views/authentication/screens/username.dart index e5a9187..6c5cf8c 100644 --- a/lib/views/authentication/screens/username.dart +++ b/lib/views/authentication/screens/username.dart @@ -39,6 +39,7 @@ class _UsernameInputState extends State { if (value.length < 4) { return 'نام کاربری نمی‌تواند از 4 کاراکتر کمتر باشد'; } + return null; }, onChanged: (value) { state.username = value; diff --git a/lib/views/home/direct/direct.dart b/lib/views/home/direct/direct.dart index 2af96ec..17ddee1 100644 --- a/lib/views/home/direct/direct.dart +++ b/lib/views/home/direct/direct.dart @@ -1,3 +1,4 @@ +import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/providers/server_data_provider.dart'; @@ -6,6 +7,7 @@ import 'package:didvan/views/home/direct/direct_state.dart'; import 'package:didvan/views/home/direct/widgets/message.dart'; import 'package:didvan/views/home/direct/widgets/message_box.dart'; import 'package:didvan/views/widgets/didvan/scaffold.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:flutter_spinkit/flutter_spinkit.dart'; @@ -66,6 +68,14 @@ class _DirectState extends State { sliver: SliverStateHandler( itemPadding: const EdgeInsets.only(bottom: 12), state: state, + enableEmptyState: state.messages.isEmpty, + emptyState: Padding( + padding: const EdgeInsets.only(bottom: 160), + child: EmptyState( + asset: Assets.emptyChat, + title: 'اولین پیام را بنویسید...', + ), + ), builder: (context, state, index) => Message( message: state.messages[index], ), diff --git a/lib/views/home/direct/widgets/audio_widget.dart b/lib/views/home/direct/widgets/audio_widget.dart index 5bb7ba7..a33827c 100644 --- a/lib/views/home/direct/widgets/audio_widget.dart +++ b/lib/views/home/direct/widgets/audio_widget.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:didvan/services/media/media.dart'; -import 'package:didvan/views/home/widgets/audio_slider.dart'; +import 'package:didvan/views/home/widgets/audio/audio_slider.dart'; import 'package:didvan/views/home/widgets/player_controller_button.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/home/direct/widgets/message_box.dart b/lib/views/home/direct/widgets/message_box.dart index 897cff5..c09d592 100644 --- a/lib/views/home/direct/widgets/message_box.dart +++ b/lib/views/home/direct/widgets/message_box.dart @@ -18,6 +18,7 @@ class MessageBox extends StatelessWidget { Consumer( builder: (context, state, child) => state.replyRadar != null ? _MessageBoxContainer( + isMessage: false, child: Padding( padding: const EdgeInsets.all(8.0), child: Row( @@ -53,6 +54,7 @@ class MessageBox extends StatelessWidget { : const SizedBox(), ), _MessageBoxContainer( + isMessage: true, child: Consumer( builder: (context, state, child) { if (state.isRecording) { @@ -71,12 +73,17 @@ class MessageBox extends StatelessWidget { class _MessageBoxContainer extends StatelessWidget { final Widget child; - const _MessageBoxContainer({Key? key, required this.child}) : super(key: key); + final bool isMessage; + const _MessageBoxContainer({ + Key? key, + required this.child, + required this.isMessage, + }) : super(key: key); @override Widget build(BuildContext context) { return Container( - height: 68, + height: isMessage ? 68 : null, decoration: BoxDecoration( border: Border( top: BorderSide( diff --git a/lib/views/home/hashtag/hashtag.dart b/lib/views/home/hashtag/hashtag.dart index 2e94ce0..24a9a7c 100644 --- a/lib/views/home/hashtag/hashtag.dart +++ b/lib/views/home/hashtag/hashtag.dart @@ -1,11 +1,12 @@ import 'package:didvan/models/tag.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/views/home/hashtag/hashtag_state.dart'; -import 'package:didvan/views/home/widgets/news_overview.dart'; -import 'package:didvan/views/home/widgets/radar_overview.dart'; +import 'package:didvan/views/home/widgets/overview/news.dart'; +import 'package:didvan/views/home/widgets/overview/radar.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:provider/provider.dart'; class Hashtag extends StatefulWidget { @@ -37,10 +38,16 @@ class _HashtagState extends State { placeholder: RadarOverview.placeholder, builder: (context, state, index) { index++; - if (index % 15 == 0 && index / 15 >= state.page) { - state.getTagItems(page: index ~/ 15 + 1); + if (index % 15 == 0 && state.lastPage != state.page) { + state.getTagItems(page: state.page + 1); } index--; + if (index == state.items.length) { + return SpinKitThreeBounce( + color: Theme.of(context).colorScheme.primary, + size: 24, + ); + } final item = state.items[index]; final type = item.type; if (type == 'radar') { @@ -57,8 +64,9 @@ class _HashtagState extends State { } return Container(); }, - childCount: state.items.length, - onRetry: () => state.getTagItems(page: 1), + childCount: + state.items.length + (state.page != state.lastPage ? 1 : 0), + onRetry: () => state.getTagItems(page: state.page), ), ) ], diff --git a/lib/views/home/hashtag/hashtag_state.dart b/lib/views/home/hashtag/hashtag_state.dart index a94cdbe..fc1e5e7 100644 --- a/lib/views/home/hashtag/hashtag_state.dart +++ b/lib/views/home/hashtag/hashtag_state.dart @@ -9,6 +9,7 @@ class HashtagState extends CoreProvier { late final int id; int page = 1; + int lastPage = 1; Future getTagItems({required int page}) async { this.page = page; diff --git a/lib/views/home/news/news.dart b/lib/views/home/news/news.dart index 2911204..421a045 100644 --- a/lib/views/home/news/news.dart +++ b/lib/views/home/news/news.dart @@ -8,7 +8,7 @@ import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/home/news/news_state.dart'; import 'package:didvan/views/home/widgets/date_picker_button.dart'; import 'package:didvan/views/home/widgets/logo_app_bar.dart'; -import 'package:didvan/views/home/widgets/news_overview.dart'; +import 'package:didvan/views/home/widgets/overview/news.dart'; import 'package:didvan/views/home/widgets/search_field.dart'; import 'package:didvan/views/widgets/item_title.dart'; import 'package:didvan/views/widgets/state_handlers/empty_result.dart'; @@ -57,7 +57,7 @@ class _NewsState extends State { builder: (context, state, index) { index += 2; if (index % 15 == 0 && state.lastPage != state.page) { - state.getNews(page: index ~/ 15 + 1); + state.getNews(page: state.page + 1); } index -= 2; if (index >= state.news.length) { diff --git a/lib/views/home/news/news_details/news_details_state.dart b/lib/views/home/news/news_details/news_details_state.dart index 879a178..279bc78 100644 --- a/lib/views/home/news/news_details/news_details_state.dart +++ b/lib/views/home/news/news_details/news_details_state.dart @@ -84,7 +84,7 @@ class NewsDetailsState extends CoreProvier { news.any((n) => newsItem != null && n != null && n.id == newsItem.id); void onCommentsChanged(int count) { - news.firstWhere((item) => item!.id == currentNews.id)!.comments = count; + news.firstWhere((item) => item?.id == currentNews.id)!.comments = count; notifyListeners(); } diff --git a/lib/views/home/radar/radar.dart b/lib/views/home/radar/radar.dart index 1a241b0..6058543 100644 --- a/lib/views/home/radar/radar.dart +++ b/lib/views/home/radar/radar.dart @@ -15,7 +15,7 @@ import 'package:didvan/views/home/radar/widgets/categories_list.dart'; import 'package:didvan/views/home/widgets/date_picker_button.dart'; import 'package:didvan/views/home/widgets/logo_app_bar.dart'; import 'package:didvan/utils/action_sheet.dart'; -import 'package:didvan/views/home/widgets/radar_overview.dart'; +import 'package:didvan/views/home/widgets/overview/radar.dart'; import 'package:didvan/views/home/widgets/search_field.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/checkbox.dart'; @@ -130,7 +130,7 @@ class _RadarState extends State { builder: (context, state, index) { index += 2; if (index % 15 == 0 && state.lastPage != state.page) { - state.getRadars(page: index ~/ 15 + 1); + state.getRadars(page: state.page + 1); } index -= 2; if (index >= state.radars.length) { diff --git a/lib/views/home/radar/radar_details/radar_details_state.dart b/lib/views/home/radar/radar_details/radar_details_state.dart index 2095cec..37ebb42 100644 --- a/lib/views/home/radar/radar_details/radar_details_state.dart +++ b/lib/views/home/radar/radar_details/radar_details_state.dart @@ -116,7 +116,7 @@ class RadarDetailsState extends CoreProvier { radars.any((r) => radar != null && r != null && r.id == radar.id); void onCommentsChanged(int count) { - radars.firstWhere((radar) => radar!.id == currentRadar.id)!.comments = + radars.firstWhere((radar) => radar?.id == currentRadar.id)!.comments = count; notifyListeners(); } diff --git a/lib/views/home/radar/widgets/category_item.dart b/lib/views/home/radar/widgets/category_item.dart index 081b152..faee630 100644 --- a/lib/views/home/radar/widgets/category_item.dart +++ b/lib/views/home/radar/widgets/category_item.dart @@ -66,10 +66,10 @@ class CategoryItem extends StatelessWidget { isVisible: !isColapsed, child: Container( width: !_useWebMobileLayout(context) - ? _width(context) / 1.5 + ? _width(context) / 2 : ds.width / 5, height: !_useWebMobileLayout(context) - ? _width(context) / 1.5 + ? _width(context) / 2 : ds.width / 5, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, diff --git a/lib/views/home/settings/bookmarks/bookmark_state.dart b/lib/views/home/settings/bookmarks/bookmark_state.dart index 1807179..063dc4b 100644 --- a/lib/views/home/settings/bookmarks/bookmark_state.dart +++ b/lib/views/home/settings/bookmarks/bookmark_state.dart @@ -9,20 +9,27 @@ class BookmarksState extends CoreProvier { final List bookmarks = []; String search = ''; String lastSearch = ''; + int page = 1; + int lastPage = 1; bool get searching => search != ''; - Future getBookmarks() async { + Future getBookmarks({required int page}) async { if (search != '') { lastSearch = search; } + if (page == 1) { + bookmarks.clear(); + } + this.page = page; appState = AppState.busy; - final service = RequestService(RequestHelper.bookmarks()); + final service = + RequestService(RequestHelper.bookmarks(page: page, search: search)); await service.httpGet(); if (service.isSuccess) { + lastPage = service.result['lastPage']; final marks = service.result['contents']; - bookmarks.clear(); for (var i = 0; i < marks.length; i++) { bookmarks.add(OverviewData.fromJson(marks[i])); } diff --git a/lib/views/home/settings/bookmarks/bookmarks.dart b/lib/views/home/settings/bookmarks/bookmarks.dart index 1cb736a..520d880 100644 --- a/lib/views/home/settings/bookmarks/bookmarks.dart +++ b/lib/views/home/settings/bookmarks/bookmarks.dart @@ -6,7 +6,7 @@ import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/home/settings/bookmarks/bookmark_state.dart'; import 'package:didvan/views/home/widgets/menu_item.dart'; -import 'package:didvan/views/home/widgets/multitype_overview.dart'; +import 'package:didvan/views/home/widgets/overview/multitype.dart'; import 'package:didvan/views/home/widgets/search_field.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; @@ -33,7 +33,7 @@ class _BookmarksState extends State { @override void initState() { Future.delayed(Duration.zero, () { - context.read().getBookmarks(); + context.read().getBookmarks(page: 1); }); super.initState(); } @@ -74,14 +74,14 @@ class _BookmarksState extends State { ), const DidvanDivider(), MenuItem( - onTap: () => _onCategorySelected('videos'), + onTap: () => _onCategorySelected('video'), title: 'ویدئو‌ها', icon: DidvanIcons.video_regular, iconSize: 24, ), const DidvanDivider(), MenuItem( - onTap: () => _onCategorySelected('podcasts'), + onTap: () => _onCategorySelected('podcast'), title: 'پادکست‌ها', icon: DidvanIcons.podcast_regular, iconSize: 24, @@ -103,26 +103,33 @@ class _BookmarksState extends State { SliverStateHandler( state: state, centerEmptyState: state.searching, - builder: (context, state, index) => MultitypeOverview( - item: state.bookmarks[index], - onMarkChanged: state.onMarkChanged, - hasUnmarkConfirmation: true, - ), + builder: (context, state, index) { + index++; + if (index % 15 == 0 && state.lastPage != state.page) { + state.getBookmarks(page: state.page + 1); + } + index--; + return MultitypeOverview( + item: state.bookmarks[index], + onMarkChanged: state.onMarkChanged, + hasUnmarkConfirmation: true, + ); + }, placeholder: MultitypeOverview.placeholder, itemPadding: const EdgeInsets.only(bottom: 8), emptyState: state.searching ? EmptyResult(onNewSearch: _focuseNode.requestFocus) : const EmptyList(), enableEmptyState: state.bookmarks.isEmpty, - childCount: state.bookmarks.length, - onRetry: state.getBookmarks, + childCount: + state.bookmarks.length + (state.page != state.lastPage ? 1 : 0), + onRetry: () => state.getBookmarks(page: state.page), ), ], ); } void _onCategorySelected(String type) { - if (type != 'radar' && type != 'news') return; FocusScope.of(context).unfocus(); Navigator.of(context).pushNamed(Routes.filteredBookmarks, arguments: type); } @@ -135,7 +142,7 @@ class _BookmarksState extends State { _timer?.cancel(); _timer = Timer(const Duration(seconds: 1), () { state.search = value; - state.getBookmarks(); + state.getBookmarks(page: 1); }); } } 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 f9a5454..2291e1a 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,7 @@ 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/news_overview.dart'; -import 'package:didvan/views/home/widgets/radar_overview.dart'; +import 'package:didvan/views/home/widgets/overview/news.dart'; +import 'package:didvan/views/home/widgets/overview/radar.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'; @@ -20,7 +20,7 @@ class _FilteredBookmarksState extends State { void initState() { Future.delayed( Duration.zero, - context.read().getBookmarks, + () => context.read().getBookmarks(page: 1), ); super.initState(); } @@ -54,6 +54,11 @@ class _FilteredBookmarksState extends State { placeholder: RadarOverview.placeholder, emptyState: const EmptyList(), builder: (context, state, index) { + index++; + if (index % 15 == 0 && state.lastPage != state.page) { + state.getBookmarks(page: state.page + 1); + } + index--; if (state.type == 'radar') { return RadarOverview( radar: state.bookmarks[index], @@ -69,7 +74,7 @@ class _FilteredBookmarksState extends State { ); }, childCount: state.bookmarks.length, - onRetry: () => state.getBookmarks(), + onRetry: () => state.getBookmarks(page: state.page), ), ), ], 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 697ee3a..7aa0463 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 @@ -10,24 +10,42 @@ class FilteredBookmarksState extends CoreProvier { String lastSearch = ''; final List bookmarks = []; final String type; + int page = 1; + int lastPage = 1; FilteredBookmarksState(this.type); bool get searching => search != ''; - Future getBookmarks() async { + Future getBookmarks({required int page}) async { if (search != '') { lastSearch = search; } + if (page == 1) { + bookmarks.clear(); + } + this.page = page; appState = AppState.busy; + String typeString = ''; + if (type == 'video' || type == 'podcast') { + typeString = 'studios'; + } else if (type == 'news') { + typeString = type; + } else { + typeString = type + 's'; + } final service = RequestService( - RequestHelper.bookmarks(type: type == 'news' ? type : type + 's'), + RequestHelper.bookmarks( + type: typeString, + page: page, + studioType: type == 'podcast' || type == 'video' ? type : null, + ), ); await service.httpGet(); if (service.isSuccess) { - final marks = service.result[type != 'news' ? type + 's' : type]; - bookmarks.clear(); + lastPage = service.result['lastPage']; + final marks = service.result[typeString]; for (var i = 0; i < marks.length; i++) { bookmarks.add(OverviewData.fromJson(marks[i])); } diff --git a/lib/views/home/settings/settings.dart b/lib/views/home/settings/settings.dart index 7753d26..63c4439 100644 --- a/lib/views/home/settings/settings.dart +++ b/lib/views/home/settings/settings.dart @@ -87,7 +87,7 @@ class Settings extends StatelessWidget { MenuItem( icon: DidvanIcons.didvan_solid, title: 'معرفی دیدوان', - onTap: () => Navigator.of(context).pushNamed(Routes.aboutUs), + onTap: () => launch('https://didvan.app/'), ), const DidvanDivider(), MenuItem( @@ -101,14 +101,14 @@ class Settings extends StatelessWidget { MenuItem( icon: DidvanIcons.alert_regular, title: 'حریم خصوصی', - onTap: () => {}, + onTap: () => launch('https://didvan.app/'), ), ], ), ), const SizedBox(height: 16), DidvanText( - 'نسخه نرم‌افزار: آزمایشی', + 'نسخه نرم‌افزار: 1.1.4', style: Theme.of(context).textTheme.caption, ), ], diff --git a/lib/views/home/studio/podcast_details/podcast_details.dart b/lib/views/home/studio/podcast_details/podcast_details.dart deleted file mode 100644 index 140d8d0..0000000 --- a/lib/views/home/studio/podcast_details/podcast_details.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class PodcastDetails extends StatelessWidget { - const PodcastDetails({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Material( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), - ), - ), - ); - } -} 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..20ce528 --- /dev/null +++ b/lib/views/home/studio/studio_details/studio_details.dart @@ -0,0 +1,170 @@ +import 'dart:io'; + +import 'package:didvan/config/design_config.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/widgets/didvan/scaffold.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; + + @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( + 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( + ''' + + + + + + + ${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, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} 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..0a6205b --- /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/media/media.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class StudioDetailsState extends CoreProvier { + final List studios = []; + late int initialIndex; + late StudioRequestArgs args; + int _selectedDetailsIndex = 0; + bool isFetchingNewItem = false; + final List relatedQueue = []; + + int _currentIndex = 0; + int get currentIndex => _currentIndex; + + int get selectedDetailsIndex => _selectedDetailsIndex; + set selectedDetailsIndex(int value) { + _selectedDetailsIndex = value; + notifyListeners(); + } + + StudioDetailsData get currentStudio { + try { + return studios[_currentIndex]!; + } catch (e) { + return studios[_currentIndex + 1]!; + } + } + + Future getStudioDetails(int id, + {bool? isForward, StudioRequestArgs? args}) async { + if (args != null) { + this.args = args; + } + if (isForward == null) { + appState = AppState.busy; + } else { + isFetchingNewItem = true; + notifyListeners(); + } + final service = RequestService(RequestHelper.studioDetails(id, this.args)); + await service.httpGet(); + if (service.isSuccess) { + final result = service.result; + final studio = StudioDetailsData.fromJson(result['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) { + 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(); + } +} diff --git a/lib/views/home/studio/studio_details/widgets/studio_details.dart b/lib/views/home/studio/studio_details/widgets/studio_details.dart new file mode 100644 index 0000000..059b4cc --- /dev/null +++ b/lib/views/home/studio/studio_details/widgets/studio_details.dart @@ -0,0 +1,97 @@ +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_state.dart b/lib/views/home/studio/studio_state.dart index 28ebd1e..18b7dc2 100644 --- a/lib/views/home/studio/studio_state.dart +++ b/lib/views/home/studio/studio_state.dart @@ -1,7 +1,93 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/studio.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 = []; + + String? search; + String? lastSearch; + int page = 1; + int lastPage = 1; + int selectedSortTypeIndex = 0; - bool videosSelected = true; + bool _videosSelected = true; + + bool get videosSelected => _videosSelected; + + 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); + }); + } + + 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: type, + search: search, + 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(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/studio/video_details/video_details.dart b/lib/views/home/studio/video_details/video_details.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/views/home/studio/widgets/tab_bar.dart b/lib/views/home/studio/widgets/tab_bar.dart index 7e11744..3d915a8 100644 --- a/lib/views/home/studio/widgets/tab_bar.dart +++ b/lib/views/home/studio/widgets/tab_bar.dart @@ -14,14 +14,15 @@ class StudioTabBar extends StatelessWidget { @override Widget build(BuildContext context) { final state = context.watch(); - return Container( + return AnimatedContainer( + duration: DesignConfig.lowAnimationDuration, margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(4), decoration: BoxDecoration( border: Border.all( color: state.videosSelected ? Theme.of(context).colorScheme.secondary - : Theme.of(context).primaryColor, + : Theme.of(context).colorScheme.primary, ), borderRadius: DesignConfig.lowBorderRadius, ), @@ -32,7 +33,7 @@ class StudioTabBar extends StatelessWidget { icon: DidvanIcons.video_solid, selectedColor: Theme.of(context).colorScheme.secondary, title: 'ویدئو', - onTap: () {}, + onTap: () => state.videosSelected = true, isSelected: state.videosSelected, ), ), @@ -46,7 +47,7 @@ class StudioTabBar extends StatelessWidget { icon: DidvanIcons.podcast_solid, selectedColor: Theme.of(context).colorScheme.focusedBorder, title: 'پادکست', - onTap: () {}, + onTap: () => state.videosSelected = false, isSelected: !state.videosSelected, ), ), diff --git a/lib/views/home/widgets/audio/audio_player_widget.dart b/lib/views/home/widgets/audio/audio_player_widget.dart new file mode 100644 index 0000000..d369b10 --- /dev/null +++ b/lib/views/home/widgets/audio/audio_player_widget.dart @@ -0,0 +1,174 @@ +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/widgets/audio/audio_slider.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +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'; + +class AudioPlayerWidget extends StatelessWidget { + final StudioDetailsData podcast; + const AudioPlayerWidget({Key? key, required this.podcast}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + height: 3, + width: 50, + color: Theme.of(context).colorScheme.hint, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + 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, + duration: podcast.duration, + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanIconButton( + icon: DidvanIcons.sleep_timer_regular, + onPressed: () {}, + ), + Column( + children: [ + DidvanIconButton( + size: 32, + icon: DidvanIcons.media_forward_solid, + onPressed: () { + MediaService.audioPlayer.seek( + Duration( + seconds: + MediaService.audioPlayer.position.inSeconds + 30, + ), + ); + }, + ), + const DidvanText('30', isEnglishFont: true), + ], + ), + _PlayPouseAnimatedIcon( + audioSource: podcast.media, + ), + Column( + children: [ + DidvanIconButton( + size: 32, + icon: DidvanIcons.media_backward_solid, + onPressed: () { + MediaService.audioPlayer.seek( + Duration( + seconds: + MediaService.audioPlayer.position.inSeconds - 10, + ), + ); + }, + ), + const DidvanText('10', isEnglishFont: true), + ], + ), + BookmarkButton( + gestureSize: 48, + value: podcast.marked, + onMarkChanged: (value) {}, + ), + ], + ), + ], + ), + ); + } +} + +class _PlayPouseAnimatedIcon extends StatefulWidget { + final String audioSource; + const _PlayPouseAnimatedIcon({Key? key, required this.audioSource}) + : super(key: key); + + @override + State<_PlayPouseAnimatedIcon> createState() => __PlayPouseAnimatedIconState(); +} + +class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> + with SingleTickerProviderStateMixin { + late final AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: DesignConfig.lowAnimationDuration, + ); + if (MediaService.audioPlayer.playing) { + _animationController.forward(); + } + } + + @override + Widget build(BuildContext context) { + return InkWrapper( + borderRadius: BorderRadius.circular(100), + onPressed: () { + MediaService.handleAudioPlayback( + audioSource: widget.audioSource, + isVoiceMessage: false, + ); + if (MediaService.audioPlayer.playing) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.title, + shape: BoxShape.circle, + ), + child: AnimatedIcon( + size: 40, + color: Theme.of(context).colorScheme.surface, + icon: AnimatedIcons.play_pause, + progress: _animationController, + ), + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} diff --git a/lib/views/home/widgets/audio/audio_slider.dart b/lib/views/home/widgets/audio/audio_slider.dart new file mode 100644 index 0000000..9c80349 --- /dev/null +++ b/lib/views/home/widgets/audio/audio_slider.dart @@ -0,0 +1,63 @@ +import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:flutter/material.dart'; + +class AudioSlider extends StatelessWidget { + final String tag; + final bool showTimer; + final int? duration; + final bool disableThumb; + const AudioSlider({ + Key? key, + required this.tag, + this.showTimer = false, + this.duration, + this.disableThumb = false, + }) : super(key: key); + + bool get _isPlaying => MediaService.audioPlayerTag == tag; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: MediaService.audioPlayerTag != tag, + child: Directionality( + textDirection: TextDirection.ltr, + child: StreamBuilder( + stream: _isPlaying ? MediaService.audioPlayer.positionStream : null, + builder: (context, snapshot) => ProgressBar( + thumbColor: Theme.of(context).colorScheme.title, + progressBarColor: DesignConfig.isDark + ? Theme.of(context).colorScheme.title + : Theme.of(context).colorScheme.primary, + baseBarColor: Theme.of(context).colorScheme.border, + bufferedBarColor: Theme.of(context).colorScheme.splash, + total: MediaService.audioPlayer.duration ?? + Duration(seconds: duration ?? 0), + progress: snapshot.data ?? Duration.zero, + buffered: _isPlaying + ? MediaService.audioPlayer.bufferedPosition + : Duration.zero, + thumbRadius: disableThumb ? 0 : 6, + barHeight: 3, + timeLabelTextStyle: TextStyle( + fontSize: showTimer ? null : 0, + height: showTimer ? 3 : 0, + fontFamily: DesignConfig.fontFamily.replaceAll( + '-FA', + '', + ), + ), + onSeek: (value) => _onSeek(value.inMilliseconds), + ), + ), + ), + ); + } + + void _onSeek(int value) { + MediaService.audioPlayer.seek(Duration(milliseconds: value)); + } +} diff --git a/lib/views/home/widgets/audio_visualizer.dart b/lib/views/home/widgets/audio/audio_visualizer.dart similarity index 100% rename from lib/views/home/widgets/audio_visualizer.dart rename to lib/views/home/widgets/audio/audio_visualizer.dart diff --git a/lib/views/home/widgets/audio_slider.dart b/lib/views/home/widgets/audio_slider.dart deleted file mode 100644 index b9edf70..0000000 --- a/lib/views/home/widgets/audio_slider.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:audio_video_progress_bar/audio_video_progress_bar.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); - - bool get _isPlaying => MediaService.audioPlayerTag == tag; - - @override - Widget build(BuildContext context) { - return IgnorePointer( - ignoring: MediaService.audioPlayerTag != tag, - child: Directionality( - textDirection: TextDirection.ltr, - child: StreamBuilder( - stream: _isPlaying ? MediaService.audioPlayer.positionStream : null, - builder: (context, snapshot) { - return ProgressBar( - total: MediaService.audioPlayer.duration ?? Duration.zero, - progress: snapshot.data ?? Duration.zero, - buffered: - _isPlaying ? MediaService.audioPlayer.bufferedPosition : null, - thumbRadius: 6, - barHeight: 3, - timeLabelTextStyle: const TextStyle(fontSize: 0), - onSeek: (value) => _onSeek(value.inMilliseconds), - ); - }, - ), - ), - ); - } - - void _onSeek(int value) { - MediaService.audioPlayer.seek(Duration(milliseconds: value)); - } -} diff --git a/lib/views/home/widgets/bnb.dart b/lib/views/home/widgets/bnb.dart index 4c78de0..a7651be 100644 --- a/lib/views/home/widgets/bnb.dart +++ b/lib/views/home/widgets/bnb.dart @@ -2,10 +2,16 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.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/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'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class DidvanBNB extends StatelessWidget { final int currentTabIndex; @@ -15,6 +21,9 @@ class DidvanBNB extends StatelessWidget { {Key? key, required this.currentTabIndex, required this.onTabChanged}) : super(key: key); + bool get _enablePlayerController => + MediaService.currentPodcast != null || MediaService.audioPlayer.playing; + @override Widget build(BuildContext context) { return StreamBuilder( @@ -22,29 +31,89 @@ class DidvanBNB extends StatelessWidget { builder: (context, snapshot) { return Stack( children: [ - AnimatedContainer( - duration: DesignConfig.lowAnimationDuration, - height: snapshot.data == true ? 120 : 72, - decoration: BoxDecoration( - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.focused - : Theme.of(context).colorScheme.navigation, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(16), - ), - ), - child: Row( - children: [ - const DidvanIconButton( - icon: DidvanIcons.close_regular, - gestureSize: 24, - onPressed: MediaService.resetAudioPlayer, + GestureDetector( + onTap: () => _showPlayerBottomSheet(context), + child: AnimatedContainer( + padding: const EdgeInsets.only(top: 12), + duration: DesignConfig.lowAnimationDuration, + height: _enablePlayerController ? 120 : 72, + decoration: BoxDecoration( + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.navigation, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), ), - const SizedBox(width: 16), - if (MediaService.audioPlayerCover != null) - SkeletonImage(imageUrl: MediaService.audioPlayerCover!), - const SizedBox(width: 16), - ], + ), + child: !_enablePlayerController + ? const SizedBox() + : SizedBox( + height: 48, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 16, + ), + child: DidvanIconButton( + icon: DidvanIcons.close_regular, + color: DesignConfig.isDark + ? null + : Theme.of(context).colorScheme.secondCTA, + gestureSize: 28, + onPressed: MediaService.resetAudioPlayer, + ), + ), + SkeletonImage( + imageUrl: MediaService.currentPodcast!.image, + width: 32, + height: 32, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + MediaService.currentPodcast!.title, + color: DesignConfig.isDark + ? null + : Theme.of(context) + .colorScheme + .secondCTA, + ), + AudioSlider( + disableThumb: true, + tag: MediaService.audioPlayerTag!, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 16, + ), + child: DidvanIconButton( + gestureSize: 28, + color: DesignConfig.isDark + ? null + : Theme.of(context).colorScheme.secondCTA, + icon: snapshot.data! + ? DidvanIcons.pause_solid + : DidvanIcons.play_solid, + onPressed: () { + MediaService.handleAudioPlayback( + audioSource: MediaService.audioPlayerTag, + ); + }, + ), + ), + ], + ), + ), ), ), Positioned( @@ -105,6 +174,55 @@ class DidvanBNB extends StatelessWidget { ); }); } + + void _showPlayerBottomSheet(BuildContext context) { + final sheetKey = GlobalKey(); + bool isExpanded = false; + final detailsState = 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, + 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: const StudioDetailsWidget(), + ), + ), + ); + } } class _NavBarItem extends StatelessWidget { diff --git a/lib/views/home/widgets/bookmark_button.dart b/lib/views/home/widgets/bookmark_button.dart index 4de2621..ca98b06 100644 --- a/lib/views/home/widgets/bookmark_button.dart +++ b/lib/views/home/widgets/bookmark_button.dart @@ -8,14 +8,14 @@ import 'package:flutter/material.dart'; class BookmarkButton extends StatefulWidget { final bool value; final void Function(bool value) onMarkChanged; - final bool bigGestureSize; final bool askForConfirmation; + final double gestureSize; const BookmarkButton({ Key? key, required this.value, - this.bigGestureSize = false, required this.onMarkChanged, this.askForConfirmation = false, + required this.gestureSize, }) : super(key: key); @override @@ -40,7 +40,7 @@ class _BookmarkButtonState extends State { @override Widget build(BuildContext context) { return DidvanIconButton( - gestureSize: widget.bigGestureSize ? 32 : 24, + gestureSize: widget.gestureSize, icon: _value ? DidvanIcons.bookmark_solid : DidvanIcons.bookmark_regular, onPressed: () async { bool confirm = false; 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/floating_navigation_bar.dart b/lib/views/home/widgets/floating_navigation_bar.dart index 3cc0323..28a88e4 100644 --- a/lib/views/home/widgets/floating_navigation_bar.dart +++ b/lib/views/home/widgets/floating_navigation_bar.dart @@ -112,7 +112,7 @@ class _FloatingNavigationBarState extends State { Navigator.of(context).pop(); } }, - bigGestureSize: true, + gestureSize: 32, ), SizedBox( width: 60, @@ -151,7 +151,7 @@ class _FloatingNavigationBarState extends State { Navigator.of(context).pop(); } }, - bigGestureSize: true, + gestureSize: 32, ), if (widget.isRadar) DidvanIconButton( @@ -233,7 +233,19 @@ class _FloatingNavigationBarState extends State { Navigator.of(context).pop(); Navigator.of(context).pushNamed( Routes.direct, - arguments: {}, + arguments: { + 'radarAttachment': RadarAttachment( + id: widget.item.id, + title: widget.item.title, + description: widget.item.contents.first.text, + timeToRead: widget.item.timeToRead, + image: widget.item.image, + forManagers: widget.item.forManagers, + categories: widget.item.categories, + createdAt: widget.item.createdAt, + ), + 'type': 'پشتیبانی' + }, ); }, icon: DidvanIcons.description_regular, diff --git a/lib/views/home/widgets/multitype_overview.dart b/lib/views/home/widgets/overview/multitype.dart similarity index 100% rename from lib/views/home/widgets/multitype_overview.dart rename to lib/views/home/widgets/overview/multitype.dart diff --git a/lib/views/home/widgets/news_overview.dart b/lib/views/home/widgets/overview/news.dart similarity index 99% rename from lib/views/home/widgets/news_overview.dart rename to lib/views/home/widgets/overview/news.dart index e998acf..ac163c2 100644 --- a/lib/views/home/widgets/news_overview.dart +++ b/lib/views/home/widgets/overview/news.dart @@ -79,6 +79,7 @@ class NewsOverview extends StatelessWidget { ], ), BookmarkButton( + gestureSize: 24, value: news.marked, onMarkChanged: (value) => onMarkChanged(news.id, value), askForConfirmation: hasUnmarkConfirmation, diff --git a/lib/views/home/widgets/podcast_overview.dart b/lib/views/home/widgets/overview/podcast.dart similarity index 50% rename from lib/views/home/widgets/podcast_overview.dart rename to lib/views/home/widgets/overview/podcast.dart index fa626fb..1a243df 100644 --- a/lib/views/home/widgets/podcast_overview.dart +++ b/lib/views/home/widgets/overview/podcast.dart @@ -1,87 +1,90 @@ +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/routes/routes.dart'; +import 'package:didvan/models/requests/studio.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'; 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'; +import 'package:provider/provider.dart'; class PodcastOverview extends StatelessWidget { - final OverviewData news; - final NewsRequestArgs? newsRequestArgs; + final OverviewData podcast; 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.onMarkChanged, - this.newsRequestArgs, - this.hasUnmarkConfirmation = false, + this.studioRequestArgs, }) : super(key: key); @override Widget build(BuildContext context) { return DidvanCard( - onTap: () => Navigator.of(context).pushNamed( - Routes.newsDetails, - arguments: { - 'onMarkChanged': onMarkChanged, - 'id': news.id, - 'args': newsRequestArgs, - 'hasUnmarkConfirmation': hasUnmarkConfirmation, - }, - ), + onTap: () { + context + .read() + .getStudioDetails(podcast.id, args: studioRequestArgs); + }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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, + gestureSize: 24, + value: podcast.marked, + onMarkChanged: (value) => onMarkChanged(podcast.id, value), ), ], ), @@ -95,7 +98,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 +112,7 @@ class PodcastOverview extends StatelessWidget { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), const ShimmerPlaceholder( height: 16, width: double.infinity, @@ -117,19 +120,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/radar_overview.dart b/lib/views/home/widgets/overview/radar.dart similarity index 98% rename from lib/views/home/widgets/radar_overview.dart rename to lib/views/home/widgets/overview/radar.dart index 5616994..cd88ff3 100644 --- a/lib/views/home/widgets/radar_overview.dart +++ b/lib/views/home/widgets/overview/radar.dart @@ -97,11 +97,13 @@ class RadarOverview extends StatelessWidget { DidvanText( radar.description, maxLines: 3, + overflow: TextOverflow.ellipsis, ), const DidvanDivider(), Row( children: [ BookmarkButton( + gestureSize: 24, value: radar.marked, onMarkChanged: (value) => onMarkChanged(radar.id, value), askForConfirmation: hasUnmarkConfirmation, diff --git a/lib/views/home/widgets/overview/video.dart b/lib/views/home/widgets/overview/video.dart new file mode 100644 index 0000000..3150b34 --- /dev/null +++ b/lib/views/home/widgets/overview/video.dart @@ -0,0 +1,161 @@ +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/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'; +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'; +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, + }) : 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, + 'state': context.read(), + }, + ), + 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( + gestureSize: 24, + 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/splash/splash.dart b/lib/views/splash/splash.dart index c0d05cd..72e3b26 100644 --- a/lib/views/splash/splash.dart +++ b/lib/views/splash/splash.dart @@ -110,7 +110,15 @@ class _SplashState extends State { if (token != null) { log(token); RequestService.token = token; - await userProvider.getUserInfo(); + final result = await userProvider.getUserInfo(); + if (!result) { + StorageService.delete(key: 'token'); + Navigator.of(context).pushNamedAndRemoveUntil( + Routes.splash, + (_) => false, + ); + return; + } await ServerDataProvider.getData(); } Navigator.of(context).pushReplacementNamed( diff --git a/lib/views/widgets/didvan/app_bar.dart b/lib/views/widgets/didvan/app_bar.dart index 1de49fe..f7f00e3 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( @@ -53,6 +52,7 @@ class DidvanAppBar extends StatelessWidget { appBarData.title!, style: Theme.of(context).textTheme.headline3, color: Theme.of(context).colorScheme.title, + overflow: TextOverflow.ellipsis, ), if (appBarData.subtitle != null) DidvanText( diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart index f0a83d3..fd3a8e9 100644 --- a/lib/views/widgets/didvan/page_view.dart +++ b/lib/views/widgets/didvan/page_view.dart @@ -3,7 +3,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/utils/date_time.dart'; -import 'package:didvan/views/home/widgets/multitype_overview.dart'; +import 'package:didvan/views/home/widgets/overview/multitype.dart'; import 'package:didvan/views/home/widgets/tag_item.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; @@ -14,6 +14,7 @@ import 'package:didvan/views/widgets/item_title.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:url_launcher/url_launcher.dart'; class DidvanPageView extends StatefulWidget { final List items; @@ -183,9 +184,11 @@ class _DidvanPageViewState extends State { if (content.text != null) { return Html( data: content.text, + onAnchorTap: (href, context, map, element) => launch(href!), style: { '*': Style( direction: TextDirection.rtl, + textAlign: TextAlign.right, lineHeight: LineHeight.percent(135), margin: EdgeInsets.zero, padding: EdgeInsets.zero, diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index db75ac9..0dabcf2 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,18 +40,15 @@ 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) - + statusBarHeight, backgroundColor: widget.backgroundColor ?? Theme.of(context).colorScheme.background, automaticallyImplyLeading: false, pinned: true, - flexibleSpace: DidvanAppBar(appBarData: widget.appBarData), - ), - if (!widget.reverse) - const SliverToBoxAdapter( - child: SizedBox(height: 16), + flexibleSpace: DidvanAppBar(appBarData: widget.appBarData!), ), if (widget.children != null) SliverPadding( @@ -79,9 +76,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/lib/views/widgets/didvan/text.dart b/lib/views/widgets/didvan/text.dart index 1ea5ec8..b2e840b 100644 --- a/lib/views/widgets/didvan/text.dart +++ b/lib/views/widgets/didvan/text.dart @@ -34,7 +34,9 @@ class DidvanText extends StatelessWidget { fontWeight: fontWeight, fontSize: fontSize, )).copyWith( - fontFamily: isEnglishFont ? DesignConfig.fontFamily : null, + fontFamily: isEnglishFont + ? DesignConfig.fontFamily.replaceAll('-FA', '') + : null, height: 1.7, ), overflow: overflow, diff --git a/pubspec.lock b/pubspec.lock index 1ae7618..ab49287 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -127,6 +127,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4+1" + expandable_bottom_sheet: + dependency: "direct main" + description: + name: expandable_bottom_sheet + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1+1" fake_async: dependency: transitive description: @@ -566,7 +573,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 +789,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 8c847b0..7bae68f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.1.0+1 +version: 1.2.0+1 environment: sdk: ">=2.12.0 <3.0.0" @@ -62,6 +62,8 @@ dependencies: image_cropper: ^1.5.0 firebase_messaging: ^11.2.8 firebase_core: ^1.13.1 + webview_flutter: ^3.0.1 + expandable_bottom_sheet: ^1.1.1+1 dev_dependencies: