diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 34d2e7f..0e7557a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,10 +3,14 @@ + + android:usesCleartextTraffic="true" + android:requestLegacyExternalStorage="true"> + + + android:name="com.yalantis.ucrop.UCropActivity" + android:screenOrientation="portrait" + android:theme="@style/Theme.AppCompat.Light.NoActionBar"/> diff --git a/ios/Podfile b/ios/Podfile index 9411102..313ea4a 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '10.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 89b36ba..f1504a2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,33 +1,42 @@ PODS: - - audio_session (0.0.1): + - assets_audio_player (0.0.1): - Flutter - - Firebase/CoreOnly (8.11.0): - - FirebaseCore (= 8.11.0) - - Firebase/Messaging (8.11.0): + - assets_audio_player_web (0.0.1): + - Flutter + - better_player (0.0.1): + - Cache (~> 6.0.0) + - Flutter + - GCDWebServer + - HLSCachingReverseProxyServer + - PINCache + - Cache (6.0.0) + - Firebase/CoreOnly (8.14.0): + - FirebaseCore (= 8.14.0) + - Firebase/Messaging (8.14.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 8.11.0) - - firebase_core (1.13.1): - - Firebase/CoreOnly (= 8.11.0) + - FirebaseMessaging (~> 8.14.0) + - firebase_core (1.14.0): + - Firebase/CoreOnly (= 8.14.0) - Flutter - - firebase_messaging (11.2.8): - - Firebase/Messaging (= 8.11.0) + - firebase_messaging (11.2.12): + - Firebase/Messaging (= 8.14.0) - firebase_core - Flutter - - FirebaseCore (8.11.0): + - FirebaseCore (8.14.0): - FirebaseCoreDiagnostics (~> 8.0) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/Logger (~> 7.7) - - FirebaseCoreDiagnostics (8.12.0): + - FirebaseCoreDiagnostics (8.14.0): - GoogleDataTransport (~> 9.1) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/Logger (~> 7.7) - nanopb (~> 2.30908.0) - - FirebaseInstallations (8.12.0): + - FirebaseInstallations (8.14.0): - FirebaseCore (~> 8.0) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/UserDefaults (~> 7.7) - PromisesObjC (< 3.0, >= 1.2) - - FirebaseMessaging (8.11.0): + - FirebaseMessaging (8.14.0): - FirebaseCore (~> 8.0) - FirebaseInstallations (~> 8.0) - GoogleDataTransport (~> 9.1) @@ -44,6 +53,9 @@ PODS: - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) + - GCDWebServer (3.5.4): + - GCDWebServer/Core (= 3.5.4) + - GCDWebServer/Core (3.5.4) - GoogleDataTransport (9.1.2): - GoogleUtilities/Environment (~> 7.2) - nanopb (~> 2.30908.0) @@ -65,13 +77,14 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/UserDefaults (7.7.0): - GoogleUtilities/Logger + - HLSCachingReverseProxyServer (0.1.0): + - GCDWebServer (~> 3.5) + - PINCache (>= 3.0.1-beta.3) - image_cropper (0.0.4): - Flutter - TOCropViewController (~> 2.6.1) - image_picker (0.0.1): - Flutter - - just_audio (0.0.1): - - Flutter - nanopb (2.30908.0): - nanopb/decode (= 2.30908.0) - nanopb/encode (= 2.30908.0) @@ -79,6 +92,16 @@ PODS: - nanopb/encode (2.30908.0) - path_provider_ios (0.0.1): - Flutter + - permission_handler_apple (9.0.4): + - Flutter + - PINCache (3.0.3): + - PINCache/Arc-exception-safe (= 3.0.3) + - PINCache/Core (= 3.0.3) + - PINCache/Arc-exception-safe (3.0.3): + - PINCache/Core + - PINCache/Core (3.0.3): + - PINOperation (~> 1.2.1) + - PINOperation (1.2.1) - PromisesObjC (2.0.0) - record (0.0.1): - Flutter @@ -88,11 +111,15 @@ PODS: - TOCropViewController (2.6.1) - url_launcher_ios (0.0.1): - Flutter + - wakelock (0.0.1): + - Flutter - webview_flutter_wkwebview (0.0.1): - Flutter DEPENDENCIES: - - audio_session (from `.symlinks/plugins/audio_session/ios`) + - assets_audio_player (from `.symlinks/plugins/assets_audio_player/ios`) + - assets_audio_player_web (from `.symlinks/plugins/assets_audio_player_web/ios`) + - better_player (from `.symlinks/plugins/better_player/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) @@ -100,30 +127,40 @@ DEPENDENCIES: - flutter_vibrate (from `.symlinks/plugins/flutter_vibrate/ios`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - - just_audio (from `.symlinks/plugins/just_audio/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - record (from `.symlinks/plugins/record/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - wakelock (from `.symlinks/plugins/wakelock/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: + - Cache - Firebase - FirebaseCore - FirebaseCoreDiagnostics - FirebaseInstallations - FirebaseMessaging - FMDB + - GCDWebServer - GoogleDataTransport - GoogleUtilities + - HLSCachingReverseProxyServer - nanopb + - PINCache + - PINOperation - PromisesObjC - TOCropViewController EXTERNAL SOURCES: - audio_session: - :path: ".symlinks/plugins/audio_session/ios" + assets_audio_player: + :path: ".symlinks/plugins/assets_audio_player/ios" + assets_audio_player_web: + :path: ".symlinks/plugins/assets_audio_player_web/ios" + better_player: + :path: ".symlinks/plugins/better_player/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: @@ -138,46 +175,56 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_cropper/ios" image_picker: :path: ".symlinks/plugins/image_picker/ios" - just_audio: - :path: ".symlinks/plugins/just_audio/ios" path_provider_ios: :path: ".symlinks/plugins/path_provider_ios/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" record: :path: ".symlinks/plugins/record/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + wakelock: + :path: ".symlinks/plugins/wakelock/ios" webview_flutter_wkwebview: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - audio_session: 4f3e461722055d21515cf3261b64c973c062f345 - Firebase: 44dd9724c84df18b486639e874f31436eaa9a20c - firebase_core: 08f6a85f62060111de5e98d6a214810d11365de9 - firebase_messaging: 36238f3d0b933af8c919aef608408aae06ba22e8 - FirebaseCore: 2f4f85b453cc8fea4bb2b37e370007d2bcafe3f0 - FirebaseCoreDiagnostics: 3b40dfadef5b90433a60ae01f01e90fe87aa76aa - FirebaseInstallations: 25764cf322e77f99449395870a65b2bef88e1545 - FirebaseMessaging: 02e248e8997f71fa8cc9d78e9d49ec1a701ba14a + assets_audio_player: edee322b9cb625571b830b35872ead1a295fd917 + assets_audio_player_web: 19826380c44375761aa0b9053665c1e3fbc3b86b + better_player: 2406bfe8175203c7a46fa15f9d778d73b12e1646 + Cache: 4ca7e00363fca5455f26534e5607634c820ffc2d + Firebase: 7e8fe528c161b9271d365217a74c16aaf834578e + firebase_core: b0b382f1497ab407aceb25e41e3036c8798c1609 + firebase_messaging: 34dd10d1aa6d8f40d03660eeacd0452d62eec7aa + FirebaseCore: b84a44ee7ba999e0f9f76d198a9c7f60a797b848 + FirebaseCoreDiagnostics: fd0c8490f34287229c1d6c103d3a55f81ec85712 + FirebaseInstallations: 7d1d967a307c12f1aadd76844fc321cef699b1ce + FirebaseMessaging: 5ebc42d281567658a2cb72b9ef3506e4a1a1a6e4 Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec flutter_vibrate: 9f4c2ab57008965f78969472367c329dd77eb801 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4 GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + HLSCachingReverseProxyServer: 59935e1e0244ad7f3375d75b5ef46e8eb26ab181 image_cropper: 60c2789d1f1a78c873235d4319ca0c34a69f2d98 - image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6 - just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa + image_picker: 541dcbb3b9cf32d87eacbd957845d8651d6c62c3 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 - path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 + path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce + PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 + PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 record: 7ee2393532f8553bbb09fa19e95478323b7c0a99 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 - url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af + url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f webview_flutter_wkwebview: 005fbd90c888a42c5690919a1527ecc6649e1162 -PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea +PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d COCOAPODS: 1.11.2 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 17f2a3a..d87332a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -28,6 +28,10 @@ Main FirebaseAppDelegateProxyEnabled + UIBackgroundModes + + audio + NSMicrophoneUsageDescription We need to access to the microphone to record audio file NSPhotoLibraryUsageDescription diff --git a/lib/config/design_config.dart b/lib/config/design_config.dart index bfb0fb8..afa9a76 100644 --- a/lib/config/design_config.dart +++ b/lib/config/design_config.dart @@ -1,6 +1,6 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/main.dart'; -import 'package:didvan/providers/theme_provider.dart'; +import 'package:didvan/providers/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; diff --git a/lib/main.dart b/lib/main.dart index 3ed3728..c8b4e77 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'package:bot_toast/bot_toast.dart'; import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/providers/theme_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/media.dart'; +import 'package:didvan/providers/theme.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/route_generator.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; @@ -20,12 +22,18 @@ class Didvan extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ + ChangeNotifierProvider( + create: (context) => MediaProvider(), + ), ChangeNotifierProvider( create: (context) => UserProvider(), ), ChangeNotifierProvider( create: (context) => ThemeProvider(), ), + ChangeNotifierProvider( + create: (context) => StudioDetailsState(), + ), ], child: Consumer( builder: (context, themeProvider, child) => MaterialApp( diff --git a/lib/models/comment/comment.dart b/lib/models/comment/comment.dart index b4f43ea..0d58166 100644 --- a/lib/models/comment/comment.dart +++ b/lib/models/comment/comment.dart @@ -6,9 +6,9 @@ class CommentData { int id; final String text; final String createdAt; - final bool liked; - final bool disliked; - FeedbackData feedback; + bool liked; + bool disliked; + final FeedbackData feedback; final UserOverview user; final List replies; diff --git a/lib/models/comment/feedback.dart b/lib/models/comment/feedback.dart index d51d67f..31696ba 100644 --- a/lib/models/comment/feedback.dart +++ b/lib/models/comment/feedback.dart @@ -1,8 +1,8 @@ class FeedbackData { - final int like; - final int dislike; + int like; + int dislike; - const FeedbackData({required this.like, required this.dislike}); + FeedbackData({required this.like, required this.dislike}); factory FeedbackData.fromJson(Map json) => FeedbackData( like: json['like'], diff --git a/lib/models/overview_data.dart b/lib/models/overview_data.dart index b72995b..f4995b0 100644 --- a/lib/models/overview_data.dart +++ b/lib/models/overview_data.dart @@ -1,4 +1,5 @@ import 'package:didvan/models/category.dart'; +import 'package:html/parser.dart'; class OverviewData { final int id; @@ -8,7 +9,8 @@ class OverviewData { final int? timeToRead; final int? duration; final String? reference; - final String? media; + final String? link; + final String? iframe; final bool forManagers; final String createdAt; final String type; @@ -26,35 +28,42 @@ class OverviewData { required this.marked, required this.comments, required this.forManagers, - this.media, + this.link, + this.iframe, this.duration, this.timeToRead, this.reference, this.categories, }); - factory OverviewData.fromJson(Map json) => OverviewData( - id: json['id'], - title: json['title'], - image: json['image'], - description: json['description'], - timeToRead: json['timeToRead'], - reference: json['reference'], - forManagers: json['forManagers'] ?? false, - comments: json['comments'] ?? 0, - createdAt: json['createdAt'], - duration: json['duration'], - type: json['type'] ?? '', - marked: json['marked'] ?? false, - media: json['media'], - categories: json['categories'] != null - ? List.from( - json['categories'].map( - (e) => CategoryData.fromJson(e), - ), - ) - : null, - ); + factory OverviewData.fromJson(Map json) { + final document = parse(json['description']); + final String parsedString = + parse(document.body!.text).documentElement!.text; + return OverviewData( + id: json['id'], + title: json['title'], + image: json['image'], + description: parsedString, + timeToRead: json['timeToRead'], + reference: json['reference'], + forManagers: json['forManagers'] ?? false, + comments: json['comments'] ?? 0, + createdAt: json['createdAt'], + duration: json['duration'], + type: json['type'] ?? '', + marked: json['marked'] ?? true, + link: json['link'], + iframe: json['iframe'], + categories: json['categories'] != null + ? List.from( + json['categories'].map( + (e) => CategoryData.fromJson(e), + ), + ) + : null, + ); + } Map toJson() => { 'id': id, diff --git a/lib/models/requests/studio.dart b/lib/models/requests/studio.dart index 74feeaa..221ea0a 100644 --- a/lib/models/requests/studio.dart +++ b/lib/models/requests/studio.dart @@ -3,9 +3,11 @@ class StudioRequestArgs { final String? search; final String? order; final String? type; + final bool? asc; const StudioRequestArgs({ required this.page, + this.asc, this.search, this.order, this.type, diff --git a/lib/models/slider_data.dart b/lib/models/slider_data.dart new file mode 100644 index 0000000..c6ff0ed --- /dev/null +++ b/lib/models/slider_data.dart @@ -0,0 +1,26 @@ +class SliderData { + final int id; + final String title; + final String image; + final String link; + + const SliderData({ + required this.id, + required this.title, + required this.image, + required this.link, + }); + + factory SliderData.fromJson(Map json) => SliderData( + id: json['id'], + title: json['title'], + image: json['image'], + link: json['link'], + ); + + Map toJson() => { + 'id': id, + 'title': title, + 'image': image, + }; +} diff --git a/lib/models/studio_details_data.dart b/lib/models/studio_details_data.dart index b83722e..334b4b2 100644 --- a/lib/models/studio_details_data.dart +++ b/lib/models/studio_details_data.dart @@ -7,7 +7,8 @@ class StudioDetailsData { final String title; final String description; final String image; - final String media; + final String link; + final String? iframe; final String createdAt; final int order; bool marked; @@ -21,7 +22,8 @@ class StudioDetailsData { required this.title, required this.description, required this.image, - required this.media, + required this.link, + required this.iframe, required this.createdAt, required this.order, required this.marked, @@ -36,7 +38,8 @@ class StudioDetailsData { title: json['title'], description: json['description'], image: json['image'], - media: json['media'], + link: json['link'], + iframe: json['iframe'], createdAt: json['createdAt'], order: json['order'], marked: json['marked'], @@ -51,7 +54,6 @@ class StudioDetailsData { 'title': title, 'description': description, 'image': image, - 'media': media, 'createdAt': createdAt, 'order': order, 'marked': marked, diff --git a/lib/providers/core_provider.dart b/lib/providers/core.dart similarity index 93% rename from lib/providers/core_provider.dart rename to lib/providers/core.dart index 820387d..b9887ef 100644 --- a/lib/providers/core_provider.dart +++ b/lib/providers/core.dart @@ -3,7 +3,7 @@ import 'package:didvan/utils/action_sheet.dart'; import 'package:flutter/cupertino.dart'; class CoreProvier with ChangeNotifier { - AppState _appState = AppState.idle; + AppState _appState = AppState.busy; set appState(AppState newState) { if (newState == AppState.isolatedBusy) { diff --git a/lib/providers/media.dart b/lib/providers/media.dart new file mode 100644 index 0000000..4d93846 --- /dev/null +++ b/lib/providers/media.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:didvan/models/enums.dart'; +import 'package:didvan/providers/core.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/storage/storage.dart'; + +class MediaProvider extends CoreProvier { + static final List downloadedItemIds = []; + final List downloadQueue = []; + + Future getDownloadsList() async { + downloadedItemIds.clear(); + final videosDir = Directory( + StorageService.appDocsDir + ('/videos'), + ); + final podcastsDir = Directory( + StorageService.appDocsDir + ('/podcasts'), + ); + if (!await videosDir.exists()) { + await videosDir.create(); + } + if (!await podcastsDir.exists()) { + await podcastsDir.create(); + } + videosDir.list(recursive: false).listen( + (event) { + downloadedItemIds.add( + int.parse( + event.path.split('/').last.split('-').last.split('.').first, + ), + ); + }, + ); + podcastsDir.list(recursive: false).listen( + (event) { + downloadedItemIds.add( + int.parse( + event.path.split('/').last.split('-').last.split('.').first, + ), + ); + }, + ); + await Future.delayed(const Duration(milliseconds: 300), notifyListeners); + } + + Future download({ + required String url, + required String fileName, + required bool isVideo, + }) async { + appState = AppState.busy; + downloadQueue.add(url); + notifyListeners(); + final service = RequestService(url); + await service.download(fileName, isVideo ? 'videos' : 'podcasts'); + downloadQueue.remove(url); + getDownloadsList(); + } +} diff --git a/lib/providers/server_data_provider.dart b/lib/providers/server_data.dart similarity index 81% rename from lib/providers/server_data_provider.dart rename to lib/providers/server_data.dart index 532bb5e..4ef71bb 100644 --- a/lib/providers/server_data_provider.dart +++ b/lib/providers/server_data.dart @@ -1,5 +1,6 @@ import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; +import 'package:collection/collection.dart'; class ServerDataProvider { static final List directTypes = []; @@ -10,7 +11,10 @@ class ServerDataProvider { static int labelToTypeId(String label) => label.contains('پشتیبانی') ? 7 - : directTypes.firstWhere((element) => element.value.contains(label)).key; + : directTypes + .firstWhereOrNull((element) => element.value.contains(label)) + ?.key ?? + 7; static Future _getDirectTypes() async { final service = RequestService(RequestHelper.directTypes); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings.dart similarity index 83% rename from lib/providers/settings_provider.dart rename to lib/providers/settings.dart index d49f51f..ebb9005 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings.dart @@ -1,4 +1,4 @@ -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:flutter/material.dart'; class SettingsProvider extends CoreProvier { diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme.dart similarity index 91% rename from lib/providers/theme_provider.dart rename to lib/providers/theme.dart index b994b5c..128443a 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme.dart @@ -1,4 +1,4 @@ -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:flutter/material.dart'; class ThemeProvider extends CoreProvier { diff --git a/lib/providers/user_provider.dart b/lib/providers/user.dart similarity index 87% rename from lib/providers/user_provider.dart rename to lib/providers/user.dart index 19c5192..d87e0ce 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user.dart @@ -2,7 +2,8 @@ import 'package:collection/collection.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/user.dart'; import 'package:didvan/models/view/alert_data.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; +import 'package:didvan/services/app_initalizer.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/storage/storage.dart'; @@ -29,16 +30,26 @@ class UserProvider extends CoreProvier { isAuthenticated = true; final RequestService service = RequestService(RequestHelper.userInfo); await service.httpGet(); - if (service.isSuccess) { - user = User.fromJson(service.result['user']); - return true; - } - if (service.statusCode == 401) { + if (service.statusCode == 401 || + (service.isSuccess && service.result['user'] == null)) { return false; } + if (service.isSuccess) { + user = User.fromJson(service.result['user']); + AppInitializer.initializeFirebase().then((_) => _registerFirebaseToken()); + _registerFirebaseToken(); + return true; + } throw 'Getting user from API failed!'; } + Future _registerFirebaseToken() async { + final service = RequestService(RequestHelper.firebaseToken, body: { + 'token': AppInitializer.fcmToken, + }); + await service.put(); + } + Future setProfilePhoto(dynamic file) async { appState = AppState.isolatedBusy; final RequestService service = @@ -132,7 +143,7 @@ class UserProvider extends CoreProvier { final MapEntry? lastChange = _radarMarkQueue.lastWhereOrNull((item) => item.key == id); if (lastChange == null) return; - final service = RequestService(RequestHelper.markRadar(id)); + final service = RequestService(RequestHelper.mark(id, 'radar')); if (lastChange.value) { await service.post(); } else { @@ -148,7 +159,7 @@ class UserProvider extends CoreProvier { final MapEntry? lastChange = _studioMarkQueue.lastWhereOrNull((item) => item.key == id); if (lastChange == null) return; - final service = RequestService(RequestHelper.markStudio(id)); + final service = RequestService(RequestHelper.mark(id, 'studio')); if (lastChange.value) { await service.post(); } else { @@ -164,7 +175,7 @@ class UserProvider extends CoreProvier { final MapEntry? lastChange = _newsMarkQueue.lastWhereOrNull((item) => item.key == id); if (lastChange == null) return; - final service = RequestService(RequestHelper.markNews(id)); + final service = RequestService(RequestHelper.mark(id, 'news')); if (lastChange.value) { await service.post(); } else { diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 7514e7c..308bc61 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -1,4 +1,3 @@ -import 'package:didvan/models/tag.dart'; import 'package:didvan/views/authentication/authentication.dart'; import 'package:didvan/views/authentication/authentication_state.dart'; import 'package:didvan/views/home/comments/comments.dart'; @@ -25,8 +24,9 @@ import 'package:didvan/views/home/settings/direct_list/direct_list_state.dart'; import 'package:didvan/views/home/settings/general_settings/settings.dart'; import 'package:didvan/views/home/settings/general_settings/settings_state.dart'; import 'package:didvan/views/home/settings/profile/profile.dart'; -import 'package:didvan/views/home/studio/studio_details/studio_details.dart'; -import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details.mobile.dart' + if (dart.library.io) 'package:didvan/views/home/studio/studio_details/studio_details.mobile.dart' + if (dart.library.html) 'package:didvan/views/home/studio/studio_details/studio_details.web.dart'; import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/routes/routes.dart'; @@ -64,9 +64,6 @@ class RouteGenerator { ChangeNotifierProvider( create: (context) => StudioState(), ), - ChangeNotifierProvider( - create: (context) => StudioDetailsState(), - ), ], child: const Home(), ), @@ -106,11 +103,8 @@ class RouteGenerator { ); case Routes.studioDetails: return _createRoute( - ChangeNotifierProvider.value( - value: (settings.arguments as Map)['state'], - child: StudioDetails( - pageData: settings.arguments as Map, - ), + StudioDetails( + pageData: settings.arguments as Map, ), ); case Routes.directList: @@ -147,17 +141,19 @@ class RouteGenerator { return _createRoute( ChangeNotifierProvider( create: (context) => HashtagState(), - child: Hashtag(tag: settings.arguments as Tag), + child: + Hashtag(pageData: settings.arguments as Map), ), ); - case Routes.filteredBookmarks: return _createRoute( ChangeNotifierProvider( create: (context) => FilteredBookmarksState( - settings.arguments as String, + (settings.arguments as Map)['type'], ), - child: const FilteredBookmarks(), + child: FilteredBookmarks( + onDeleted: + (settings.arguments as Map)['onDeleted']), ), ); default: @@ -184,17 +180,30 @@ class RouteGenerator { final shortestSide = MediaQuery.of(context).size.shortestSide; final bool useMobileLayout = shortestSide < 600; if (kIsWeb && !useMobileLayout) { - return Container( - color: Theme.of(context).colorScheme.background, - alignment: Alignment.center, - child: AspectRatio(aspectRatio: 9 / 16, child: page), + final deviceSize = MediaQuery.of(context).size; + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: 1.0, + size: Size( + deviceSize.width / 16 * 9, + deviceSize.height, + ), + ), + child: 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, - child: SafeArea( - child: page, - top: false, + return MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + child: Container( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + child: page, + top: false, + ), ), ); }, diff --git a/lib/services/app_initalizer.dart b/lib/services/app_initalizer.dart index 56d5f3f..749460e 100644 --- a/lib/services/app_initalizer.dart +++ b/lib/services/app_initalizer.dart @@ -1,5 +1,4 @@ import 'package:didvan/models/settings_data.dart'; -import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/storage/storage.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; @@ -8,13 +7,13 @@ import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; class AppInitializer { + static String? fcmToken; + static Future setupServices() async { if (!kIsWeb) { StorageService.appDocsDir = (await getApplicationDocumentsDirectory()).path; StorageService.appTempsDir = (await getTemporaryDirectory()).path; - await _initializeFirebase(); - MediaService.init(); } } @@ -34,11 +33,11 @@ class AppInitializer { } else { await StorageService.setValue( key: 'notificationTimeRangeStart', - value: '00:00', + value: '0', ); await StorageService.setValue( key: 'notificationTimeRangeEnd', - value: '23:59', + value: '24', ); await StorageService.setValue( key: 'fontFamily', @@ -60,7 +59,7 @@ class AppInitializer { } } - static Future _initializeFirebase() async { + static Future initializeFirebase() async { try { await Firebase.initializeApp( options: const FirebaseOptions( @@ -74,6 +73,8 @@ class AppInitializer { Firebase.app(); } final FirebaseMessaging fcm = FirebaseMessaging.instance; + fcmToken = await fcm.getToken(); + await fcm.subscribeToTopic('general'); await fcm.requestPermission( alert: true, announcement: false, diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 353b309..a783e23 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -1,64 +1,79 @@ import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/models/studio_details_data.dart'; +import 'package:didvan/providers/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/services/storage/storage.dart'; import 'package:flutter/foundation.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:just_audio/just_audio.dart'; +import 'package:assets_audio_player/assets_audio_player.dart'; class MediaService { - static final AudioPlayer audioPlayer = AudioPlayer(); + static final audioPlayer = AssetsAudioPlayer(); static String? audioPlayerTag; static StudioDetailsData? currentPodcast; static StudioRequestArgs? podcastPlaylistArgs; - static void init() { - audioPlayer.positionStream.listen((event) { - if (audioPlayer.duration != null && audioPlayer.duration! < event) { - audioPlayer.stop(); - audioPlayer.seek(const Duration(seconds: 0)); - } - }); - } + static Duration? get duration => audioPlayer.current.value?.audio.duration; static Future handleAudioPlayback({ required dynamic audioSource, + required int id, + bool isNetworkAudio = true, bool isVoiceMessage = true, + void Function(bool isNext)? onTrackChanged, }) async { - bool isNetworkAudio = audioSource.runtimeType == String; String tag; - if (isNetworkAudio) { - tag = audioSource; - } else { - tag = audioSource.path; + tag = '${isVoiceMessage ? 'message' : 'podcast'}-$id'; + if (!isVoiceMessage && MediaProvider.downloadedItemIds.contains(id)) { + audioSource = StorageService.appDocsDir + '/podcasts/podcast-$id.mp3'; + isNetworkAudio = false; } if (audioPlayerTag == tag) { - if (audioPlayer.playing) { - await audioPlayer.pause(); - } else { - await audioPlayer.play(); - } - } else { - await audioPlayer.stop(); - audioPlayerTag = tag; - if (isNetworkAudio) { - await audioPlayer.setUrl( - isVoiceMessage - ? (RequestHelper.baseUrl + - audioSource + - '?accessToken=${RequestService.token}') - : audioSource, - ); - } else { - if (kIsWeb) { - await audioPlayer - .setUrl(audioSource!.uri.path.replaceAll('%3A', ':')); - } else { - await audioPlayer.setFilePath(audioSource.path); - } - } - audioPlayer.play(); + await audioPlayer.playOrPause(); + return; } + await audioPlayer.stop(); + audioPlayerTag = tag; + Audio audio; + String source; + if (isNetworkAudio) { + if (isVoiceMessage) { + source = RequestHelper.baseUrl + + audioSource + + '?accessToken=${RequestService.token}'; + } else { + source = audioSource; + } + audio = Audio.network( + kIsWeb ? source.replaceAll('%3A', ':') : source, + metas: isVoiceMessage + ? null + : Metas( + artist: 'استودیو دیدوان', + title: currentPodcast!.title, + ), + ); + } else { + audio = Audio.file( + audioSource, + metas: isVoiceMessage + ? null + : Metas( + artist: 'استودیو دیدوان', + title: currentPodcast!.title, + ), + ); + } + await audioPlayer.open( + audio, + showNotification: !isVoiceMessage, + notificationSettings: NotificationSettings( + customStopAction: (_) => resetAudioPlayer(), + customNextAction: (_) => onTrackChanged?.call(true), + customPrevAction: (_) => onTrackChanged?.call(false), + ), + ); } static Future resetAudioPlayer() async { diff --git a/lib/services/network/request.dart b/lib/services/network/request.dart index 67ac373..d444a9a 100644 --- a/lib/services/network/request.dart +++ b/lib/services/network/request.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:didvan/services/storage/storage.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart' as parser; +import 'package:permission_handler/permission_handler.dart'; class RequestService { static late String token; @@ -162,6 +164,16 @@ class RequestService { } } + Future download(String fileName, String subDirectory) async { + await Permission.storage.request(); + final response = await http.get(Uri.parse(url)); + StorageService.createFile( + bytes: response.bodyBytes, + subDirectory: subDirectory, + name: fileName, + ); + } + void _handleResponse(http.Response? response) { statusCode = response?.statusCode; if (_handleError(response)) { diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index bfddb9a..525622e 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -3,7 +3,7 @@ import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/models/requests/studio.dart'; class RequestHelper { - static const String baseUrl = 'https://api.didvan.app'; + static const String baseUrl = 'https://test.api.didvan.app'; static const String _baseUserUrl = baseUrl + '/user'; static const String _baseRadarUrl = baseUrl + '/radar'; static const String _baseNewsUrl = baseUrl + '/news'; @@ -15,6 +15,8 @@ class RequestHelper { static const String login = _baseUserUrl + '/login'; static const String directs = _baseUserUrl + '/direct'; static const String userInfo = _baseUserUrl + '/info'; + static const String firebaseToken = _baseUserUrl + '/firebaseToken'; + static const String silenceInterval = _baseUserUrl + '/silenceInterval'; static const String updateProfilePhoto = _baseUserUrl + '/profile/photo'; static const String checkUsername = _baseUserUrl + '/CheckUsername'; static const String updateProfile = _baseUserUrl + '/profile/edit'; @@ -54,11 +56,6 @@ class RequestHelper { MapEntry('tags', _urlListConcatGenerator(ids)), ]); - static String markRadar(int id) => _baseRadarUrl + '/$id/mark'; - static String radarComments(int id) => _baseRadarUrl + '/$id/comments'; - static String addRadarComment(int id) => _baseRadarUrl + '/$id/comments/add'; - static String feedbackRadarComment(int radarId, int id) => - _baseRadarUrl + '/$radarId/comments/$id/feedback'; static String radarDetails(int id, RadarRequestArgs args) => _baseRadarUrl + '/$id' + @@ -79,11 +76,6 @@ class RequestHelper { MapEntry('categories', _urlListConcatGenerator(args.categories)), ]); - static String markNews(int id) => _baseNewsUrl + '/$id/mark'; - static String newsComments(int id) => _baseNewsUrl + '/$id/comments'; - static String addNewsComment(int id) => _baseNewsUrl + '/$id/comments/add'; - static String feedbackNewsComment(int radarId, int id) => - _baseNewsUrl + '/$radarId/comments/$id/feedback'; static String newsDetails(int id, NewsRequestArgs args) => _baseNewsUrl + '/$id' + @@ -102,12 +94,10 @@ class RequestHelper { MapEntry('search', args.search), ]); - static String markStudio(int id) => _baseStudioUrl + '/$id/mark'; - static String studioComments(int id) => _baseStudioUrl + '/$id/comments'; - static String addStudioComment(int id) => - _baseStudioUrl + '/$id/comments/add'; - static String feedbackStudioComment(int studioId, int id) => - _baseStudioUrl + '/$studioId/comments/$id/feedback'; + static String sudioSlider(String type) => + _baseStudioUrl + + '/slider' + + _urlConcatGenerator([MapEntry('type', type)]); static String studioDetails(int id, StudioRequestArgs args) => _baseStudioUrl + '/$id' + @@ -116,6 +106,7 @@ class RequestHelper { MapEntry('type', args.type), MapEntry('order', args.order), MapEntry('search', args.search), + MapEntry('asc', args.asc), ]); static String studioOverviews({required StudioRequestArgs args}) => _baseStudioUrl + @@ -124,8 +115,19 @@ class RequestHelper { MapEntry('type', args.type), MapEntry('order', args.order), MapEntry('search', args.search), + MapEntry('asc', args.asc), ]); + static String mark(int id, String type) => baseUrl + '/$type/$id/mark'; + static String tracking(int id, String type) => + baseUrl + '/$type/$id/tracking'; + static String comments(int id, String type) => + baseUrl + '/$type/$id/comments'; + static String feedback(int id, int commentId, String type) => + baseUrl + '/$type/$id/comments/$commentId/feedback'; + static String addComment(int id, String type) => + baseUrl + '/$type/$id/comments/add'; + static String _urlConcatGenerator(List> additions) { String result = ''; additions.removeWhere( diff --git a/lib/services/storage/storage.dart b/lib/services/storage/storage.dart index 012936c..ed607e8 100644 --- a/lib/services/storage/storage.dart +++ b/lib/services/storage/storage.dart @@ -1,3 +1,6 @@ +import 'dart:typed_data'; +import 'dart:io' as io; + import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:universal_html/html.dart'; @@ -6,6 +9,21 @@ class StorageService { static late String appTempsDir; static const FlutterSecureStorage _storage = FlutterSecureStorage(); + static Future createFile({ + required Uint8List bytes, + required String subDirectory, + required String name, + }) async { + final dir = io.Directory(appDocsDir + '/$subDirectory'); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + final file = await io.File( + appDocsDir + '/$subDirectory/$name', + ).create(recursive: true); + await file.writeAsBytes(bytes); + } + static Future setValue({ required String key, required dynamic value, diff --git a/lib/utils/action_sheet.dart b/lib/utils/action_sheet.dart index 15ea262..bbb3c2c 100644 --- a/lib/utils/action_sheet.dart +++ b/lib/utils/action_sheet.dart @@ -17,6 +17,7 @@ class ActionSheetUtils { static Future showLogoLoadingIndicator() async { await showDialog( + barrierDismissible: false, context: context, builder: (context) => Padding( padding: EdgeInsets.symmetric( @@ -79,7 +80,9 @@ class ActionSheetUtils { isScrollControlled: true, context: context, builder: (context) => Container( - padding: data.hasPadding ? const EdgeInsets.all(20) : EdgeInsets.zero, + padding: data.hasPadding + ? const EdgeInsets.all(20).copyWith(top: 0) + : EdgeInsets.zero, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.vertical( diff --git a/lib/views/authentication/authentication_state.dart b/lib/views/authentication/authentication_state.dart index ffda950..441d661 100644 --- a/lib/views/authentication/authentication_state.dart +++ b/lib/views/authentication/authentication_state.dart @@ -1,7 +1,7 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/alert_data.dart'; -import 'package:didvan/providers/core_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/core.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/utils/action_sheet.dart'; diff --git a/lib/views/authentication/screens/password.dart b/lib/views/authentication/screens/password.dart index 69cdc42..d558a49 100644 --- a/lib/views/authentication/screens/password.dart +++ b/lib/views/authentication/screens/password.dart @@ -1,8 +1,8 @@ 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/providers/server_data.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/authentication/authentication_state.dart'; @@ -76,7 +76,9 @@ class _PasswordInputState extends State { final token = await state.login(userProvider); if (token != null) { log(token); + ActionSheetUtils.showLogoLoadingIndicator(); await ServerDataProvider.getData(); + ActionSheetUtils.pop(); Navigator.of(context).pushReplacementNamed(Routes.home); _showResetPasswordDialog(); } diff --git a/lib/views/authentication/screens/reset_password.dart b/lib/views/authentication/screens/reset_password.dart index 9985a95..560ea4b 100644 --- a/lib/views/authentication/screens/reset_password.dart +++ b/lib/views/authentication/screens/reset_password.dart @@ -1,4 +1,4 @@ -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/authentication/authentication_state.dart'; import 'package:didvan/views/authentication/widgets/authentication_layout.dart'; diff --git a/lib/views/authentication/screens/verification.dart b/lib/views/authentication/screens/verification.dart index e18e5c3..161d73b 100644 --- a/lib/views/authentication/screens/verification.dart +++ b/lib/views/authentication/screens/verification.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/user.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'; diff --git a/lib/views/home/comments/comments.dart b/lib/views/home/comments/comments.dart index 0bf0c72..6e03d73 100644 --- a/lib/views/home/comments/comments.dart +++ b/lib/views/home/comments/comments.dart @@ -1,6 +1,7 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/views/home/comments/comments_state.dart'; import 'package:didvan/views/home/comments/widgets/comment_item.dart'; @@ -9,6 +10,7 @@ import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -32,7 +34,7 @@ class _CommentsState extends State { void initState() { final state = context.read(); state.itemId = widget.pageData['id']; - state.isRadar = widget.pageData['isRadar']; + state.type = widget.pageData['type']; state.onCommentsChanged = widget.pageData['onCommentsChanged']; Future.delayed( Duration.zero, @@ -41,6 +43,8 @@ class _CommentsState extends State { super.initState(); } + bool get _isPage => widget.pageData['isPage'] != false; + @override Widget build(BuildContext context) { final bottomViewInset = MediaQuery.of(context).viewInsets.bottom; @@ -55,12 +59,15 @@ class _CommentsState extends State { child: Stack( children: [ DidvanScaffold( + physics: const BouncingScrollPhysics(), backgroundColor: Theme.of(context).colorScheme.surface, - appBarData: AppBarData( - hasBack: true, - title: 'نظرات', - subtitle: widget.pageData['title'], - ), + appBarData: _isPage + ? AppBarData( + hasBack: true, + title: 'نظرات', + subtitle: widget.pageData['title'], + ) + : null, padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92), slivers: [ Consumer( @@ -71,7 +78,17 @@ class _CommentsState extends State { itemPadding: const EdgeInsets.symmetric(vertical: 16), childCount: state.comments.length, placeholder: const _CommentPlaceholder(), + centerEmptyState: _isPage, + enableEmptyState: state.comments.isEmpty, + emptyState: EmptyState( + asset: Assets.emptyChat, + title: 'اولین نظر را بنویسید...', + ), builder: (context, state, index) => Comment( + key: ValueKey( + state.comments[index].id.toString() + + state.comments[index].text, + ), focusNode: _focusNode, comment: state.comments[index], ), diff --git a/lib/views/home/comments/comments_state.dart b/lib/views/home/comments/comments_state.dart index ead8409..4b02d00 100644 --- a/lib/views/home/comments/comments_state.dart +++ b/lib/views/home/comments/comments_state.dart @@ -4,8 +4,8 @@ import 'package:didvan/models/comment/feedback.dart'; import 'package:didvan/models/comment/reply.dart'; import 'package:didvan/models/comment/user.dart'; import 'package:didvan/models/enums.dart'; -import 'package:didvan/providers/core_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/core.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:provider/provider.dart'; @@ -17,19 +17,16 @@ class CommentsState extends CoreProvier { bool showReplyBox = false; late void Function(int count) onCommentsChanged; int _count = 0; + late String type; final List comments = []; final Map> _feedbackQueue = {}; - bool isRadar = true; int itemId = 0; Future getComments() async { - appState = AppState.busy; final service = RequestService( - isRadar - ? RequestHelper.radarComments(itemId) - : RequestHelper.newsComments(itemId), + RequestHelper.comments(itemId, type), ); await service.httpGet(); if (service.isSuccess) { @@ -47,18 +44,40 @@ class CommentsState extends CoreProvier { appState = AppState.failed; } - Future feedback(int id, bool like, bool dislike) async { + Future feedback({ + required int id, + required bool like, + required bool dislike, + required int likeCount, + required int dislikeCount, + int? replyId, + }) async { _feedbackQueue.addAll({id: MapEntry(like, dislike)}); + dynamic comment; + if (replyId == null) { + comment = comments.firstWhere((comment) => comment.id == id); + } else { + comment = comments + .firstWhere((comment) => comment.id == id) + .replies + .firstWhere((element) => element.id == replyId); + } + + if (comment != null) { + comment.feedback.like = likeCount; + comment.feedback.dislike = dislikeCount; + comment.disliked = dislike; + comment.liked = like; + } Future.delayed(const Duration(milliseconds: 500), () async { if (!_feedbackQueue.containsKey(id)) return; final service = RequestService( - isRadar - ? RequestHelper.feedbackRadarComment(itemId, id) - : RequestHelper.feedbackNewsComment(itemId, id), - body: { - 'like': _feedbackQueue[id]!.key, - 'dislike': _feedbackQueue[id]!.value, - }); + RequestHelper.feedback(itemId, id, type), + body: { + 'like': _feedbackQueue[id]!.key, + 'dislike': _feedbackQueue[id]!.value, + }, + ); await service.put(); _feedbackQueue.remove(id); }); @@ -74,7 +93,7 @@ class CommentsState extends CoreProvier { createdAt: DateTime.now().toString(), liked: false, disliked: false, - feedback: const FeedbackData(like: 0, dislike: 0), + feedback: FeedbackData(like: 0, dislike: 0), toUser: replyingTo!, user: UserOverview( id: user.id, @@ -92,7 +111,7 @@ class CommentsState extends CoreProvier { createdAt: DateTime.now().toString(), liked: false, disliked: false, - feedback: const FeedbackData(like: 0, dislike: 0), + feedback: FeedbackData(like: 0, dislike: 0), user: UserOverview( id: user.id, fullName: user.fullName, @@ -119,10 +138,9 @@ class CommentsState extends CoreProvier { update(); body.addAll({'text': text}); final service = RequestService( - isRadar - ? RequestHelper.addRadarComment(itemId) - : RequestHelper.addNewsComment(itemId), - body: body); + RequestHelper.addComment(itemId, type), + body: body, + ); await service.post(); if (service.isSuccess) { diff --git a/lib/views/home/comments/widgets/comment_item.dart b/lib/views/home/comments/widgets/comment_item.dart index 62fc0a2..4748031 100644 --- a/lib/views/home/comments/widgets/comment_item.dart +++ b/lib/views/home/comments/widgets/comment_item.dart @@ -49,7 +49,7 @@ class CommentState extends State { duration: DesignConfig.lowAnimationDuration, isVisible: _showSubComments, child: _commentBuilder( - isSubComment: true, + isReply: true, comment: _comment.replies[i], ), ), @@ -57,11 +57,10 @@ class CommentState extends State { ); } - Widget _commentBuilder({required comment, bool isSubComment = false}) => - Container( + Widget _commentBuilder({required comment, bool isReply = false}) => Container( decoration: BoxDecoration( border: Border( - right: isSubComment + right: isReply ? BorderSide(color: Theme.of(context).colorScheme.caption) : BorderSide.none, ), @@ -69,7 +68,7 @@ class CommentState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isSubComment) const SizedBox(width: 12), + if (isReply) const SizedBox(width: 12), if (comment.user.photo == null) const Icon(DidvanIcons.avatar_light), if (comment.user.photo != null) @@ -99,7 +98,7 @@ class CommentState extends State { ], ), const SizedBox(height: 8), - if (isSubComment) + if (isReply) DidvanText( 'پاسخ به ${comment.toUser.fullName}', style: Theme.of(context).textTheme.caption, @@ -124,8 +123,8 @@ class CommentState extends State { color: Theme.of(context).colorScheme.primary, ), ), - if (!isSubComment) const SizedBox(width: 20), - if (!isSubComment && comment.replies.isNotEmpty) + if (!isReply) const SizedBox(width: 20), + if (!isReply && comment.replies.isNotEmpty) InkWrapper( onPressed: () => setState( () => _showSubComments = !_showSubComments, @@ -154,8 +153,15 @@ class CommentState extends State { dislikeCount: comment.feedback.dislike, likeValue: comment.liked, dislikeValue: comment.disliked, - onFeedback: (like, dislike) => - state.feedback(comment.id, like, dislike), + onFeedback: (like, dislike, likeCount, dislikeCount) => + state.feedback( + id: _comment.id, + like: like, + dislike: dislike, + likeCount: likeCount, + dislikeCount: dislikeCount, + replyId: isReply ? comment.id : null, + ), ), ], ), @@ -172,7 +178,8 @@ class _FeedbackButtons extends StatefulWidget { final int dislikeCount; final bool likeValue; final bool dislikeValue; - final void Function(bool like, bool dislike) onFeedback; + final void Function(bool like, bool dislike, int likeCount, int dislikeCount) + onFeedback; const _FeedbackButtons({ Key? key, required this.onFeedback, @@ -228,7 +235,8 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> { } _likeValue = !_likeValue; }); - widget.onFeedback(_likeValue, _dislikeValue); + widget.onFeedback( + _likeValue, _dislikeValue, _likeCount, _dislikeCount); }, ), const SizedBox(width: 16), @@ -257,7 +265,8 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> { } _dislikeValue = !_dislikeValue; }); - widget.onFeedback(_likeValue, _dislikeValue); + widget.onFeedback( + _likeValue, _dislikeValue, _likeCount, _dislikeCount); }, ), ], diff --git a/lib/views/home/direct/direct.dart b/lib/views/home/direct/direct.dart index 17ddee1..d47be3c 100644 --- a/lib/views/home/direct/direct.dart +++ b/lib/views/home/direct/direct.dart @@ -1,7 +1,7 @@ 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'; +import 'package:didvan/providers/server_data.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/views/home/direct/direct_state.dart'; import 'package:didvan/views/home/direct/widgets/message.dart'; diff --git a/lib/views/home/direct/direct_state.dart b/lib/views/home/direct/direct_state.dart index a4a14aa..20964d5 100644 --- a/lib/views/home/direct/direct_state.dart +++ b/lib/views/home/direct/direct_state.dart @@ -3,7 +3,8 @@ import 'dart:io'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/message_data/message_data.dart'; import 'package:didvan/models/message_data/radar_attachment.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; +import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:flutter/foundation.dart'; @@ -46,6 +47,7 @@ class DirectState extends CoreProvier { } Future startRecording() async { + text = null; await _recorder.hasPermission(); if (!kIsWeb) { Vibrate.feedback(FeedbackType.medium); @@ -88,6 +90,7 @@ class DirectState extends CoreProvier { Future sendMessage() async { if ((text == null || text!.isEmpty) && recordedFile == null) return; + MediaService.audioPlayer.stop(); messages.insert( 0, MessageData( diff --git a/lib/views/home/direct/widgets/audio_widget.dart b/lib/views/home/direct/widgets/audio_widget.dart index a33827c..5f1c8bd 100644 --- a/lib/views/home/direct/widgets/audio_widget.dart +++ b/lib/views/home/direct/widgets/audio_widget.dart @@ -1,31 +1,39 @@ import 'dart:io'; +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/widgets/audio/audio_slider.dart'; -import 'package:didvan/views/home/widgets/player_controller_button.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:flutter/material.dart'; class AudioWidget extends StatelessWidget { final String? audioUrl; final File? audioFile; - const AudioWidget({Key? key, this.audioUrl, this.audioFile}) - : super(key: key); + final int id; + const AudioWidget({ + Key? key, + this.audioUrl, + this.audioFile, + required this.id, + }) : super(key: key); @override Widget build(BuildContext context) { return StreamBuilder( - stream: MediaService.audioPlayer.playingStream, + stream: MediaService.audioPlayer.isPlaying, builder: (context, snapshot) { return Row( children: [ Expanded( child: AudioSlider( - tag: audioUrl ?? audioFile!.path, + tag: 'message-$id', ), ), - AudioControllerButton( + _AudioControllerButton( audioFile: audioFile, audioUrl: audioUrl, + id: id, ), ], ); @@ -33,3 +41,33 @@ class AudioWidget extends StatelessWidget { ); } } + +class _AudioControllerButton extends StatelessWidget { + final String? audioUrl; + final File? audioFile; + final int id; + + const _AudioControllerButton( + {Key? key, this.audioUrl, this.audioFile, required this.id}) + : super(key: key); + + bool get _nowPlaying => MediaService.audioPlayerTag == 'message-$id'; + + @override + Widget build(BuildContext context) { + return DidvanIconButton( + icon: MediaService.audioPlayer.isPlaying.value && _nowPlaying + ? DidvanIcons.pause_circle_solid + : DidvanIcons.play_circle_solid, + color: Theme.of(context).colorScheme.focusedBorder, + onPressed: () { + MediaService.handleAudioPlayback( + audioSource: audioFile?.path ?? audioUrl, + id: id, + isNetworkAudio: audioFile == null, + isVoiceMessage: true, + ); + }, + ); + } +} diff --git a/lib/views/home/direct/widgets/message.dart b/lib/views/home/direct/widgets/message.dart index 49ab63d..ff90eb1 100644 --- a/lib/views/home/direct/widgets/message.dart +++ b/lib/views/home/direct/widgets/message.dart @@ -65,6 +65,7 @@ class Message extends StatelessWidget { AudioWidget( audioFile: message.audioFile, audioUrl: message.audio, + id: message.id, ), if (message.radar != null) const DidvanDivider(), if (message.radar != null) const SizedBox(height: 4), diff --git a/lib/views/home/direct/widgets/message_box.dart b/lib/views/home/direct/widgets/message_box.dart index c09d592..554e336 100644 --- a/lib/views/home/direct/widgets/message_box.dart +++ b/lib/views/home/direct/widgets/message_box.dart @@ -211,7 +211,10 @@ class _RecordChecking extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), - child: AudioWidget(audioFile: state.recordedFile!), + child: AudioWidget( + audioFile: state.recordedFile!, + id: 0, + ), ), ), DidvanIconButton( diff --git a/lib/views/home/hashtag/hashtag.dart b/lib/views/home/hashtag/hashtag.dart index 24a9a7c..c12e92e 100644 --- a/lib/views/home/hashtag/hashtag.dart +++ b/lib/views/home/hashtag/hashtag.dart @@ -1,8 +1,11 @@ +import 'package:didvan/models/requests/studio.dart'; 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/overview/news.dart'; +import 'package:didvan/views/home/widgets/overview/podcast.dart'; import 'package:didvan/views/home/widgets/overview/radar.dart'; +import 'package:didvan/views/home/widgets/overview/video.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; @@ -10,18 +13,20 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:provider/provider.dart'; class Hashtag extends StatefulWidget { - final Tag tag; - const Hashtag({Key? key, required this.tag}) : super(key: key); + final Map pageData; + const Hashtag({Key? key, required this.pageData}) : super(key: key); @override _HashtagState createState() => _HashtagState(); } class _HashtagState extends State { + Tag get _tag => widget.pageData['tag']; + @override void initState() { final state = context.read(); - state.id = widget.tag.id; + state.id = _tag.id; Future.delayed(Duration.zero, () => state.getTagItems(page: 1)); super.initState(); } @@ -29,7 +34,7 @@ class _HashtagState extends State { @override Widget build(BuildContext context) { return DidvanScaffold( - appBarData: AppBarData(title: '#' + widget.tag.label, hasBack: true), + appBarData: AppBarData(title: '#' + _tag.label, hasBack: true), slivers: [ Consumer( builder: (context, state, child) => SliverStateHandler( @@ -50,19 +55,38 @@ class _HashtagState extends State { } final item = state.items[index]; final type = item.type; - if (type == 'radar') { - return RadarOverview( - radar: item, - onCommentsChanged: (id, count) => item.comments = count, - onMarkChanged: (id, value) => item.marked = value, - ); - } else if (type == 'news') { - return NewsOverview( - news: item, - onMarkChanged: (id, value) => item.marked = value, - ); + switch (type) { + case 'radar': + return RadarOverview( + radar: item, + onMarkChanged: (_, value, __) => + _changeMark(item.id, value, type), + onCommentsChanged: (_, count) => item.comments = count, + ); + case 'news': + return NewsOverview( + news: item, + onMarkChanged: (_, value, __) => + _changeMark(item.id, value, type), + ); + case 'podcast': + return PodcastOverview( + podcast: item, + onMarkChanged: (_, value, __) => + _changeMark(item.id, value, type), + studioRequestArgs: + const StudioRequestArgs(page: 0, type: 'podcast'), + ); + case 'video': + return VideoOverview( + video: item, + onMarkChanged: (_, value, __) => + _changeMark(item.id, value, type), + studioRequestArgs: + const StudioRequestArgs(page: 0, type: 'video'), + ); } - return Container(); + return const SizedBox(); }, childCount: state.items.length + (state.page != state.lastPage ? 1 : 0), @@ -72,4 +96,15 @@ class _HashtagState extends State { ], ); } + + void _changeMark(int id, bool value, String type) { + final state = context.read(); + state.items + .firstWhere((element) => element.id == id && element.type == type) + .marked = value; + state.update(); + if (type == widget.pageData['type']) { + widget.pageData['onMarkChanged'](id, value); + } + } } diff --git a/lib/views/home/hashtag/hashtag_state.dart b/lib/views/home/hashtag/hashtag_state.dart index fc1e5e7..0c41280 100644 --- a/lib/views/home/hashtag/hashtag_state.dart +++ b/lib/views/home/hashtag/hashtag_state.dart @@ -1,6 +1,6 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -18,13 +18,12 @@ class HashtagState extends CoreProvier { } final service = RequestService(RequestHelper.tag( ids: [id], - itemId: 1, - type: 'radar', limit: 15, page: page, )); await service.httpGet(); if (service.isSuccess) { + lastPage = service.result['lastPage']; final contents = service.result['contents']; for (var i = 0; i < contents.length; i++) { items.add(OverviewData.fromJson(contents[i])); diff --git a/lib/views/home/home.dart b/lib/views/home/home.dart index 0e1d5a8..b9c203e 100644 --- a/lib/views/home/home.dart +++ b/lib/views/home/home.dart @@ -5,7 +5,7 @@ import 'package:didvan/views/home/radar/radar.dart'; import 'package:didvan/views/home/settings/settings.dart'; import 'package:didvan/views/home/statistics/statistics.dart'; import 'package:didvan/views/home/studio/studio.dart'; -import 'package:didvan/views/home/widgets/bnb.dart'; +import 'package:didvan/views/widgets/didvan/bnb.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/views/home/home_state.dart b/lib/views/home/home_state.dart index ed4ea99..c866e0e 100644 --- a/lib/views/home/home_state.dart +++ b/lib/views/home/home_state.dart @@ -1,4 +1,4 @@ -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; class HomeState extends CoreProvier { int _currentPageIndex = 2; diff --git a/lib/views/home/news/news.dart b/lib/views/home/news/news.dart index 421a045..9387dae 100644 --- a/lib/views/home/news/news.dart +++ b/lib/views/home/news/news.dart @@ -66,7 +66,7 @@ class _NewsState extends State { final news = state.news[index]; return NewsOverview( news: news, - onMarkChanged: (id, value) => state.onMarkChanged(id, value), + onMarkChanged: state.onMarkChanged, newsRequestArgs: NewsRequestArgs( page: state.page, endDate: state.endDate, diff --git a/lib/views/home/news/news_details/news_details.dart b/lib/views/home/news/news_details/news_details.dart index 55d09d4..3becf7e 100644 --- a/lib/views/home/news/news_details/news_details.dart +++ b/lib/views/home/news/news_details/news_details.dart @@ -38,12 +38,16 @@ class _NewsDetailsState extends State { Widget build(BuildContext context) { return Scaffold( body: Consumer( - builder: (context, state, child) => StateHandler( - onRetry: () => state.getNewsDetails(state.currentNews.id), - state: state, - builder: (context, state) => Stack( - children: [ - if (state.news.isNotEmpty) + builder: (context, state, child) => WillPopScope( + onWillPop: () async { + state.handleTracking(sendRequest: true); + return true; + }, + child: StateHandler( + onRetry: () => state.getNewsDetails(state.currentNews.id), + state: state, + builder: (context, state) => Stack( + children: [ IgnorePointer( ignoring: state.isFetchingNewItem, child: DidvanPageView( @@ -53,9 +57,10 @@ class _NewsDetailsState extends State { scrollController: _scrollController, items: state.news, currentIndex: state.currentIndex, + onMarkChanged: (id, value) => + widget.pageData['onMarkChanged'](id, value), ), ), - if (state.news.isNotEmpty) Positioned( bottom: 0, left: 0, @@ -73,7 +78,8 @@ class _NewsDetailsState extends State { isRadar: false, ), ), - ], + ], + ), ), ), ), 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 279bc78..8adbb91 100644 --- a/lib/views/home/news/news_details/news_details_state.dart +++ b/lib/views/home/news/news_details/news_details_state.dart @@ -5,7 +5,7 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/news_details_data.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/news.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -32,7 +32,7 @@ class NewsDetailsState extends CoreProvier { } final service = RequestService(RequestHelper.newsDetails(id, args)); await service.httpGet(); - _handleTracking(sendRequest: isForward != null); + handleTracking(sendRequest: isForward != null); if (service.isSuccess) { final result = service.result; final newsItem = NewsDetailsData.fromJson(result['news']); @@ -88,15 +88,21 @@ class NewsDetailsState extends CoreProvier { notifyListeners(); } - Future _handleTracking({bool sendRequest = true}) async { + Future handleTracking({bool sendRequest = true}) async { if (!sendRequest) { + _trackingTimerCounter = 0; _trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _trackingTimerCounter++; }); return; } - //send request - _trackingTimerCounter = 0; + final service = RequestService( + RequestHelper.tracking(currentNews.id, 'news'), + body: { + 'sec': _trackingTimerCounter, + }, + ); + service.put(); } Future getRelatedContents() async { diff --git a/lib/views/home/news/news_state.dart b/lib/views/home/news/news_state.dart index 47f3366..b022538 100644 --- a/lib/views/home/news/news_state.dart +++ b/lib/views/home/news/news_state.dart @@ -1,8 +1,7 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/news.dart'; -import 'package:didvan/providers/core_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -69,10 +68,11 @@ class NewsState extends CoreProvier { appState = AppState.failed; } - Future onMarkChanged(int id, bool value) async { + Future onMarkChanged(int id, bool value, bool shouldUpdate) async { news.firstWhere((element) => element.id == id).marked = value; - notifyListeners(); - UserProvider.changeNewsMark(id, value); + if (shouldUpdate) { + notifyListeners(); + } } bool get isFiltering => startDate != null || endDate != null; diff --git a/lib/views/home/radar/radar.dart b/lib/views/home/radar/radar.dart index 6058543..6384c96 100644 --- a/lib/views/home/radar/radar.dart +++ b/lib/views/home/radar/radar.dart @@ -139,7 +139,7 @@ class _RadarState extends State { final radar = state.radars[index]; return RadarOverview( radar: radar, - onMarkChanged: (id, value) => state.changeMark(id, value), + onMarkChanged: state.changeMark, onCommentsChanged: (id, count) => state.onCommentsChanged(id, count), radarRequestArgs: RadarRequestArgs( diff --git a/lib/views/home/radar/radar_details/radar_details.dart b/lib/views/home/radar/radar_details/radar_details.dart index 1c9eb11..c371e5f 100644 --- a/lib/views/home/radar/radar_details/radar_details.dart +++ b/lib/views/home/radar/radar_details/radar_details.dart @@ -38,12 +38,16 @@ class _RadarDetailsState extends State { Widget build(BuildContext context) { return Scaffold( body: Consumer( - builder: (context, state, child) => StateHandler( - onRetry: () => state.getRadarDetails(widget.pageData['id']), - state: state, - builder: (context, state) => Stack( - children: [ - if (state.radars.isNotEmpty) + builder: (context, state, child) => WillPopScope( + onWillPop: () async { + state.handleTracking(sendRequest: true); + return true; + }, + child: StateHandler( + onRetry: () => state.getRadarDetails(widget.pageData['id']), + state: state, + builder: (context, state) => Stack( + children: [ IgnorePointer( ignoring: state.isFetchingNewItem, child: DidvanPageView( @@ -53,9 +57,10 @@ class _RadarDetailsState extends State { scrollController: _scrollController, items: state.radars, currentIndex: state.currentIndex, + onMarkChanged: (id, value) => + widget.pageData['onMarkChanged']?.call(id, value), ), ), - if (state.radars.isNotEmpty) Positioned( bottom: 0, left: 0, @@ -80,7 +85,8 @@ class _RadarDetailsState extends State { }, ), ), - ], + ], + ), ), ), ), 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 37ebb42..81240a3 100644 --- a/lib/views/home/radar/radar_details/radar_details_state.dart +++ b/lib/views/home/radar/radar_details/radar_details_state.dart @@ -5,7 +5,7 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/radar_details_data.dart'; import 'package:didvan/models/requests/radar.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -38,7 +38,7 @@ class RadarDetailsState extends CoreProvier { } final service = RequestService(RequestHelper.radarDetails(id, args)); await service.httpGet(); - _handleTracking(sendRequest: isForward != null); + handleTracking(sendRequest: isForward != null); if (service.isSuccess) { final result = service.result; final radar = RadarDetailsData.fromJson(result['radar']); @@ -121,15 +121,21 @@ class RadarDetailsState extends CoreProvier { notifyListeners(); } - Future _handleTracking({bool sendRequest = true}) async { + Future handleTracking({bool sendRequest = true}) async { if (!sendRequest) { + _trackingTimerCounter = 0; _trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _trackingTimerCounter++; }); return; } - //send request - _trackingTimerCounter = 0; + final service = RequestService( + RequestHelper.tracking(currentRadar.id, 'radar'), + body: { + 'sec': _trackingTimerCounter, + }, + ); + service.put(); } @override diff --git a/lib/views/home/radar/radar_state.dart b/lib/views/home/radar/radar_state.dart index 75318ee..fb293a2 100644 --- a/lib/views/home/radar/radar_state.dart +++ b/lib/views/home/radar/radar_state.dart @@ -3,8 +3,7 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/models/view/radar_category.dart'; -import 'package:didvan/providers/core_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -82,10 +81,11 @@ class RadarState extends CoreProvier { appState = AppState.failed; } - Future changeMark(int id, bool value) async { + Future changeMark(int id, bool value, bool shouldUpdate) async { radars.firstWhere((element) => element.id == id).marked = value; - notifyListeners(); - UserProvider.changeRadarMark(id, value); + if (shouldUpdate) { + notifyListeners(); + } } void onCommentsChanged(int id, int count) { diff --git a/lib/views/home/settings/bookmarks/bookmark_state.dart b/lib/views/home/settings/bookmarks/bookmark_state.dart index 063dc4b..85d5a44 100644 --- a/lib/views/home/settings/bookmarks/bookmark_state.dart +++ b/lib/views/home/settings/bookmarks/bookmark_state.dart @@ -1,7 +1,6 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; -import 'package:didvan/providers/core_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -41,16 +40,6 @@ class BookmarksState extends CoreProvier { void onMarkChanged(int id, bool value) { if (value) return; - final type = bookmarks.firstWhere((element) => element.id == id).type; - switch (type) { - case 'radar': - UserProvider.changeRadarMark(id, value); - break; - case 'news': - UserProvider.changeNewsMark(id, value); - break; - default: - } bookmarks.removeWhere((element) => element.id == id); notifyListeners(); } diff --git a/lib/views/home/settings/bookmarks/bookmarks.dart b/lib/views/home/settings/bookmarks/bookmarks.dart index 520d880..81ddeab 100644 --- a/lib/views/home/settings/bookmarks/bookmarks.dart +++ b/lib/views/home/settings/bookmarks/bookmarks.dart @@ -131,7 +131,15 @@ class _BookmarksState extends State { void _onCategorySelected(String type) { FocusScope.of(context).unfocus(); - Navigator.of(context).pushNamed(Routes.filteredBookmarks, arguments: type); + Navigator.of(context).pushNamed(Routes.filteredBookmarks, arguments: { + 'type': type, + 'onDeleted': (int id) { + final state = context.read(); + state.bookmarks + .removeWhere((element) => element.id == id && element.type == type); + state.update(); + }, + }); } void _onChanged(String value) { diff --git a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart index 2291e1a..0ecc547 100644 --- a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart +++ b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart @@ -1,7 +1,10 @@ +import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart'; import 'package:didvan/views/home/widgets/overview/news.dart'; +import 'package:didvan/views/home/widgets/overview/podcast.dart'; import 'package:didvan/views/home/widgets/overview/radar.dart'; +import 'package:didvan/views/home/widgets/overview/video.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/state_handlers/empty_list.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; @@ -9,7 +12,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class FilteredBookmarks extends StatefulWidget { - const FilteredBookmarks({Key? key}) : super(key: key); + final void Function(int id)? onDeleted; + const FilteredBookmarks({Key? key, this.onDeleted}) : super(key: key); @override _FilteredBookmarksState createState() => _FilteredBookmarksState(); @@ -67,8 +71,26 @@ class _FilteredBookmarksState extends State { hasUnmarkConfirmation: true, ); } - return NewsOverview( - news: state.bookmarks[index], + if (state.type == 'news') { + return NewsOverview( + news: state.bookmarks[index], + onMarkChanged: _onBookmarkChanged, + hasUnmarkConfirmation: true, + ); + } + if (state.type == 'podcast') { + return PodcastOverview( + studioRequestArgs: + const StudioRequestArgs(page: 0, type: 'podcast'), + podcast: state.bookmarks[index], + onMarkChanged: _onBookmarkChanged, + hasUnmarkConfirmation: true, + ); + } + return VideoOverview( + studioRequestArgs: + const StudioRequestArgs(page: 0, type: 'video'), + video: state.bookmarks[index], onMarkChanged: _onBookmarkChanged, hasUnmarkConfirmation: true, ); @@ -81,9 +103,10 @@ class _FilteredBookmarksState extends State { ); } - Future _onBookmarkChanged(int id, bool value) async { + Future _onBookmarkChanged(int id, bool value, bool shouldUpdate) async { if (value) return; final state = context.read(); state.onMarkChanged(id, false); + widget.onDeleted?.call(id); } } diff --git a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart index 7aa0463..0940883 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 @@ -1,13 +1,10 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; -import 'package:didvan/providers/core_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; class FilteredBookmarksState extends CoreProvier { - String search = ''; - String lastSearch = ''; final List bookmarks = []; final String type; int page = 1; @@ -15,17 +12,8 @@ class FilteredBookmarksState extends CoreProvier { FilteredBookmarksState(this.type); - bool get searching => search != ''; - 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'; @@ -56,15 +44,6 @@ class FilteredBookmarksState extends CoreProvier { } void onMarkChanged(int id, bool value) { - switch (type) { - case 'radar': - UserProvider.changeRadarMark(id, value); - break; - case 'news': - UserProvider.changeNewsMark(id, value); - break; - default: - } bookmarks.removeWhere((element) => element.id == id); notifyListeners(); } diff --git a/lib/views/home/settings/direct_list/direct_list.dart b/lib/views/home/settings/direct_list/direct_list.dart index 7c8d264..f0f0966 100644 --- a/lib/views/home/settings/direct_list/direct_list.dart +++ b/lib/views/home/settings/direct_list/direct_list.dart @@ -36,8 +36,11 @@ class _DirectListState extends State { title: 'پیام‌ها', trailing: state.unreadCount == 0 ? null - : DidvanBadge( - text: state.unreadCount.toString(), + : Padding( + padding: const EdgeInsets.only(left: 20), + child: DidvanBadge( + text: state.unreadCount.toString(), + ), ), ), slivers: [ diff --git a/lib/views/home/settings/direct_list/direct_list_state.dart b/lib/views/home/settings/direct_list/direct_list_state.dart index 728305a..40aaee9 100644 --- a/lib/views/home/settings/direct_list/direct_list_state.dart +++ b/lib/views/home/settings/direct_list/direct_list_state.dart @@ -1,6 +1,6 @@ import 'package:didvan/models/chat_room/chat_room.dart'; import 'package:didvan/models/enums.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -15,7 +15,6 @@ class DirectListState extends CoreProvier { } Future getDirectsList() async { - appState = AppState.busy; final RequestService service = RequestService(RequestHelper.directs); await service.httpGet(); if (service.isSuccess) { diff --git a/lib/views/home/settings/direct_list/widgets/direct_item.dart b/lib/views/home/settings/direct_list/widgets/direct_item.dart index 6aa9a45..da51414 100644 --- a/lib/views/home/settings/direct_list/widgets/direct_item.dart +++ b/lib/views/home/settings/direct_list/widgets/direct_item.dart @@ -3,10 +3,12 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/chat_room/chat_room.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/settings/direct_list/direct_list_state.dart'; import 'package:didvan/views/widgets/didvan/badge.dart'; import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class ChatRoomItem extends StatelessWidget { final ChatRoom chatRoom; @@ -20,7 +22,10 @@ class ChatRoomItem extends StatelessWidget { Routes.direct, arguments: {'type': chatRoom.type}, ); + final state = context.read(); + int unreadCount = chatRoom.unread; chatRoom.unread = 0; + state.unreadCount -= unreadCount; }, child: Container( color: Colors.transparent, diff --git a/lib/views/home/settings/general_settings/settings.dart b/lib/views/home/settings/general_settings/settings.dart index 6dcfd2c..41a9947 100644 --- a/lib/views/home/settings/general_settings/settings.dart +++ b/lib/views/home/settings/general_settings/settings.dart @@ -6,8 +6,9 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/models/view/app_bar_data.dart'; -import 'package:didvan/providers/theme_provider.dart'; +import 'package:didvan/providers/theme.dart'; import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/utils/date_time.dart'; import 'package:didvan/views/home/settings/general_settings/settings_state.dart'; import 'package:didvan/views/home/widgets/menu_item.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; @@ -36,6 +37,9 @@ class _GeneralSettingsState extends State { return 'کوچک'; } + int _intervalStart = 0; + int _intervalEnd = 24; + @override Widget build(BuildContext context) { return Consumer( @@ -50,7 +54,13 @@ class _GeneralSettingsState extends State { title: 'زمان دریافت اعلان', onTap: () => _pickTimeRange(context), icon: DidvanIcons.notification_regular, - suffix: state.notificationTimeRange[0], + suffix: DateTimeUtils.normalizeTimeDuration( + Duration(minutes: state.notificationTimeRange[1]), + ) + + ' - ' + + DateTimeUtils.normalizeTimeDuration( + Duration(minutes: state.notificationTimeRange[0]), + ), ), ), const ItemTitle( @@ -183,6 +193,9 @@ class _GeneralSettingsState extends State { } Future _pickTimeRange(BuildContext context) async { + final state = context.read(); + _intervalStart = state.notificationTimeRange[0]; + _intervalEnd = state.notificationTimeRange[1]; ActionSheetUtils.showBottomSheet( data: ActionSheetData( content: Row( @@ -198,6 +211,9 @@ class _GeneralSettingsState extends State { ), title: 'زمان دریافت اعلان', titleIcon: DidvanIcons.notification_regular, + onConfirmed: () { + state.notificationTimeRange = [_intervalStart, _intervalEnd]; + }, ), ); } @@ -222,7 +238,9 @@ class _GeneralSettingsState extends State { color: Theme.of(context).colorScheme.border, ), ), - child: DidvanText(state.notificationTimeRange[index]), + child: DidvanText(DateTimeUtils.normalizeTimeDuration( + Duration(minutes: index == 0 ? _intervalStart : _intervalEnd), + )), ), ), ), @@ -230,7 +248,6 @@ class _GeneralSettingsState extends State { } Future _openTimePicker(BuildContext context, int index) async { - final GeneralSettingsState state = context.read(); await Navigator.of(context).push( showPicker( okText: 'تایید', @@ -240,8 +257,9 @@ class _GeneralSettingsState extends State { cancelStyle: Theme.of(context).textTheme.bodyText2!, unselectedColor: Theme.of(context).colorScheme.text, blurredBackground: true, - hourLabel: 'ساعت', - minuteLabel: 'دقیقه', + disableMinute: true, + hourLabel: ':', + minuteLabel: '', is24HrFormat: true, iosStylePicker: true, minuteInterval: MinuteInterval.FIFTEEN, @@ -249,12 +267,11 @@ class _GeneralSettingsState extends State { value: const TimeOfDay(hour: 0, minute: 0), themeData: Theme.of(context), onChange: (time) { - state.notificationTimeRange = state.notificationTimeRange - ..replaceRange( - index, - index + 1, - ['${time.hour}:${time.minute}'], - ); + if (index == 0) { + _intervalStart = time.hour; + return; + } + _intervalEnd = time.hour; }, ), ); diff --git a/lib/views/home/settings/general_settings/settings_state.dart b/lib/views/home/settings/general_settings/settings_state.dart index 19d5d5b..8f5249f 100644 --- a/lib/views/home/settings/general_settings/settings_state.dart +++ b/lib/views/home/settings/general_settings/settings_state.dart @@ -1,5 +1,7 @@ import 'package:didvan/models/enums.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/storage/storage.dart'; class GeneralSettingsState extends CoreProvier { @@ -7,24 +9,26 @@ class GeneralSettingsState extends CoreProvier { getSettingsFromStorage(); } - List _notificationTimeRange = ['00:00', '23:59']; + List _notificationTimeRange = [0, 24]; String _fontFamily = 'Dana-FA'; double _fontSizeScale = 1; String _brightness = 'light'; - set notificationTimeRange(List value) { + set notificationTimeRange(List value) { _notificationTimeRange = value; StorageService.setValue( key: 'notificationTimeRangeStart', value: value[0], ); StorageService.setValue( - key: 'notificationTimeRangeStart', + key: 'notificationTimeRangeEnd', value: value[1], ); + notifyListeners(); + _setSilenceInterval(); } - List get notificationTimeRange => _notificationTimeRange; + List get notificationTimeRange => _notificationTimeRange; set fontFamily(String value) { _fontFamily = value; @@ -59,12 +63,27 @@ class GeneralSettingsState extends CoreProvier { String get brightness => _brightness; + Future _setSilenceInterval() async { + final service = RequestService(RequestHelper.silenceInterval, body: { + 'start': notificationTimeRange[0], + 'end': notificationTimeRange[1] + }); + await service.put(); + } + Future getSettingsFromStorage() async { appState = AppState.busy; - _notificationTimeRange[0] = - await StorageService.getValue(key: 'notificationTimeRangeStart'); - _notificationTimeRange[1] = - await StorageService.getValue(key: 'notificationTimeRangeEnd'); + try { + _notificationTimeRange[0] = int.parse( + await StorageService.getValue(key: 'notificationTimeRangeStart'), + ); + _notificationTimeRange[1] = int.parse( + await StorageService.getValue(key: 'notificationTimeRangeEnd'), + ); + } catch (e) { + notificationTimeRange = [0, 0]; + } + _fontFamily = await StorageService.getValue(key: 'fontFamily'); _brightness = await StorageService.getValue(key: 'brightness'); final scale = await StorageService.getValue(key: 'fontSizeScale'); diff --git a/lib/views/home/settings/profile/profile.dart b/lib/views/home/settings/profile/profile.dart index 5b666e7..ca9acf6 100644 --- a/lib/views/home/settings/profile/profile.dart +++ b/lib/views/home/settings/profile/profile.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:didvan/config/design_config.dart'; import 'package:didvan/models/view/app_bar_data.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/home/settings/profile/widgets/profile_photo.dart'; import 'package:didvan/views/home/widgets/menu_item.dart'; diff --git a/lib/views/home/settings/profile/widgets/profile_photo.dart b/lib/views/home/settings/profile/widgets/profile_photo.dart index 62f1018..00e1e01 100644 --- a/lib/views/home/settings/profile/widgets/profile_photo.dart +++ b/lib/views/home/settings/profile/widgets/profile_photo.dart @@ -5,7 +5,7 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/models/view/alert_data.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/home/widgets/menu_item.dart'; @@ -149,7 +149,7 @@ class _ProfilePhotoState extends State { cancelButtonTitle: 'بازگشت', ), androidUiSettings: const AndroidUiSettings(toolbarTitle: 'برش تصویر'), - compressQuality: 70, + compressQuality: 30, ); if (file == null) return; } diff --git a/lib/views/home/settings/settings.dart b/lib/views/home/settings/settings.dart index 63c4439..e923da5 100644 --- a/lib/views/home/settings/settings.dart +++ b/lib/views/home/settings/settings.dart @@ -1,6 +1,6 @@ import 'package:didvan/constants/app_icons.dart'; -import 'package:didvan/providers/theme_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/theme.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/storage/storage.dart'; import 'package:didvan/views/home/widgets/logo_app_bar.dart'; @@ -108,7 +108,7 @@ class Settings extends StatelessWidget { ), const SizedBox(height: 16), DidvanText( - 'نسخه نرم‌افزار: 1.1.4', + 'نسخه نرم‌افزار: 1.5.0', style: Theme.of(context).textTheme.caption, ), ], diff --git a/lib/views/home/studio/studio.dart b/lib/views/home/studio/studio.dart index 5f5f4ca..296a552 100644 --- a/lib/views/home/studio/studio.dart +++ b/lib/views/home/studio/studio.dart @@ -1,26 +1,227 @@ -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/assets.dart'; -import 'package:didvan/views/home/widgets/logo_app_bar.dart'; -import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; -import 'package:flutter/material.dart'; +import 'dart:async'; -class Studio extends StatelessWidget { +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/requests/studio.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; +import 'package:didvan/views/home/studio/widgets/slider.dart'; +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/overview/podcast.dart'; +import 'package:didvan/views/home/widgets/overview/video.dart'; +import 'package:didvan/views/home/widgets/search_field.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/radial_button.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_result.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Studio extends StatefulWidget { const Studio({Key? key}) : super(key: key); + @override + State createState() => _StudioState(); +} + +class _StudioState extends State { + final _focusNode = FocusNode(); + Timer? _timer; + + @override + void initState() { + context.read().init(); + super.initState(); + } + @override Widget build(BuildContext context) { - return Column( - children: [ - const LogoAppBar(), - Expanded( - child: EmptyState( - asset: Assets.emptyStudio, - title: 'استودیو آینده', - subtitle: 'به زودی...', - titleColor: Theme.of(context).colorScheme.title, + return Consumer( + builder: (context, state, child) => CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Row( + children: [ + const Expanded(child: LogoAppBar(type: 'studio')), + Padding( + padding: + EdgeInsets.only(top: MediaQuery.of(context).padding.top), + child: DidvanIconButton( + icon: DidvanIcons.bookmark_regular, + onPressed: () => Navigator.of(context).pushNamed( + Routes.filteredBookmarks, + arguments: {'type': state.type, 'onDeleted': (_) {}}, + ), + ), + ), + ], + ), + ), + const SliverToBoxAdapter( + child: StudioTabBar(), + ), + if (state.appState != AppState.failed) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SearchField( + title: 'استودیو', + onChanged: _onChanged, + focusNode: _focusNode, + ), + ), + ), + if (state.appState != AppState.failed) + SliverToBoxAdapter( + child: AnimatedVisibility( + isVisible: !state.searching, + duration: DesignConfig.lowAnimationDuration, + child: const StudioSlider(), + ), + ), + if (state.appState != AppState.failed && state.studios.isNotEmpty) + const SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: DidvanDivider( + verticalPadding: 0, + ), + ), + ), + if (state.appState != AppState.failed && state.studios.isNotEmpty) + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AnimatedVisibility( + isVisible: !state.searching, + duration: DesignConfig.lowAnimationDuration, + child: ItemTitle(title: state.orderString), + ), + DidvanIconButton( + gestureSize: 36, + icon: DidvanIcons.sort_regular, + onPressed: _showSortDialog, + ), + ], + ), + ), + ), + SliverStateHandler( + state: state, + itemPadding: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + emptyState: EmptyResult( + onNewSearch: () => _focusNode.requestFocus(), + ), + centerEmptyState: true, + enableEmptyState: state.studios.isEmpty, + placeholder: state.videosSelected + ? VideoOverview.placeHolder + : PodcastOverview.placeholder, + builder: (context, state, index) => state.videosSelected + ? VideoOverview( + onMarkChanged: state.changeMark, + hasUnmarkConfirmation: false, + video: state.studios[index], + studioRequestArgs: StudioRequestArgs( + page: state.page, + order: state.order, + search: state.search, + type: state.type, + asc: state.selectedSortTypeIndex == 1, + ), + ) + : PodcastOverview( + podcast: state.studios[index], + onMarkChanged: state.changeMark, + studioRequestArgs: StudioRequestArgs( + page: state.page, + order: state.order, + search: state.search, + type: state.type, + asc: state.selectedSortTypeIndex == 1, + ), + ), + childCount: state.studios.length, + onRetry: () => state.getStudios(page: 1), + ), + ], + ), + ); + } + + void _onChanged(String value) { + final state = context.read(); + if (value.length < 4 && value.isNotEmpty || state.lastSearch == value) { + return; + } + _timer?.cancel(); + _timer = Timer(const Duration(seconds: 1), () { + state.search = value; + state.getStudios(page: 1); + }); + } + + void _showSortDialog() { + final state = context.read(); + ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: StatefulBuilder( + builder: (context, setState) => Column( + children: [ + DidvanRadialButton( + title: 'تازه‌ترین‌ها', + onSelected: () => setState( + () => state.selectedSortTypeIndex = 0, + ), + value: state.selectedSortTypeIndex == 0, + ), + const SizedBox(height: 24), + DidvanRadialButton( + title: 'قدیمی‌ترین‌ها', + onSelected: () => setState( + () => state.selectedSortTypeIndex = 1, + ), + value: state.selectedSortTypeIndex == 1, + ), + const SizedBox(height: 24), + DidvanRadialButton( + title: 'پربازدیدترین‌ها', + onSelected: () => setState( + () => state.selectedSortTypeIndex = 2, + ), + value: state.selectedSortTypeIndex == 2, + ), + const SizedBox(height: 24), + DidvanRadialButton( + title: 'پربحث‌ترین‌ها', + onSelected: () => setState( + () => state.selectedSortTypeIndex = 3, + ), + value: state.selectedSortTypeIndex == 3, + ), + ], ), ), - ], + title: 'مرتب‌‌سازی', + titleIcon: DidvanIcons.sort_regular, + hasDismissButton: false, + confrimTitle: 'مرتب سازی', + onConfirmed: () => state.getStudios(page: 1), + ), ); } } diff --git a/lib/views/home/studio/studio_details/studio_details.dart b/lib/views/home/studio/studio_details/studio_details.dart deleted file mode 100644 index 20ce528..0000000 --- a/lib/views/home/studio/studio_details/studio_details.dart +++ /dev/null @@ -1,170 +0,0 @@ -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.mobile.dart b/lib/views/home/studio/studio_details/studio_details.mobile.dart new file mode 100644 index 0000000..2c34596 --- /dev/null +++ b/lib/views/home/studio/studio_details/studio_details.mobile.dart @@ -0,0 +1,140 @@ +import 'package:better_player/better_player.dart'; +import 'package:didvan/models/view/app_bar_data.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_widget.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/app_bar.dart'; +import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.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 { + int _currentlyPlayingId = 0; + late BetterPlayerController _betterPlayerController; + + @override + void initState() { + _betterPlayerController = BetterPlayerController( + const BetterPlayerConfiguration( + aspectRatio: 16 / 9, + showPlaceholderUntilPlay: true, + autoDispose: false, + fullScreenAspectRatio: 16 / 9, + ), + ); + final state = context.read(); + state.args = widget.pageData['args']; + Future.delayed( + Duration.zero, + () => state.getStudioDetails(widget.pageData['id']), + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final d = MediaQuery.of(context); + return Consumer( + builder: (context, state, child) => StateHandler( + state: state, + onRetry: () { + try { + state.getStudioDetails(state.studio.id); + } catch (e) { + state.getStudioDetails(widget.pageData['id']); + } + }, + builder: (context, state) { + if (_currentlyPlayingId != state.studio.id) { + _handleVideoPlayback(state); + } + return WillPopScope( + onWillPop: () async { + if (MediaService.currentPodcast != null) { + state.studio = MediaService.currentPodcast!; + } + return true; + }, + child: SafeArea( + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: DidvanAppBar( + appBarData: AppBarData( + trailing: BookmarkButton( + itemId: state.studio.id, + type: 'video', + value: state.studio.marked, + onMarkChanged: (value) { + widget.pageData['onMarkChanged']( + state.studio.id, value); + }, + gestureSize: 48, + ), + isSmall: true, + title: state.studio.title, + ), + ), + ), + body: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: SizedBox( + height: d.size.height - d.padding.top - 56, + child: Column( + children: [ + BetterPlayer(controller: _betterPlayerController), + Expanded( + child: StudioDetailsWidget( + onMarkChanged: (id, value) => widget + .pageData['onMarkChanged'](id, value, true), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + ); + } + + Future _handleVideoPlayback(state) async { + final betterPlayerDataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + state.studio.link, + ); + await _betterPlayerController.clearCache(); + await _betterPlayerController.setupDataSource(betterPlayerDataSource); + _betterPlayerController.setBetterPlayerControlsConfiguration( + BetterPlayerControlsConfiguration( + enablePlaybackSpeed: false, + enableSubtitles: false, + enableAudioTracks: false, + progressBarPlayedColor: Theme.of(context).colorScheme.secondary, + progressBarHandleColor: Theme.of(context).colorScheme.secondary, + ), + ); + _currentlyPlayingId = state.studio.id; + } + + @override + void dispose() { + _betterPlayerController.pause(); + _betterPlayerController.dispose(); + super.dispose(); + } +} diff --git a/lib/views/home/studio/studio_details/studio_details.web.dart b/lib/views/home/studio/studio_details/studio_details.web.dart new file mode 100644 index 0000000..b944905 --- /dev/null +++ b/lib/views/home/studio/studio_details/studio_details.web.dart @@ -0,0 +1,111 @@ +import 'dart:ui' as ui; + +import 'package:didvan/models/view/app_bar_data.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_widget.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/app_bar.dart'; +import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:universal_html/html.dart' as html; + +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 { + @override + void initState() { + final state = context.read(); + state.args = widget.pageData['args']; + Future.delayed( + Duration.zero, + () => state.getStudioDetails(widget.pageData['id']), + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final d = MediaQuery.of(context); + return Consumer( + builder: (context, state, child) => StateHandler( + state: state, + onRetry: () => state.getStudioDetails(state.studio.id), + builder: (context, state) { + // ignore: undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory( + "video", + (int viewId) => html.IFrameElement() + ..allowFullscreen = true + ..src = Uri.dataFromString( + '' + + state.studio.iframe!, + mimeType: 'text/html', + ).toString() + ..style.border = 'none', + ); + return WillPopScope( + onWillPop: () async { + if (MediaService.currentPodcast != null) { + state.studio = MediaService.currentPodcast!; + } + return true; + }, + child: SafeArea( + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: DidvanAppBar( + appBarData: AppBarData( + trailing: BookmarkButton( + itemId: state.studio.id, + type: 'video', + value: state.studio.marked, + onMarkChanged: (value) { + widget.pageData['onMarkChanged']( + state.studio.id, value); + }, + gestureSize: 48, + ), + isSmall: true, + title: state.studio.title, + ), + ), + ), + body: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: SizedBox( + height: d.size.height - d.padding.top - 56, + child: Column( + children: [ + const AspectRatio( + aspectRatio: 16 / 9, + child: HtmlElementView(viewType: 'video'), + ), + Expanded( + child: StudioDetailsWidget( + onMarkChanged: (id, value) => widget + .pageData['onMarkChanged'](id, value, 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 index 0a6205b..1d4e994 100644 --- a/lib/views/home/studio/studio_details/studio_details_state.dart +++ b/lib/views/home/studio/studio_details/studio_details_state.dart @@ -1,141 +1,170 @@ 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/providers/core.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 StudioDetailsData studio; + StudioDetailsData? nextStudio; + StudioDetailsData? prevStudio; late int initialIndex; late StudioRequestArgs args; - int _selectedDetailsIndex = 0; - bool isFetchingNewItem = false; + StudioRequestArgs? podcastArgs; final List relatedQueue = []; + bool _positionListenerActivated = false; + AppState alongSideState = AppState.idle; - int _currentIndex = 0; - int get currentIndex => _currentIndex; + int _selectedDetailsIndex = 0; + Timer? timer; + int timerValue = 10; + bool stopOnPodcastEnds = false; int get selectedDetailsIndex => _selectedDetailsIndex; set selectedDetailsIndex(int value) { _selectedDetailsIndex = value; + if (value == 2) { + getRelatedContents(); + } notifyListeners(); } - StudioDetailsData get currentStudio { - try { - return studios[_currentIndex]!; - } catch (e) { - return studios[_currentIndex + 1]!; - } - } - - Future getStudioDetails(int id, - {bool? isForward, StudioRequestArgs? args}) async { + Future getStudioDetails( + int id, { + StudioRequestArgs? args, + bool? isForward, + bool fetchOnly = false, + }) async { if (args != null) { this.args = args; } + if (this.args.type == 'podcast') { + podcastArgs = this.args; + } + if (MediaService.currentPodcast?.id == id && + this.args.type == 'podcast' && + !fetchOnly) { + return; + } + _selectedDetailsIndex = 0; + if (isForward != null) { + if (isForward) { + prevStudio = studio; + studio = nextStudio!; + nextStudio = null; + } else { + nextStudio = studio; + studio = prevStudio!; + prevStudio = null; + } + notifyListeners(); + _handlePodcastPlayback(studio); + } if (isForward == null) { + if (this.args.type == 'podcast') { + MediaService.audioPlayerTag = + 'podcast-${MediaService.currentPodcast?.id ?? ''}'; + } appState = AppState.busy; } else { - isFetchingNewItem = true; + alongSideState = AppState.busy; notifyListeners(); } final service = RequestService(RequestHelper.studioDetails(id, this.args)); await service.httpGet(); + nextStudio = null; + prevStudio = null; + if (stopOnPodcastEnds) { + timerValue = 10; + } + stopOnPodcastEnds = false; 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) { + studio = StudioDetailsData.fromJson(result['studio']); + if (result['nextStudio'].isNotEmpty && this.args.page != 0) { 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--; + if (result['prevStudio'].isNotEmpty && this.args.page != 0) { + prevStudio = StudioDetailsData.fromJson(result['prevStudio']); } - isFetchingNewItem = false; + if (isForward == null && !fetchOnly) { + await _handlePodcastPlayback(studio); + } + alongSideState = AppState.idle; appState = AppState.idle; return; } - //why? total page state shouldn't die! if (isForward == null) { appState = AppState.failed; + } else { + alongSideState = AppState.failed; + notifyListeners(); + } + } + + Future _handlePodcastPlayback(StudioDetailsData studio) async { + if (args.type == 'podcast') { + MediaService.currentPodcast = studio; + MediaService.podcastPlaylistArgs = args; + await MediaService.handleAudioPlayback( + audioSource: studio.link, + id: studio.id, + isVoiceMessage: false, + onTrackChanged: (isNext) { + if (isNext && nextStudio != null) { + getStudioDetails(nextStudio!.id); + } else if (!isNext && prevStudio != null) { + getStudioDetails(prevStudio!.id); + } + }, + ); + if (nextStudio != null && !_positionListenerActivated) { + _positionListenerActivated = true; + MediaService.audioPlayer.currentPosition.listen((event) { + if (MediaService.audioPlayerTag?.contains('message') == true) { + return; + } + final duration = + MediaService.duration ?? Duration(seconds: studio.duration); + if (event.compareTo(duration) > 0 && nextStudio != null) { + if (stopOnPodcastEnds) { + MediaService.resetAudioPlayer(); + return; + } + getStudioDetails(nextStudio!.id, isForward: true); + } + }); + } + } else { + MediaService.audioPlayer.pause(); } } Future getRelatedContents() async { - if (currentStudio.relatedContents.isNotEmpty) return; - relatedQueue.add(currentStudio.id); + if (studio.relatedContents.isNotEmpty) return; + relatedQueue.add(studio.id); final service = RequestService(RequestHelper.tag( - ids: currentStudio.tags.map((tag) => tag.id).toList(), - itemId: currentStudio.id, - type: 'studio', + ids: studio.tags.map((tag) => tag.id).toList(), + itemId: studio.id, + type: args.type, )); 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])); + studio.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; + studio.comments = count; notifyListeners(); } } diff --git a/lib/views/home/studio/studio_details/widgets/details_tab_bar.dart b/lib/views/home/studio/studio_details/widgets/details_tab_bar.dart new file mode 100644 index 0000000..ba160e7 --- /dev/null +++ b/lib/views/home/studio/studio_details/widgets/details_tab_bar.dart @@ -0,0 +1,126 @@ +import 'package:didvan/config/design_config.dart'; +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:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class DetailsTabBar extends StatelessWidget { + final bool isVideo; + + const DetailsTabBar({ + Key? key, + required this.isVideo, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return WillPopScope( + onWillPop: () async { + state.selectedDetailsIndex = 0; + return true; + }, + child: Container( + height: 72, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: const Color(0XFF1B3C59).withOpacity(0.15), + offset: const Offset(0, 8), + blurRadius: 8, + spreadRadius: 0, + ) + ], + ), + child: Row( + children: [ + _TabItem( + icon: DidvanIcons.description_solid, + title: 'توضیحات', + onTap: () => state.selectedDetailsIndex = 0, + isSelected: state.selectedDetailsIndex == 0, + isVideo: isVideo, + ), + _TabItem( + icon: DidvanIcons.chats_solid, + title: 'نظرات', + onTap: () { + state.selectedDetailsIndex = 1; + }, + isSelected: state.selectedDetailsIndex == 1, + isVideo: isVideo, + ), + _TabItem( + icon: DidvanIcons.puzzle_solid, + title: 'مطالب مرتبط', + onTap: () => state.selectedDetailsIndex = 2, + isSelected: state.selectedDetailsIndex == 2, + isVideo: isVideo, + ), + ], + ), + ), + ); + } +} + +class _TabItem extends StatelessWidget { + final IconData icon; + final String title; + final VoidCallback onTap; + final bool isSelected; + final bool isVideo; + const _TabItem({ + Key? key, + required this.icon, + required this.title, + required this.onTap, + required this.isSelected, + required this.isVideo, + }) : super(key: key); + + Color? _color(context) { + if (isSelected) { + if (isVideo) { + return Theme.of(context).colorScheme.secondary; + } + return Theme.of(context).colorScheme.focusedBorder; + } + return Theme.of(context).colorScheme.hint; + } + + @override + Widget build(BuildContext context) { + return Expanded( + child: GestureDetector( + onTap: onTap, + child: Container( + color: Colors.transparent, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: _color(context), + ), + AnimatedContainer( + duration: DesignConfig.lowAnimationDuration, + width: isSelected ? 64 : 0, + height: 1, + color: _color(context), + ), + DidvanText( + title, + color: _color(context), + style: Theme.of(context).textTheme.caption, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/home/studio/studio_details/widgets/studio_details.dart b/lib/views/home/studio/studio_details/widgets/studio_details.dart deleted file mode 100644 index 059b4cc..0000000 --- a/lib/views/home/studio/studio_details/widgets/studio_details.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; -import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; -import 'package:didvan/views/widgets/didvan/text.dart'; -import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class StudioDetailsWidget extends StatelessWidget { - const StudioDetailsWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, state, child) => StateHandler( - onRetry: () {}, - state: state, - builder: (context, state) => Container( - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _TabItem( - icon: DidvanIcons.description_solid, - title: 'توضیحات', - onTap: () => state.selectedDetailsIndex = 0, - isSelected: state.selectedDetailsIndex == 0, - ), - _TabItem( - icon: DidvanIcons.chats_solid, - title: 'نظرات', - onTap: () => state.selectedDetailsIndex = 1, - isSelected: state.selectedDetailsIndex == 1, - ), - _TabItem( - icon: DidvanIcons.puzzle_solid, - title: 'مطالب مرتبط', - onTap: () => state.selectedDetailsIndex = 2, - isSelected: state.selectedDetailsIndex == 2, - ), - ], - ), - const SizedBox(height: 16), - ], - ), - ), - ), - ); - } -} - -class _TabItem extends StatelessWidget { - final IconData icon; - final String title; - final VoidCallback onTap; - final bool isSelected; - const _TabItem({ - Key? key, - required this.icon, - required this.title, - required this.onTap, - required this.isSelected, - }) : super(key: key); - - Color? _color(context) => - isSelected ? Theme.of(context).colorScheme.focusedBorder : null; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - color: Colors.transparent, - child: Column( - children: [ - Icon( - icon, - color: _color(context), - ), - Container( - width: 64, - height: 1, - color: _color(context), - ), - DidvanText( - title, - color: _color(context), - style: Theme.of(context).textTheme.caption, - ) - ], - ), - ), - ); - } -} diff --git a/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart b/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart new file mode 100644 index 0000000..268309f --- /dev/null +++ b/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart @@ -0,0 +1,280 @@ +import 'dart:math'; + +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/studio_details_data.dart'; +import 'package:didvan/views/home/comments/comments.dart'; +import 'package:didvan/views/home/comments/comments_state.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; +import 'package:didvan/views/home/studio/studio_details/widgets/details_tab_bar.dart'; +import 'package:didvan/views/home/widgets/overview/multitype.dart'; +import 'package:didvan/views/home/widgets/tag_item.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.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_html/flutter_html.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class StudioDetailsWidget extends StatelessWidget { + final void Function(int id, bool value) onMarkChanged; + const StudioDetailsWidget({ + Key? key, + required this.onMarkChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final ds = MediaQuery.of(context).size; + + return SafeArea( + bottom: true, + child: Consumer( + builder: (context, state, child) { + bool isVideo = state.studio.iframe != null; + return Container( + height: max( + ds.height - + ds.width * 9 / 16 - + 72 - + MediaQuery.of(context).padding.top, + 0), + color: Theme.of(context).colorScheme.surface, + child: Stack( + children: [ + Positioned( + top: 72, + left: 0, + right: 0, + bottom: 0, + child: StateHandler( + onRetry: () {}, + state: state, + builder: (context, state) { + if (state.selectedDetailsIndex == 0) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Html( + key: ValueKey(state.studio.id), + data: state.studio.description, + 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, + ), + }, + ), + if (state.studio.tags.isNotEmpty) + const SizedBox(height: 20), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var i = 0; + i < state.studio.tags.length; + i++) + TagItem( + tag: state.studio.tags[i], + onMarkChanged: (id, value) => + _onMarkChanged(id, value, state), + type: isVideo ? 'video' : 'podcast', + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + if (state.nextStudio != null && + state.alongSideState == AppState.idle) + _StudioPreview( + isNext: true, + studio: state.nextStudio!, + ), + if (state.alongSideState == AppState.busy) + _StudioPreview.placeHolder, + if (state.prevStudio != null && + state.alongSideState == AppState.idle) + _StudioPreview( + isNext: false, + studio: state.prevStudio!, + ), + if (state.alongSideState == AppState.busy) + _StudioPreview.placeHolder, + const SizedBox(), + ], + ), + ], + ), + ); + } + if (state.selectedDetailsIndex == 1) { + return ChangeNotifierProvider( + create: (context) => CommentsState(), + child: SizedBox( + height: ds.height - + ds.width * 9 / 16 - + 172 - + MediaQuery.of(context).padding.top, + child: Comments( + pageData: { + 'id': state.studio.id, + 'type': 'studio', + 'title': state.studio.title, + 'onCommentsChanged': state.onCommentsChanged, + 'isPage': false, + }, + ), + ), + ); + } + return Column( + children: [ + if (state.studio.relatedContents.isEmpty) + for (var i = 0; i < 3; i++) + Padding( + padding: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + child: MultitypeOverview.placeholder, + ), + for (var i = 0; + i < state.studio.relatedContents.length; + i++) + Padding( + padding: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + child: MultitypeOverview( + item: state.studio.relatedContents[i], + onMarkChanged: (id, value) {}, + ), + ), + ], + ); + }, + ), + ), + DetailsTabBar( + isVideo: isVideo, + ), + ], + ), + ); + }, + ), + ); + } + + void _onMarkChanged(id, value, state) { + onMarkChanged(id, value); + if (state.studio.id == id) { + state.studio.marked = value; + } else if (state.nextStudio?.id == id) { + state.nextStudio!.marked = value; + } else if (state.prevStudio?.id == id) { + state.prevStudio!.marked = value; + } + } +} + +class _StudioPreview extends StatelessWidget { + final bool isNext; + final StudioDetailsData studio; + const _StudioPreview({ + Key? key, + required this.isNext, + required this.studio, + }) : super(key: key); + + String get _previewTitle { + if (studio.iframe != null) { + return 'ویدئو ${isNext ? 'بعدی' : 'قبلی'} '; + } + return 'پادکست ${isNext ? 'بعدی' : 'قبلی'} '; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + final state = context.read(); + state.getStudioDetails( + isNext ? state.nextStudio!.id : state.prevStudio!.id, + args: state.args, + isForward: isNext, + ); + }, + child: Container( + width: 88, + height: 216, + color: Colors.transparent, + child: Column( + children: [ + SkeletonImage( + imageUrl: studio.image, + aspectRatio: 1 / 1, + ), + const SizedBox(height: 8), + Icon( + isNext + ? DidvanIcons.angle_right_regular + : DidvanIcons.angle_left_regular, + ), + const SizedBox(height: 8), + DidvanText( + _previewTitle, + style: Theme.of(context).textTheme.caption, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + DidvanText( + studio.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + ), + ); + } + + static Widget get placeHolder => SizedBox( + width: 88, + height: 216, + child: Column( + children: const [ + ShimmerPlaceholder(width: 88, height: 88), + SizedBox(height: 8), + ShimmerPlaceholder(height: 20, width: 20), + SizedBox(height: 16), + ShimmerPlaceholder(height: 14, width: 60), + SizedBox(height: 16), + ShimmerPlaceholder(height: 12, width: double.infinity), + SizedBox(height: 8), + ShimmerPlaceholder(height: 12, width: 40), + ], + ), + ); +} diff --git a/lib/views/home/studio/studio_state.dart b/lib/views/home/studio/studio_state.dart index 18b7dc2..bb5b65a 100644 --- a/lib/views/home/studio/studio_state.dart +++ b/lib/views/home/studio/studio_state.dart @@ -1,16 +1,19 @@ +import 'dart:async'; + 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/models/slider_data.dart'; +import 'package:didvan/providers/core.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 sliders = []; - String? search; - String? lastSearch; + String search = ''; + String lastSearch = ''; int page = 1; int lastPage = 1; @@ -20,11 +23,32 @@ class StudioState extends CoreProvier { bool get videosSelected => _videosSelected; + bool get searching => search.isNotEmpty; + set videosSelected(bool value) { - if (_videosSelected == value) return; + if (_videosSelected == value || appState == AppState.busy) return; _videosSelected = value; - studios.clear(); - getStudioOverviews(page: page); + selectedSortTypeIndex = 0; + _getSliders(); + getStudios(page: page); + } + + String get order { + if (selectedSortTypeIndex == 0 || selectedSortTypeIndex == 1) return 'date'; + if (selectedSortTypeIndex == 2) return 'view'; + return 'comment'; + } + + String get orderString { + if (selectedSortTypeIndex == 0) return 'تازه‌ترین‌ها'; + if (selectedSortTypeIndex == 1) return 'قدیمی‌ترین‌ها'; + if (selectedSortTypeIndex == 2) return 'پربازدیدترین‌ها'; + return 'پربحث‌نرین‌ها'; + } + + String get type { + if (videosSelected) return 'video'; + return 'podcast'; } void init() { @@ -33,23 +57,29 @@ class StudioState extends CoreProvier { _videosSelected = true; selectedSortTypeIndex = 0; Future.delayed(Duration.zero, () { - getStudioOverviews(page: 1); + _getSliders(); + getStudios(page: 1); }); } - String get order { - if (selectedSortTypeIndex == 0) return 'date'; - if (selectedSortTypeIndex == 1) return 'view'; - return 'comment'; + Future _getSliders() async { + final service = RequestService( + RequestHelper.sudioSlider(type), + ); + await service.httpGet(); + if (service.isSuccess) { + sliders.clear(); + final sliderItems = service.result['studios']; + for (var i = 0; i < sliderItems.length; i++) { + sliders.add(SliderData.fromJson(sliderItems[i])); + } + } + notifyListeners(); } - String get type { - if (videosSelected) return 'video'; - return 'podcast'; - } - - Future getStudioOverviews({required int page}) async { + Future getStudios({required int page}) async { this.page = page; + lastSearch = search; if (page == 1) { appState = AppState.busy; } @@ -60,10 +90,10 @@ class StudioState extends CoreProvier { type: type, search: search, order: order, + asc: selectedSortTypeIndex == 1, ), ), ); - await service.httpGet(); if (service.isSuccess) { if (page == 1) { @@ -80,10 +110,11 @@ class StudioState extends CoreProvier { appState = AppState.failed; } - Future changeMark(int id, bool value) async { + Future changeMark(int id, bool value, bool shouldUpdate) async { studios.firstWhere((element) => element.id == id).marked = value; - notifyListeners(); - UserProvider.changeStudioMark(id, value); + if (shouldUpdate) { + notifyListeners(); + } } void onCommentsChanged(int id, int count) { diff --git a/lib/views/home/studio/widgets/slider.dart b/lib/views/home/studio/widgets/slider.dart index d312b2b..78bf110 100644 --- a/lib/views/home/studio/widgets/slider.dart +++ b/lib/views/home/studio/widgets/slider.dart @@ -1,28 +1,146 @@ import 'package:carousel_slider/carousel_slider.dart'; +import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/requests/studio.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; -class StudioSlider extends StatelessWidget { +class StudioSlider extends StatefulWidget { const StudioSlider({Key? key}) : super(key: key); + @override + State createState() => _StudioSliderState(); +} + +class _StudioSliderState extends State { + int selectedIndex = 0; + @override Widget build(BuildContext context) { + final state = context.watch(); return Column( children: [ CarouselSlider( items: [ - Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), - Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), - Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), - Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), + if (state.appState == AppState.busy) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: ShimmerPlaceholder(), + ), + if (state.appState == AppState.idle) + for (var i = 0; i < state.sliders.length; i++) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: GestureDetector( + onTap: () { + if (state.videosSelected) { + Navigator.of(context) + .pushNamed(Routes.studioDetails, arguments: { + 'onMarkChanged': state.changeMark, + 'id': state.sliders[i].id, + 'args': + const StudioRequestArgs(page: 0, type: 'video'), + 'hasUnmarkConfirmation': false, + 'isVideo': true, + }); + return; + } + context.read().getStudioDetails( + state.sliders[i].id, + args: const StudioRequestArgs( + page: 0, + type: 'podcast', + ), + ); + }, + child: Stack( + alignment: Alignment.center, + children: [ + SkeletonImage( + borderRadius: DesignConfig.mediumBorderRadius, + imageUrl: state.sliders[i].image, + width: double.infinity, + height: double.infinity, + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + decoration: BoxDecoration( + color: (state.videosSelected + ? Theme.of(context) + .colorScheme + .secondaryDisabled + : Theme.of(context).colorScheme.focused) + .withOpacity(0.9), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(10), + ), + ), + child: DidvanText( + state.sliders[i].title, + color: Theme.of(context).colorScheme.title, + style: Theme.of(context).textTheme.caption, + ), + ), + ), + if (state.videosSelected) + Container( + height: 52, + width: 52, + 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, + size: 48, + ), + ), + ], + ), + ), + ), ], options: CarouselOptions( + autoPlayAnimationDuration: DesignConfig.mediumAnimationDuration, + onPageChanged: (index, reason) => setState( + () => selectedIndex = index, + ), viewportFraction: 0.94, aspectRatio: 16 / 9, - autoPlay: true, + autoPlay: state.appState == AppState.idle, ), ), - Row(), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (var i = 0; i < state.sliders.length; i++) + _SliderIndicator( + isCurrentIndex: selectedIndex == i, + isVideo: state.videosSelected, + ), + ], + ), + const SizedBox(height: 16), ], ); } @@ -30,21 +148,33 @@ class StudioSlider extends StatelessWidget { class _SliderIndicator extends StatelessWidget { final bool isCurrentIndex; - const _SliderIndicator({Key? key, required this.isCurrentIndex}) - : super(key: key); + final bool isVideo; + const _SliderIndicator({ + Key? key, + required this.isCurrentIndex, + required this.isVideo, + }) : super(key: key); + + Color _color(BuildContext context) { + if (isVideo) { + return Theme.of(context).colorScheme.secondary; + } + return Theme.of(context).colorScheme.focusedBorder; + } @override Widget build(BuildContext context) { - return Container( + return AnimatedContainer( + duration: DesignConfig.lowAnimationDuration, height: 8, width: 8, + margin: const EdgeInsets.only(left: 4), decoration: BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.focusedBorder, + color: _color(context), ), shape: BoxShape.circle, - color: - isCurrentIndex ? Theme.of(context).colorScheme.focusedBorder : null, + color: isCurrentIndex ? _color(context) : null, ), ); } diff --git a/lib/views/home/studio/widgets/tab_bar.dart b/lib/views/home/studio/widgets/tab_bar.dart index 3d915a8..ca27288 100644 --- a/lib/views/home/studio/widgets/tab_bar.dart +++ b/lib/views/home/studio/widgets/tab_bar.dart @@ -20,9 +20,7 @@ class StudioTabBar extends StatelessWidget { padding: const EdgeInsets.all(4), decoration: BoxDecoration( border: Border.all( - color: state.videosSelected - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.border, ), borderRadius: DesignConfig.lowBorderRadius, ), @@ -78,7 +76,10 @@ class _StudioTypeButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: onTap, + onTap: () { + onTap(); + FocusScope.of(context).unfocus(); + }, child: Container( color: Colors.transparent, child: Column( @@ -88,19 +89,17 @@ class _StudioTypeButton extends StatelessWidget { size: 32, color: _color(context), ), - if (!isSelected) const SizedBox(height: 18), - if (isSelected) - Container( - width: 88, - height: 1, - color: _color(context), - ), - if (isSelected) - DidvanText( - title, - style: Theme.of(context).textTheme.overline, - color: _color(context), - ) + AnimatedContainer( + duration: DesignConfig.lowAnimationDuration, + width: isSelected ? 88 : 0, + height: 1, + color: _color(context), + ), + DidvanText( + title, + style: Theme.of(context).textTheme.overline, + color: _color(context), + ) ], ), ), diff --git a/lib/views/home/widgets/audio/audio_player_widget.dart b/lib/views/home/widgets/audio/audio_player_widget.dart index d369b10..79d1aff 100644 --- a/lib/views/home/widgets/audio/audio_player_widget.dart +++ b/lib/views/home/widgets/audio/audio_player_widget.dart @@ -1,15 +1,26 @@ +import 'dart:async'; +import 'dart:math'; + import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; import 'package:didvan/models/studio_details_data.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/services/media/media.dart'; +import 'package:didvan/utils/action_sheet.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/home/widgets/audio/audio_slider.dart'; import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/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/item_title.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class AudioPlayerWidget extends StatelessWidget { final StudioDetailsData podcast; @@ -17,6 +28,7 @@ class AudioPlayerWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final state = context.read(); return Container( decoration: BoxDecoration( borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), @@ -47,60 +59,126 @@ class AudioPlayerWidget extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: AudioSlider( - tag: podcast.media, + tag: 'podcast-${podcast.id}', 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, + Expanded( + child: Center( + child: StatefulBuilder( + builder: (context, setState) => Column( + children: [ + DidvanIconButton( + icon: state.timer == null && !state.stopOnPodcastEnds + ? DidvanIcons.sleep_timer_regular + : DidvanIcons.sleep_enabled_regular, + color: Theme.of(context).colorScheme.title, + onPressed: () => _showSleepTimer( + state, + () => setState(() {}), + ), ), + if (state.timer != null) + DidvanText( + state.stopOnPodcastEnds + ? 'پایان پادکست' + : '\'' + state.timerValue.toString(), + isEnglishFont: true, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.title, + ), + ], + ), + ), + ), + ), + Expanded( + child: Center( + child: Column( + children: [ + DidvanIconButton( + color: Theme.of(context).colorScheme.title, + size: 32, + icon: DidvanIcons.media_forward_solid, + onPressed: () { + MediaService.audioPlayer.seek( + Duration( + seconds: MediaService.audioPlayer.currentPosition + .value.inSeconds + + 30, + ), + ); + }, + ), + DidvanText( + '30', + isEnglishFont: true, + color: Theme.of(context).colorScheme.title, + ), + ], + ), + ), + ), + Expanded( + child: Center( + child: StreamBuilder( + stream: MediaService.audioPlayer.isPlaying, + builder: (context, snapshot) { + return _PlayPouseAnimatedIcon( + audioSource: podcast.link, + id: podcast.id, ); }, ), - 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, - ), - ); - }, + Expanded( + child: Center( + child: Column( + children: [ + DidvanIconButton( + size: 32, + icon: DidvanIcons.media_backward_solid, + color: Theme.of(context).colorScheme.title, + onPressed: () { + MediaService.audioPlayer.seek( + Duration( + seconds: max( + 0, + MediaService.audioPlayer.currentPosition.value + .inSeconds - + 10, + ), + ), + ); + }, + ), + DidvanText( + '10', + isEnglishFont: true, + color: Theme.of(context).colorScheme.title, + ), + ], ), - const DidvanText('10', isEnglishFont: true), - ], + ), ), - BookmarkButton( - gestureSize: 48, - value: podcast.marked, - onMarkChanged: (value) {}, + Expanded( + child: Center( + child: BookmarkButton( + itemId: state.studio.id, + type: 'podcast', + gestureSize: 48, + color: Theme.of(context).colorScheme.title, + value: podcast.marked, + onMarkChanged: (value) => context + .read() + .changeMark(podcast.id, value, true), + ), + ), ), ], ), @@ -108,11 +186,136 @@ class AudioPlayerWidget extends StatelessWidget { ), ); } + + Future _showSleepTimer(StudioDetailsState state, update) async { + int timerValue = 10; + final controller = FixedExtentScrollController(); + bool isInit = true; + Future.delayed( + const Duration(milliseconds: 100), + () async { + await controller.animateTo( + state.timerValue * 10, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ); + isInit = false; + }, + ); + await ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: StatefulBuilder( + builder: (context, setState) => Column( + children: [ + const ItemTitle( + title: 'زمان خواب', + icon: DidvanIcons.sleep_timer_regular, + ), + const SizedBox(height: 24), + DidvanText( + timerValue.toString() + ' دقیقه', + style: Theme.of(context).textTheme.headline3, + ), + const SizedBox(height: 12), + const Icon(DidvanIcons.caret_down_solid), + const SizedBox(height: 8), + SizedBox( + height: 50, + child: RotatedBox( + quarterTurns: 3, + child: ListWheelScrollView( + physics: const FixedExtentScrollPhysics(), + controller: controller, + itemExtent: 10, + onSelectedItemChanged: (index) { + if (!isInit) { + state.stopOnPodcastEnds = false; + } + final minutes = index == 0 ? 1 : index; + timerValue = minutes; + setState(() {}); + }, + children: [ + for (var i = 0; i < 61; i++) ...[ + if (i % 5 == 0) + Center( + child: Container( + color: Theme.of(context).colorScheme.text, + width: 50, + height: 3, + ), + ), + if (i % 5 != 0) const SizedBox(height: 3), + ], + ], + ), + ), + ), + const SizedBox(height: 32), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 64), + child: DidvanButton( + style: state.timerValue == MediaService.duration?.inMinutes && + state.stopOnPodcastEnds + ? ButtonStyleMode.primary + : ButtonStyleMode.flat, + title: 'پایان پادکست', + onPressed: () async { + state.timerValue = MediaService.duration!.inMinutes - + MediaService + .audioPlayer.currentPosition.value.inMinutes; + await controller.animateTo( + state.timerValue * 10, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ); + state.stopOnPodcastEnds = true; + setState(() {}); + }, + ), + ), + ], + ), + ), + onConfirmed: () { + if (!state.stopOnPodcastEnds) { + state.timer = Timer.periodic( + const Duration(minutes: 1), + (timer) { + timerValue--; + if (timerValue == 0) { + MediaService.audioPlayer.stop(); + state.stopOnPodcastEnds = false; + state.timer?.cancel(); + state.timer = null; + state.timerValue = 10; + state.update(); + } + }, + ); + } + state.timerValue = timerValue; + update(); + }, + confrimTitle: 'شروع زمان خواب', + dismissTitle: 'لغو', + onDismissed: () { + state.timer?.cancel(); + state.timer = null; + state.timerValue = 10; + update(); + }, + ), + ); + controller.dispose(); + } } class _PlayPouseAnimatedIcon extends StatefulWidget { final String audioSource; - const _PlayPouseAnimatedIcon({Key? key, required this.audioSource}) + final int id; + const _PlayPouseAnimatedIcon( + {Key? key, required this.audioSource, required this.id}) : super(key: key); @override @@ -123,6 +326,12 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> with SingleTickerProviderStateMixin { late final AnimationController _animationController; + @override + void didUpdateWidget(covariant _PlayPouseAnimatedIcon oldWidget) { + _handleAnimation(); + super.didUpdateWidget(oldWidget); + } + @override void initState() { super.initState(); @@ -130,8 +339,13 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> vsync: this, duration: DesignConfig.lowAnimationDuration, ); - if (MediaService.audioPlayer.playing) { + } + + void _handleAnimation() { + if (MediaService.audioPlayer.isPlaying.value) { _animationController.forward(); + } else { + _animationController.reverse(); } } @@ -143,12 +357,9 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> MediaService.handleAudioPlayback( audioSource: widget.audioSource, isVoiceMessage: false, + id: widget.id, ); - if (MediaService.audioPlayer.playing) { - _animationController.forward(); - } else { - _animationController.reverse(); - } + _handleAnimation(); }, child: Container( padding: const EdgeInsets.all(8), diff --git a/lib/views/home/widgets/audio/audio_slider.dart b/lib/views/home/widgets/audio/audio_slider.dart index 9c80349..bf8969e 100644 --- a/lib/views/home/widgets/audio/audio_slider.dart +++ b/lib/views/home/widgets/audio/audio_slider.dart @@ -22,11 +22,14 @@ class AudioSlider extends StatelessWidget { @override Widget build(BuildContext context) { return IgnorePointer( - ignoring: MediaService.audioPlayerTag != tag, + ignoring: !_isPlaying, child: Directionality( textDirection: TextDirection.ltr, child: StreamBuilder( - stream: _isPlaying ? MediaService.audioPlayer.positionStream : null, + stream: + _isPlaying && MediaService.audioPlayer.currentPosition.hasValue + ? MediaService.audioPlayer.currentPosition + : null, builder: (context, snapshot) => ProgressBar( thumbColor: Theme.of(context).colorScheme.title, progressBarColor: DesignConfig.isDark @@ -34,17 +37,14 @@ class AudioSlider extends StatelessWidget { : 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), + total: MediaService.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, + color: Theme.of(context).colorScheme.text, fontFamily: DesignConfig.fontFamily.replaceAll( '-FA', '', diff --git a/lib/views/home/widgets/bnb.dart b/lib/views/home/widgets/bnb.dart deleted file mode 100644 index a7651be..0000000 --- a/lib/views/home/widgets/bnb.dart +++ /dev/null @@ -1,292 +0,0 @@ -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; - final void Function(int index) onTabChanged; - - const DidvanBNB( - {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( - stream: MediaService.audioPlayer.playingStream, - builder: (context, snapshot) { - return Stack( - children: [ - 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), - ), - ), - 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( - bottom: 0, - left: 0, - right: 0, - child: Container( - height: 72, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(16)), - boxShadow: DesignConfig.defaultShadow, - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - _NavBarItem( - isSelected: currentTabIndex == 0, - title: 'اخبار', - selectedIcon: DidvanIcons.news_solid, - unselectedIcon: DidvanIcons.news_light, - onTap: () => onTabChanged(0), - ), - _NavBarItem( - isSelected: currentTabIndex == 1, - title: 'آمار', - selectedIcon: DidvanIcons.chart_solid, - unselectedIcon: DidvanIcons.chart_light, - onTap: () => onTabChanged(1), - ), - _NavBarItem( - isSelected: currentTabIndex == 2, - title: 'رادار', - selectedIcon: DidvanIcons.radar_solid, - unselectedIcon: DidvanIcons.radar_light, - onTap: () => onTabChanged(2), - ), - _NavBarItem( - isSelected: currentTabIndex == 3, - title: 'استودیو', - selectedIcon: DidvanIcons.play_circle_solid, - unselectedIcon: DidvanIcons.play_circle_light, - onTap: () => onTabChanged(3), - ), - _NavBarItem( - isSelected: currentTabIndex == 4, - title: 'تنظیمات', - selectedIcon: DidvanIcons.setting_solid, - unselectedIcon: DidvanIcons.setting_light, - onTap: () => onTabChanged(4), - ), - ], - ), - ), - ), - ], - ); - }); - } - - 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 { - final VoidCallback onTap; - final bool isSelected; - final String title; - final IconData selectedIcon; - final IconData unselectedIcon; - const _NavBarItem({ - Key? key, - required this.isSelected, - required this.title, - required this.selectedIcon, - required this.unselectedIcon, - required this.onTap, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Expanded( - child: Tooltip( - message: title, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.title, - borderRadius: DesignConfig.highBorderRadius, - boxShadow: DesignConfig.defaultShadow, - ), - child: GestureDetector( - onTap: onTap, - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 4, - ), - AnimatedContainer( - padding: const EdgeInsets.all(4), - duration: DesignConfig.lowAnimationDuration, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isSelected - ? Theme.of(context).colorScheme.focused - : Theme.of(context).colorScheme.surface, - ), - child: Icon( - isSelected ? selectedIcon : unselectedIcon, - size: 32, - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.text - : Theme.of(context).colorScheme.title, - ), - ), - DidvanText( - title, - style: Theme.of(context).textTheme.caption, - color: Theme.of(context).colorScheme.title, - ), - const Spacer(), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/views/home/widgets/bookmark_button.dart b/lib/views/home/widgets/bookmark_button.dart index ca98b06..79939d5 100644 --- a/lib/views/home/widgets/bookmark_button.dart +++ b/lib/views/home/widgets/bookmark_button.dart @@ -1,5 +1,7 @@ +import 'package:didvan/config/design_config.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; @@ -7,15 +9,21 @@ import 'package:flutter/material.dart'; class BookmarkButton extends StatefulWidget { final bool value; + final Color? color; final void Function(bool value) onMarkChanged; final bool askForConfirmation; final double gestureSize; + final String type; + final int itemId; const BookmarkButton({ Key? key, required this.value, required this.onMarkChanged, - this.askForConfirmation = false, required this.gestureSize, + required this.type, + required this.itemId, + this.askForConfirmation = false, + this.color, }) : super(key: key); @override @@ -41,6 +49,10 @@ class _BookmarkButtonState extends State { Widget build(BuildContext context) { return DidvanIconButton( gestureSize: widget.gestureSize, + color: widget.color ?? + (DesignConfig.isDark || !_value + ? null + : Theme.of(context).colorScheme.primary), icon: _value ? DidvanIcons.bookmark_solid : DidvanIcons.bookmark_regular, onPressed: () async { bool confirm = false; @@ -62,6 +74,21 @@ class _BookmarkButtonState extends State { _value = !_value; }); widget.onMarkChanged(_value); + switch (widget.type) { + case 'radar': + UserProvider.changeRadarMark(widget.itemId, _value); + break; + case 'news': + UserProvider.changeNewsMark(widget.itemId, _value); + break; + case 'podcast': + UserProvider.changeStudioMark(widget.itemId, _value); + break; + case 'video': + UserProvider.changeStudioMark(widget.itemId, _value); + break; + default: + } } }, ); diff --git a/lib/views/home/widgets/duration_widget.dart b/lib/views/home/widgets/duration_widget.dart index 025224d..b27f210 100644 --- a/lib/views/home/widgets/duration_widget.dart +++ b/lib/views/home/widgets/duration_widget.dart @@ -19,11 +19,10 @@ class DurationWidget extends StatelessWidget { borderRadius: BorderRadius.circular(5), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( DidvanIcons.timer_regular, - size: 16, + size: 18, color: Theme.of(context).colorScheme.focusedBorder, ), const SizedBox(width: 4), @@ -32,12 +31,13 @@ class DurationWidget extends StatelessWidget { Duration(seconds: duration), ), isEnglishFont: true, + style: Theme.of(context).textTheme.caption, color: Theme.of(context).colorScheme.focusedBorder, ), const SizedBox(width: 4), Icon( DidvanIcons.play_circle_regular, - size: 16, + size: 18, 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 28a88e4..754e40d 100644 --- a/lib/views/home/widgets/floating_navigation_bar.dart +++ b/lib/views/home/widgets/floating_navigation_bar.dart @@ -104,6 +104,11 @@ class _FloatingNavigationBarState extends State { const Spacer(), if (widget.isRadar) BookmarkButton( + itemId: widget.item.id, + type: 'radar', + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.focusedBorder + : Theme.of(context).colorScheme.focused, askForConfirmation: widget.hasUnmarkConfirmation, value: widget.item.marked, onMarkChanged: (value) { @@ -130,7 +135,7 @@ class _FloatingNavigationBarState extends State { Routes.comments, arguments: { 'id': widget.item.id, - 'isRadar': widget.isRadar, + 'type': widget.isRadar ? 'radar' : 'news', 'title': widget.item.title, 'onCommentsChanged': widget.onCommentsChanged, }, @@ -143,6 +148,11 @@ class _FloatingNavigationBarState extends State { if (!widget.isRadar) const SizedBox(width: 12), if (!widget.isRadar) BookmarkButton( + itemId: widget.item.id, + type: 'news', + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.focusedBorder + : Theme.of(context).colorScheme.focused, askForConfirmation: widget.hasUnmarkConfirmation, value: widget.item.marked, onMarkChanged: (value) { diff --git a/lib/views/home/widgets/overview/multitype.dart b/lib/views/home/widgets/overview/multitype.dart index 0076a05..136d78e 100644 --- a/lib/views/home/widgets/overview/multitype.dart +++ b/lib/views/home/widgets/overview/multitype.dart @@ -3,13 +3,17 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:provider/provider.dart'; class MultitypeOverview extends StatelessWidget { final OverviewData item; @@ -23,20 +27,60 @@ class MultitypeOverview extends StatelessWidget { this.hasUnmarkConfirmation = false, }) : super(key: key); + get _targetPageArgs { + if (item.type == 'radar') { + return const RadarRequestArgs(page: 0); + } + if (item.type == 'news') { + return const NewsRequestArgs(page: 0); + } + return StudioRequestArgs(page: 0, type: item.type); + } + + String get _targetPageRouteName { + if (item.type == 'radar') { + return Routes.radarDetails; + } + if (item.type == 'news') { + return Routes.newsDetails; + } + return Routes.studioDetails; + } + + IconData get _icon { + if (item.type == 'radar') { + return DidvanIcons.radar_light; + } + if (item.type == 'news') { + return DidvanIcons.news_light; + } + if (item.type == 'video') { + return DidvanIcons.video_light; + } + return DidvanIcons.podcast_light; + } + @override Widget build(BuildContext context) { return DidvanCard( - onTap: () => Navigator.of(context).pushNamed( - item.type == 'radar' ? Routes.radarDetails : Routes.newsDetails, - arguments: { - 'onMarkChanged': onMarkChanged, - 'id': item.id, - 'args': item.type == 'radar' - ? const RadarRequestArgs(page: 0) - : const NewsRequestArgs(page: 0), - 'hasUnmarkConfirmation': hasUnmarkConfirmation, - }, - ), + onTap: () { + if (item.type == 'podcast') { + context.read().getStudioDetails( + item.id, + args: StudioRequestArgs(page: 0, type: item.type), + ); + return; + } + Navigator.of(context).pushNamed( + _targetPageRouteName, + arguments: { + 'onMarkChanged': onMarkChanged, + 'id': item.id, + 'args': _targetPageArgs, + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + }, + ); + }, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -52,9 +96,7 @@ class MultitypeOverview extends StatelessWidget { ), ), child: Icon( - item.type == 'radar' - ? DidvanIcons.radar_light - : DidvanIcons.news_light, + _icon, color: Theme.of(context).colorScheme.white, size: 18, ), @@ -86,7 +128,22 @@ class MultitypeOverview extends StatelessWidget { DateTime.parse(item.createdAt).toPersianDateStr(), style: Theme.of(context).textTheme.overline, ), - // DidvanText('text'), + const Spacer(), + if ((item.timeToRead ?? item.duration) != null) ...[ + const Icon( + DidvanIcons.timer_light, + size: 18, + ), + const SizedBox(width: 4), + DidvanText( + item.timeToRead != null + ? 'خواندن در ${item.timeToRead} دقیقه' + : DateTimeUtils.normalizeTimeDuration( + Duration(seconds: item.duration!), + ), + style: Theme.of(context).textTheme.overline, + ), + ] ], ), ], diff --git a/lib/views/home/widgets/overview/news.dart b/lib/views/home/widgets/overview/news.dart index ac163c2..6a8db98 100644 --- a/lib/views/home/widgets/overview/news.dart +++ b/lib/views/home/widgets/overview/news.dart @@ -13,7 +13,7 @@ import 'package:flutter/material.dart'; class NewsOverview extends StatelessWidget { final OverviewData news; final NewsRequestArgs? newsRequestArgs; - final void Function(int id, bool value) onMarkChanged; + final void Function(int id, bool value, bool shouldUpdate) onMarkChanged; final bool hasUnmarkConfirmation; const NewsOverview({ Key? key, @@ -29,7 +29,7 @@ class NewsOverview extends StatelessWidget { onTap: () => Navigator.of(context).pushNamed( Routes.newsDetails, arguments: { - 'onMarkChanged': onMarkChanged, + 'onMarkChanged': (id, value) => onMarkChanged(id, value, true), 'id': news.id, 'args': newsRequestArgs, 'hasUnmarkConfirmation': hasUnmarkConfirmation, @@ -79,9 +79,11 @@ class NewsOverview extends StatelessWidget { ], ), BookmarkButton( - gestureSize: 24, + itemId: news.id, + type: 'news', + gestureSize: 32, value: news.marked, - onMarkChanged: (value) => onMarkChanged(news.id, value), + onMarkChanged: (value) => onMarkChanged(news.id, value, false), askForConfirmation: hasUnmarkConfirmation, ), ], diff --git a/lib/views/home/widgets/overview/podcast.dart b/lib/views/home/widgets/overview/podcast.dart index 1a243df..24ce673 100644 --- a/lib/views/home/widgets/overview/podcast.dart +++ b/lib/views/home/widgets/overview/podcast.dart @@ -1,7 +1,9 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/studio.dart'; +import 'package:didvan/providers/media.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'; @@ -12,18 +14,21 @@ 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/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class PodcastOverview extends StatelessWidget { final OverviewData podcast; - final void Function(int id, bool value) onMarkChanged; - final StudioRequestArgs? studioRequestArgs; + final void Function(int id, bool value, bool shouldUpdate) onMarkChanged; + final StudioRequestArgs studioRequestArgs; + final bool hasUnmarkConfirmation; const PodcastOverview({ Key? key, required this.podcast, required this.onMarkChanged, - this.studioRequestArgs, + required this.studioRequestArgs, + this.hasUnmarkConfirmation = false, }) : super(key: key); @override @@ -71,28 +76,62 @@ class PodcastOverview extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const DidvanDivider(verticalPadding: 8), - Row( - children: [ - DurationWidget(duration: podcast.duration!), - const Spacer(), - DidvanIconButton( - gestureSize: 28, - icon: DidvanIcons.download_regular, - onPressed: () {}, - ), - const SizedBox(width: 16), - BookmarkButton( - gestureSize: 24, - value: podcast.marked, - onMarkChanged: (value) => onMarkChanged(podcast.id, value), - ), - ], + Consumer( + builder: (context, state, child) => Row( + children: [ + DurationWidget(duration: podcast.duration!), + const Spacer(), + if (!kIsWeb) ...[ + if (state.appState == AppState.idle || + !state.downloadQueue.contains(podcast.link)) + DidvanIconButton( + gestureSize: 28, + color: _isDownloaded + ? Theme.of(context).colorScheme.primary + : null, + icon: _isDownloaded + ? DidvanIcons.download_solid + : DidvanIcons.download_regular, + onPressed: _isDownloaded + ? () {} + : () => state.download( + fileName: 'podcast-${podcast.id}.mp3', + isVideo: false, + url: podcast.link!, + ), + ), + if (state.appState == AppState.busy && + state.downloadQueue.contains(podcast.link)) + const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + const SizedBox(width: 16), + ], + BookmarkButton( + itemId: podcast.id, + type: 'podcast', + askForConfirmation: hasUnmarkConfirmation, + gestureSize: 32, + value: podcast.marked, + onMarkChanged: (value) => + onMarkChanged(podcast.id, value, false), + ), + ], + ), ), ], ), ); } + bool get _isDownloaded { + return MediaProvider.downloadedItemIds.contains(podcast.id); + } + static Widget get placeholder => DidvanCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/views/home/widgets/overview/radar.dart b/lib/views/home/widgets/overview/radar.dart index cd88ff3..3d0e6ff 100644 --- a/lib/views/home/widgets/overview/radar.dart +++ b/lib/views/home/widgets/overview/radar.dart @@ -16,7 +16,7 @@ import 'package:flutter/material.dart'; class RadarOverview extends StatelessWidget { final OverviewData radar; final void Function(int id, int count) onCommentsChanged; - final void Function(int id, bool value) onMarkChanged; + final void Function(int id, bool value, bool shouldUpdate) onMarkChanged; final bool hasUnmarkConfirmation; final RadarRequestArgs? radarRequestArgs; const RadarOverview({ @@ -34,7 +34,7 @@ class RadarOverview extends StatelessWidget { onTap: () => Navigator.of(context).pushNamed( Routes.radarDetails, arguments: { - 'onMarkChanged': onMarkChanged, + 'onMarkChanged': (id, value) => onMarkChanged(id, value, true), 'onCommentsChanged': onCommentsChanged, 'id': radar.id, 'args': radarRequestArgs, @@ -102,13 +102,6 @@ class RadarOverview extends StatelessWidget { const DidvanDivider(), Row( children: [ - BookmarkButton( - gestureSize: 24, - value: radar.marked, - onMarkChanged: (value) => onMarkChanged(radar.id, value), - askForConfirmation: hasUnmarkConfirmation, - ), - const Spacer(), if (radar.comments != 0) DidvanText(radar.comments.toString()), const SizedBox(width: 4), DidvanIconButton( @@ -117,7 +110,7 @@ class RadarOverview extends StatelessWidget { onPressed: () => Navigator.of(context).pushNamed( Routes.comments, arguments: { - 'isRadar': true, + 'type': 'radar', 'title': radar.title, 'id': radar.id, 'onCommentsChanged': (count) => @@ -125,10 +118,19 @@ class RadarOverview extends StatelessWidget { }, ), ), - const SizedBox(width: 16), + // const SizedBox(width: 16), // const DidvanText('10'), // const SizedBox(width: 4), // const Icon(DidvanIcons.evaluation_regular), + const Spacer(), + BookmarkButton( + itemId: radar.id, + type: 'radar', + gestureSize: 32, + value: radar.marked, + onMarkChanged: (value) => onMarkChanged(radar.id, value, false), + askForConfirmation: hasUnmarkConfirmation, + ), ], ), ], diff --git a/lib/views/home/widgets/overview/video.dart b/lib/views/home/widgets/overview/video.dart index 3150b34..b70918b 100644 --- a/lib/views/home/widgets/overview/video.dart +++ b/lib/views/home/widgets/overview/video.dart @@ -4,31 +4,26 @@ 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 void Function(int id, bool value, bool shouldUpdate) onMarkChanged; final bool hasUnmarkConfirmation; - final StudioRequestArgs? studioRequestArgs; + final StudioRequestArgs studioRequestArgs; const VideoOverview({ Key? key, required this.video, - required this.onCommentsChanged, required this.onMarkChanged, - required this.hasUnmarkConfirmation, - this.studioRequestArgs, + required this.studioRequestArgs, + this.hasUnmarkConfirmation = false, }) : super(key: key); @override @@ -37,41 +32,34 @@ class VideoOverview extends StatelessWidget { onTap: () => Navigator.of(context).pushNamed( Routes.studioDetails, arguments: { - 'onMarkChanged': onMarkChanged, - 'onCommentsChanged': onCommentsChanged, + 'onMarkChanged': (id, value) => onMarkChanged(id, value, true), 'id': video.id, 'args': studioRequestArgs, 'hasUnmarkConfirmation': hasUnmarkConfirmation, 'isVideo': true, - 'state': context.read(), }, ), child: Row( children: [ Stack( + alignment: Alignment.center, 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, - ), - ), + 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, ), ), ], @@ -105,16 +93,21 @@ class VideoOverview extends StatelessWidget { children: [ DurationWidget(duration: video.duration!), const Spacer(), - DidvanIconButton( - gestureSize: 28, - icon: DidvanIcons.download_regular, - onPressed: () {}, - ), - const SizedBox(width: 16), + // DidvanIconButton( + // gestureSize: 28, + // icon: DidvanIcons.download_regular, + // onPressed: () => + // context.read().download(video.media!), + // ), + // const SizedBox(width: 16), BookmarkButton( - gestureSize: 24, + itemId: video.id, + type: 'video', + gestureSize: 32, value: video.marked, - onMarkChanged: (value) => onMarkChanged(video.id, value), + onMarkChanged: (value) => + onMarkChanged(video.id, value, false), + askForConfirmation: hasUnmarkConfirmation, ), ], ), diff --git a/lib/views/home/widgets/player_controller_button.dart b/lib/views/home/widgets/player_controller_button.dart deleted file mode 100644 index 7e2cc14..0000000 --- a/lib/views/home/widgets/player_controller_button.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:io'; - -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/widgets/didvan/icon_button.dart'; -import 'package:flutter/material.dart'; - -class AudioControllerButton extends StatelessWidget { - final String? audioUrl; - final File? audioFile; - - const AudioControllerButton({Key? key, this.audioUrl, this.audioFile}) - : super(key: key); - - bool get _nowPlaying => - MediaService.audioPlayerTag == audioUrl || - audioFile != null && MediaService.audioPlayerTag == audioFile!.path; - - @override - Widget build(BuildContext context) { - return DidvanIconButton( - icon: MediaService.audioPlayer.playing == true && _nowPlaying - ? DidvanIcons.pause_circle_solid - : DidvanIcons.play_circle_solid, - color: Theme.of(context).colorScheme.focusedBorder, - onPressed: () { - MediaService.handleAudioPlayback( - audioSource: audioFile ?? audioUrl, - ); - }, - ); - } -} diff --git a/lib/views/home/widgets/search_field.dart b/lib/views/home/widgets/search_field.dart index dea0aa1..9a979b7 100644 --- a/lib/views/home/widgets/search_field.dart +++ b/lib/views/home/widgets/search_field.dart @@ -125,6 +125,7 @@ class _SearchFieldState extends State { @override void dispose() { + widget.focusNode.removeListener(() {}); super.dispose(); } } diff --git a/lib/views/home/widgets/tag_item.dart b/lib/views/home/widgets/tag_item.dart index 1265361..56d14ef 100644 --- a/lib/views/home/widgets/tag_item.dart +++ b/lib/views/home/widgets/tag_item.dart @@ -9,18 +9,26 @@ import 'package:flutter/material.dart'; class TagItem extends StatelessWidget { final Tag tag; + final void Function(int id, bool value) onMarkChanged; + final String type; const TagItem({ Key? key, required this.tag, + required this.onMarkChanged, + required this.type, }) : super(key: key); @override Widget build(BuildContext context) { return InkWrapper( borderRadius: DesignConfig.lowBorderRadius, - onPressed: () => - Navigator.of(context).pushNamed(Routes.hashtag, arguments: tag), + onPressed: () => Navigator.of(context).pushNamed(Routes.hashtag, + arguments: { + 'tag': tag, + 'onMarkChanged': onMarkChanged, + 'type': type + }), child: Container( padding: const EdgeInsets.symmetric( vertical: 4, diff --git a/lib/views/splash/splash.dart b/lib/views/splash/splash.dart index 72e3b26..27943c1 100644 --- a/lib/views/splash/splash.dart +++ b/lib/views/splash/splash.dart @@ -2,9 +2,10 @@ import 'dart:developer'; import 'package:didvan/config/design_config.dart'; import 'package:didvan/main.dart'; -import 'package:didvan/providers/server_data_provider.dart'; -import 'package:didvan/providers/theme_provider.dart'; -import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/providers/media.dart'; +import 'package:didvan/providers/server_data.dart'; +import 'package:didvan/providers/theme.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/app_initalizer.dart'; import 'package:didvan/services/network/request.dart'; @@ -49,8 +50,8 @@ class _SplashState extends State { value: DesignConfig.systemUiOverlayStyle.copyWith( systemNavigationBarColor: Theme.of(context).colorScheme.background, ), - child: Scaffold( - body: Container( + child: Material( + child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(60), color: Theme.of(context).colorScheme.background, @@ -93,7 +94,6 @@ class _SplashState extends State { .removeWhere((key, value) => key == 'image-cache'); }); } - await AppInitializer.setupServices(); final settingsData = await AppInitializer.initilizeSettings(); final themeProvider = context.read(); themeProvider.themeMode = settingsData.themeMode; @@ -105,10 +105,12 @@ class _SplashState extends State { _isGettingThemeData = false; }), ); + await AppInitializer.setupServices(); final userProvider = context.read(); final String? token = await userProvider.setAndGetToken(); if (token != null) { log(token); + context.read().getDownloadsList(); RequestService.token = token; final result = await userProvider.getUserInfo(); if (!result) { diff --git a/lib/views/widgets/didvan/app_bar.dart b/lib/views/widgets/didvan/app_bar.dart index f7f00e3..81770c8 100644 --- a/lib/views/widgets/didvan/app_bar.dart +++ b/lib/views/widgets/didvan/app_bar.dart @@ -20,7 +20,7 @@ class DidvanAppBar extends StatelessWidget { return Container( height: appBarData.isSmall ? 56 : 72, width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.only(right: 4, left: 20), + padding: const EdgeInsets.only(right: 4), decoration: BoxDecoration( border: hasBorder ? Border( diff --git a/lib/views/widgets/didvan/bnb.dart b/lib/views/widgets/didvan/bnb.dart new file mode 100644 index 0000000..a89de8f --- /dev/null +++ b/lib/views/widgets/didvan/bnb.dart @@ -0,0 +1,413 @@ +import 'package:assets_audio_player/assets_audio_player.dart'; +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/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_widget.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; +import 'package:didvan/views/home/widgets/audio/audio_player_widget.dart'; +import 'package:didvan/views/home/widgets/audio/audio_slider.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:provider/provider.dart'; + +class DidvanBNB extends StatelessWidget { + final int currentTabIndex; + final void Function(int index) onTabChanged; + + const DidvanBNB( + {Key? key, required this.currentTabIndex, required this.onTabChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + const _PlayerNavBar(), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 72, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: [ + BoxShadow( + color: const Color(0XFF1B3C59).withOpacity(0.15), + blurRadius: 8, + spreadRadius: 0, + offset: const Offset(0, -8), + ) + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + _NavBarItem( + isSelected: currentTabIndex == 0, + title: 'اخبار', + selectedIcon: DidvanIcons.news_solid, + unselectedIcon: DidvanIcons.news_light, + onTap: () => onTabChanged(0), + ), + _NavBarItem( + isSelected: currentTabIndex == 1, + title: 'آمار', + selectedIcon: DidvanIcons.chart_solid, + unselectedIcon: DidvanIcons.chart_light, + onTap: () => onTabChanged(1), + ), + _NavBarItem( + isSelected: currentTabIndex == 2, + title: 'رادار', + selectedIcon: DidvanIcons.radar_solid, + unselectedIcon: DidvanIcons.radar_light, + onTap: () => onTabChanged(2), + ), + _NavBarItem( + isSelected: currentTabIndex == 3, + title: 'استودیو', + selectedIcon: DidvanIcons.play_circle_solid, + unselectedIcon: DidvanIcons.play_circle_light, + onTap: () => onTabChanged(3), + ), + _NavBarItem( + isSelected: currentTabIndex == 4, + title: 'تنظیمات', + selectedIcon: DidvanIcons.setting_solid, + unselectedIcon: DidvanIcons.setting_light, + onTap: () => onTabChanged(4), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _PlayerNavBar extends StatelessWidget { + const _PlayerNavBar({Key? key}) : super(key: key); + + bool _enablePlayerController(StudioDetailsState state) => + MediaService.currentPodcast != null || + (MediaService.audioPlayerTag?.contains('podcast') ?? false); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: MediaService.audioPlayer.isPlaying, + builder: (context, snapshot) => GestureDetector( + onTap: () => MediaService.currentPodcast == null + ? null + : _showPlayerBottomSheet(context), + child: Consumer( + builder: (context, state, child) => AnimatedContainer( + padding: const EdgeInsets.only(top: 12), + duration: DesignConfig.lowAnimationDuration, + height: _enablePlayerController(state) ? 128 : 72, + decoration: BoxDecoration( + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.navigation, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + alignment: Alignment.topCenter, + child: Builder(builder: (context) { + if (!_enablePlayerController(state)) return const SizedBox(); + if (state.appState == AppState.failed) { + Future.delayed(const Duration(seconds: 2), () { + MediaService.resetAudioPlayer(); + }); + return DidvanText( + 'اتصال اینترنت برقرار نمی‌باشد', + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.title + : Theme.of(context).colorScheme.secondCTA, + ); + } + if (MediaService.currentPodcast == null) { + return SizedBox( + height: 32, + child: Center( + child: SpinKitThreeBounce( + size: 18, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.title + : Theme.of(context).colorScheme.secondCTA, + ), + ), + ); + } + return SizedBox( + height: 56, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 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, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: DesignConfig.isDark + ? null + : Theme.of(context).colorScheme.secondCTA, + ), + AudioSlider( + disableThumb: true, + tag: MediaService.audioPlayerTag!, + ), + ], + ), + ), + StreamBuilder( + stream: MediaService.audioPlayer.onReadyToPlay, + builder: (context, snapshot) { + if (snapshot.data == null || + state.appState == AppState.busy) { + return Padding( + padding: const EdgeInsets.only( + top: 4, + left: 16, + right: 16, + ), + child: SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.title + : Theme.of(context).colorScheme.secondCTA, + ), + ), + ); + } + return const SizedBox(); + }, + ), + if (state.appState != AppState.busy && + snapshot.data != null) + 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: () { + if (state.args.type == 'video') { + state.getStudioDetails( + MediaService.currentPodcast!.id, + args: state.podcastArgs, + fetchOnly: true, + ); + } + MediaService.handleAudioPlayback( + audioSource: MediaService.currentPodcast!.link, + id: MediaService.currentPodcast!.id, + isVoiceMessage: false, + ); + }, + ), + ), + ], + ), + ); + }), + ), + ), + ), + ); + } + + void _showPlayerBottomSheet(BuildContext context) { + final sheetKey = GlobalKey(); + bool isExpanded = false; + final detailsState = context.read(); + if (detailsState.args.type == 'video') { + detailsState.getStudioDetails( + MediaService.currentPodcast!.id, + args: detailsState.podcastArgs, + fetchOnly: true, + ); + } + final state = context.read(); + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + isScrollControlled: true, + builder: (context) => ChangeNotifierProvider.value( + value: state, + child: Consumer( + builder: (context, state, child) => ExpandableBottomSheet( + key: sheetKey, + background: Align( + alignment: Alignment.bottomCenter, + child: Container( + height: MediaQuery.of(context).size.height * 0.7, + color: Theme.of(context).colorScheme.surface, + ), + ), + persistentHeader: GestureDetector( + onVerticalDragUpdate: (details) { + if (details.delta.dy > 10) { + Navigator.of(context).pop(); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AudioPlayerWidget( + podcast: MediaService.currentPodcast!, + ), + Container( + width: MediaQuery.of(context).size.width, + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + DidvanIconButton( + size: 32, + icon: DidvanIcons.angle_up_regular, + onPressed: () { + if (!isExpanded) { + sheetKey.currentState?.expand(); + isExpanded = true; + } else { + isExpanded = false; + sheetKey.currentState?.contract(); + } + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ], + ), + ), + expandableContent: state.appState == AppState.busy + ? Container( + height: MediaQuery.of(context).size.height / 2, + alignment: Alignment.center, + child: SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ), + ) + : StudioDetailsWidget( + onMarkChanged: (id, value) => + context.read().changeMark(id, value, true), + ), + ), + ), + ), + ); + } +} + +class _NavBarItem extends StatelessWidget { + final VoidCallback onTap; + final bool isSelected; + final String title; + final IconData selectedIcon; + final IconData unselectedIcon; + const _NavBarItem({ + Key? key, + required this.isSelected, + required this.title, + required this.selectedIcon, + required this.unselectedIcon, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Tooltip( + message: title, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.title, + borderRadius: DesignConfig.highBorderRadius, + boxShadow: DesignConfig.defaultShadow, + ), + child: GestureDetector( + onTap: onTap, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 4, + ), + AnimatedContainer( + padding: const EdgeInsets.all(4), + duration: DesignConfig.lowAnimationDuration, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.surface, + ), + child: Icon( + isSelected ? selectedIcon : unselectedIcon, + size: 32, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.text + : Theme.of(context).colorScheme.title, + ), + ), + DidvanText( + title, + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.title, + ), + const Spacer(), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart index fd3a8e9..a7cb236 100644 --- a/lib/views/widgets/didvan/page_view.dart +++ b/lib/views/widgets/didvan/page_view.dart @@ -21,6 +21,7 @@ class DidvanPageView extends StatefulWidget { final int initialIndex; final int currentIndex; final bool isRadar; + final void Function(int id, bool value) onMarkChanged; final ScrollController scrollController; final void Function(int index) onPageChanged; @@ -32,6 +33,7 @@ class DidvanPageView extends StatefulWidget { required this.onPageChanged, required this.isRadar, required this.currentIndex, + required this.onMarkChanged, }) : super(key: key); @override @@ -119,7 +121,11 @@ class _DidvanPageViewState extends State { runSpacing: 8, children: [ for (var i = 0; i < item.tags.length; i++) - TagItem(tag: item.tags[i]), + TagItem( + tag: item.tags[i], + onMarkChanged: widget.onMarkChanged, + type: widget.isRadar ? 'radar' : 'news', + ), ], ), ), diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index 0dabcf2..38f9dde 100644 --- a/lib/views/widgets/didvan/scaffold.dart +++ b/lib/views/widgets/didvan/scaffold.dart @@ -9,15 +9,21 @@ class DidvanScaffold extends StatefulWidget { final EdgeInsets padding; final Color? backgroundColor; final bool reverse; + final ScrollPhysics? physics; + final ScrollController? scrollController; + final bool showSliversFirst; const DidvanScaffold({ Key? key, this.slivers, required this.appBarData, this.children, + this.physics, this.padding = const EdgeInsets.symmetric(horizontal: 16), this.backgroundColor, this.reverse = false, + this.scrollController, + this.showSliversFirst = false, }) : super(key: key); @override @@ -25,7 +31,13 @@ class DidvanScaffold extends StatefulWidget { } class _DidvanScaffoldState extends State { - final _scrollController = ScrollController(); + late final ScrollController _scrollController; + + @override + void initState() { + _scrollController = widget.scrollController ?? ScrollController(); + super.initState(); + } @override Widget build(BuildContext context) { @@ -33,10 +45,13 @@ class _DidvanScaffoldState extends State { return Scaffold( backgroundColor: widget.backgroundColor, body: Padding( - padding: EdgeInsets.only(top: statusBarHeight), + padding: widget.appBarData == null + ? EdgeInsets.zero + : EdgeInsets.only(top: statusBarHeight), child: Stack( children: [ CustomScrollView( + physics: widget.physics, controller: _scrollController, reverse: widget.reverse, slivers: [ @@ -50,7 +65,7 @@ class _DidvanScaffoldState extends State { pinned: true, flexibleSpace: DidvanAppBar(appBarData: widget.appBarData!), ), - if (widget.children != null) + if (widget.children != null && !widget.showSliversFirst) SliverPadding( padding: widget.padding, sliver: SliverList( @@ -66,6 +81,16 @@ class _DidvanScaffoldState extends State { padding: widget.padding, sliver: widget.slivers![i], ), + if (widget.children != null && widget.showSliversFirst) + SliverPadding( + padding: widget.padding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.children![index], + childCount: widget.children!.length, + ), + ), + ), if (widget.reverse) SliverToBoxAdapter( child: SizedBox( diff --git a/lib/views/widgets/skeleton_image.dart b/lib/views/widgets/skeleton_image.dart index c59f431..0c16a7b 100644 --- a/lib/views/widgets/skeleton_image.dart +++ b/lib/views/widgets/skeleton_image.dart @@ -24,29 +24,23 @@ class SkeletonImage extends StatelessWidget { @override Widget build(BuildContext context) { return _aspectRatioGenerator( - child: CachedNetworkImage( - imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, - httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'}, - width: width, - height: height, - imageUrl: RequestHelper.baseUrl + imageUrl, - imageBuilder: (context, imageProvider) => ClipRRect( - borderRadius: borderRadius ?? DesignConfig.lowBorderRadius, - child: Image( - image: imageProvider, - fit: BoxFit.cover, - ), - ), - progressIndicatorBuilder: (context, url, progress) => - ShimmerPlaceholder( - borderRadius: borderRadius, + child: ClipRRect( + borderRadius: borderRadius, + child: CachedNetworkImage( + fit: BoxFit.cover, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'}, + width: width, + height: height, + imageUrl: RequestHelper.baseUrl + imageUrl, + placeholder: (context, _) => const ShimmerPlaceholder(), ), ), ); } Widget _aspectRatioGenerator({required Widget child}) => aspectRatio == null - ? SizedBox(key: ValueKey(imageUrl), child: child) + ? child : AspectRatio( key: ValueKey(imageUrl), aspectRatio: aspectRatio!, diff --git a/lib/views/widgets/state_handlers/sliver_state_handler.dart b/lib/views/widgets/state_handlers/sliver_state_handler.dart index cb762f5..1e70ddb 100644 --- a/lib/views/widgets/state_handlers/sliver_state_handler.dart +++ b/lib/views/widgets/state_handlers/sliver_state_handler.dart @@ -1,5 +1,5 @@ import 'package:didvan/models/enums.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/views/widgets/state_handlers/empty_connection.dart'; import 'package:flutter/material.dart'; @@ -40,7 +40,9 @@ class SliverStateHandler extends SliverList { if (enableEmptyState && state.appState == AppState.idle) { return Padding( padding: EdgeInsets.only( - top: centerEmptyState ? 120 : 20, + top: centerEmptyState + ? MediaQuery.of(context).size.height / 4 + : 20, bottom: 20, ), child: emptyState, diff --git a/lib/views/widgets/state_handlers/state_handler.dart b/lib/views/widgets/state_handlers/state_handler.dart index 513ac3f..0addd30 100644 --- a/lib/views/widgets/state_handlers/state_handler.dart +++ b/lib/views/widgets/state_handlers/state_handler.dart @@ -1,5 +1,5 @@ import 'package:didvan/models/enums.dart'; -import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/views/widgets/state_handlers/empty_connection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; diff --git a/pubspec.lock b/pubspec.lock index ab49287..f08bc28 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + assets_audio_player: + dependency: "direct main" + description: + name: assets_audio_player + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4+1" + assets_audio_player_web: + dependency: transitive + description: + name: assets_audio_player_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4+1" async: dependency: transitive description: @@ -8,13 +22,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" - audio_session: - dependency: transitive - description: - name: audio_session - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.6+1" audio_video_progress_bar: dependency: "direct main" description: @@ -22,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.10.0" + better_player: + dependency: "direct main" + description: + name: better_player + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.81" boolean_selector: dependency: transitive description: @@ -126,7 +140,7 @@ packages: name: day_night_time_picker url: "https://pub.dartlang.org" source: hosted - version: "1.0.4+1" + version: "1.0.5" expandable_bottom_sheet: dependency: "direct main" description: @@ -161,7 +175,7 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.13.1" + version: "1.14.0" firebase_core_platform_interface: dependency: transitive description: @@ -182,21 +196,21 @@ packages: name: firebase_messaging url: "https://pub.dartlang.org" source: hosted - version: "11.2.8" + version: "11.2.12" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.2.2" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web url: "https://pub.dartlang.org" source: hosted - version: "2.2.9" + version: "2.2.10" flutter: dependency: "direct main" description: flutter @@ -208,7 +222,7 @@ packages: name: flutter_blurhash url: "https://pub.dartlang.org" source: hosted - version: "0.6.0" + version: "0.6.4" flutter_cache_manager: dependency: transitive description: @@ -297,7 +311,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -315,6 +329,20 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_widget_from_html_core: + dependency: transitive + description: + name: flutter_widget_from_html_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.5+1" + fwfh_text_style: + dependency: transitive + description: + name: fwfh_text_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.7.3+1" graphs: dependency: transitive description: @@ -349,28 +377,28 @@ packages: name: image_cropper url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.5.1" image_picker: dependency: "direct main" description: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.8.4+4" + version: "0.8.4+11" image_picker_for_web: dependency: transitive description: name: image_picker_for_web url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.4.4" intl: dependency: transitive description: @@ -385,27 +413,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" - just_audio: - dependency: "direct main" - description: - name: just_audio - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.18" - just_audio_platform_interface: - dependency: transitive - description: - name: just_audio_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - just_audio_web: - dependency: transitive - description: - name: just_audio_web - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.2" lints: dependency: transitive description: @@ -482,49 +489,49 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.9" path_provider_android: dependency: transitive description: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.12" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.8" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" pedantic: dependency: transitive description: @@ -532,6 +539,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "9.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + url: "https://pub.dartlang.org" + source: hosted + version: "9.0.2+1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + url: "https://pub.dartlang.org" + source: hosted + version: "9.0.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.7.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" persian_datetime_picker: dependency: "direct main" description: @@ -594,7 +636,7 @@ packages: name: record url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.4" record_platform_interface: dependency: transitive description: @@ -648,14 +690,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.1" stack_trace: dependency: transitive description: @@ -683,7 +725,7 @@ packages: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -725,63 +767,63 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.18" + version: "6.0.20" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.14" + version: "6.0.15" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.14" + version: "6.0.15" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "3.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "3.0.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.9" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" uuid: dependency: transitive description: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.5" + version: "3.0.6" vector_math: dependency: transitive description: @@ -789,6 +831,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.2" + wakelock: + dependency: transitive + description: + name: wakelock + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.6" + wakelock_macos: + dependency: transitive + description: + name: wakelock_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + wakelock_platform_interface: + dependency: transitive + description: + name: wakelock_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + wakelock_web: + dependency: transitive + description: + name: wakelock_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + wakelock_windows: + dependency: transitive + description: + name: wakelock_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" webview_flutter: dependency: "direct main" description: @@ -802,7 +886,7 @@ packages: name: webview_flutter_android url: "https://pub.dartlang.org" source: hosted - version: "2.8.3" + version: "2.8.4" webview_flutter_platform_interface: dependency: transitive description: @@ -823,14 +907,14 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.3" + version: "2.5.1" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.0+1" xml: dependency: transitive description: @@ -840,4 +924,4 @@ packages: version: "5.3.1" sdks: dart: ">=2.16.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7bae68f..dfc7dfa 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.2.0+1 +version: 1.5.0+10 environment: sdk: ">=2.12.0 <3.0.0" @@ -40,7 +40,7 @@ dependencies: pin_code_fields: ^7.3.0 rive: ^0.7.33 image_picker: ^0.8.4+4 - day_night_time_picker: ^1.0.3+1 + day_night_time_picker: ^1.0.5 path_provider: ^2.0.8 flutter_spinkit: ^5.1.0 flutter_svg: ^1.0.0 @@ -50,7 +50,6 @@ dependencies: flutter_vibrate: ^1.3.0 universal_html: ^2.0.8 record: ^3.0.2 - just_audio: ^0.9.18 record_web: ^0.2.1 persian_datetime_picker: ^2.4.0 persian_number_utility: ^1.1.1 @@ -64,6 +63,9 @@ dependencies: firebase_core: ^1.13.1 webview_flutter: ^3.0.1 expandable_bottom_sheet: ^1.1.1+1 + permission_handler: ^9.2.0 + better_player: ^0.0.81 + assets_audio_player: ^3.0.4+1 dev_dependencies: @@ -90,42 +92,12 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - lib/assets/images/logos/logo-vertical-dark.svg - - lib/assets/images/logos/logo-vertical-light.svg - - lib/assets/images/logos/logo-horizontal-dark.svg - - lib/assets/images/logos/logo-horizontal-light.svg - - lib/assets/images/logos/studio-dark.svg - - lib/assets/images/logos/studio-light.svg - - lib/assets/images/categories/business-light.svg - - lib/assets/images/categories/economic-light.svg - - lib/assets/images/categories/enviromental-light.svg - - lib/assets/images/categories/political-light.svg - - lib/assets/images/categories/social-light.svg - - lib/assets/images/categories/tech-light.svg - - lib/assets/images/categories/business-dark.svg - - lib/assets/images/categories/economic-dark.svg - - lib/assets/images/categories/enviromental-dark.svg - - lib/assets/images/categories/political-dark.svg - - lib/assets/images/categories/social-dark.svg - - lib/assets/images/categories/tech-dark.svg - - lib/assets/images/themes/theme-light.svg - - lib/assets/images/themes/theme-dark.svg - - lib/assets/images/records/record-dark.svg - - lib/assets/images/records/record-light.svg - - lib/assets/images/empty_states/bookmark-light.svg - - lib/assets/images/empty_states/chart-light.svg - - lib/assets/images/empty_states/chat-light.svg - - lib/assets/images/empty_states/connection-light.svg - - lib/assets/images/empty_states/result-light.svg - - lib/assets/images/empty_states/studio-light.svg - - lib/assets/images/empty_states/bookmark-dark.svg - - lib/assets/images/empty_states/chart-dark.svg - - lib/assets/images/empty_states/chat-dark.svg - - lib/assets/images/empty_states/connection-dark.svg - - lib/assets/images/empty_states/result-dark.svg - - lib/assets/images/empty_states/studio-dark.svg - - lib/assets/animations/indicator-light.riv - - lib/assets/animations/indicator-dark.riv + - lib/assets/images/logos/ + - lib/assets/images/categories/ + - lib/assets/images/themes/ + - lib/assets/images/records/ + - lib/assets/images/empty_states/ + - lib/assets/animations/ - lib/assets/loading.gif diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..9913c8f --- /dev/null +++ b/release.sh @@ -0,0 +1,13 @@ +flutter clean +flutter build apk +cp build/app/outputs/flutter-apk/app-release.apk /users/arytan/desktop +flutter build web --web-renderer canvaskit +cd build/web +fandogh login --username didvan --password 12799721 +fandogh image publish --version $1 +fandogh service apply -f ../../.fandogh/fandogh.yaml +cd ../.. +flutter build ipa +xcodebuild -exportArchive -exportOptionsPlist ios/runner/info.plist -archivePath build/ios/archive/runner.xcarchive -exportPath /users/arytan/desktop/App.ipa +cp /users/arytan/desktop/didvan.ipa/didvan.ipa /users/arytan/desktop +echo "Done!" \ No newline at end of file diff --git a/web/index.html b/web/index.html index a2dfb65..b00093f 100644 --- a/web/index.html +++ b/web/index.html @@ -60,7 +60,7 @@ } scriptLoaded = true; var scriptTag = document.createElement('script'); - scriptTag.src = 'main.dart.js'; + scriptTag.src = `main.dart.js?version=${Math.random()}`; scriptTag.type = 'application/javascript'; document.body.append(scriptTag); }