diff --git a/android/app/build.gradle b/android/app/build.gradle index 293ad3a..efe0f78 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -65,5 +65,6 @@ flutter { } dependencies { + implementation platform('com.google.firebase:firebase-bom:29.1.0') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..5cb370e --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "935017686266", + "project_id": "didvan-9b7da", + "storage_bucket": "didvan-9b7da.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:935017686266:android:f9cbc9aba8e3d65ed2d543", + "android_client_info": { + "package_name": "com.didvan.didvanapp" + } + }, + "oauth_client": [ + { + "client_id": "935017686266-lebnol7rb05oi9h0mripb41c892d2gij.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBp-UHjWeM0H0UHtX5yguFKG-riMzvvCzw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "935017686266-lebnol7rb05oi9h0mripb41c892d2gij.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 09fbd64..125594d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,6 +7,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.google.gms:google-services:4.3.10' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/ios/Podfile b/ios/Podfile index 252d9ec..9411102 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '10.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 402e2e1..55fa4cf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,41 @@ PODS: - audio_session (0.0.1): - Flutter + - Firebase/CoreOnly (8.11.0): + - FirebaseCore (= 8.11.0) + - Firebase/Messaging (8.11.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 8.11.0) + - firebase_core (1.13.1): + - Firebase/CoreOnly (= 8.11.0) + - Flutter + - firebase_messaging (11.2.8): + - Firebase/Messaging (= 8.11.0) + - firebase_core + - Flutter + - FirebaseCore (8.11.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - FirebaseCoreDiagnostics (8.12.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - nanopb (~> 2.30908.0) + - FirebaseInstallations (8.12.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - PromisesObjC (< 3.0, >= 1.2) + - FirebaseMessaging (8.11.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Reachability (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - nanopb (~> 2.30908.0) - Flutter (1.0.0) - flutter_secure_storage (3.3.1): - Flutter @@ -9,6 +44,27 @@ PODS: - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) + - GoogleDataTransport (9.1.2): + - GoogleUtilities/Environment (~> 7.2) + - nanopb (~> 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.7.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.7.0)" + - GoogleUtilities/Reachability (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Logger - image_cropper (0.0.4): - Flutter - TOCropViewController (~> 2.6.1) @@ -16,8 +72,14 @@ PODS: - Flutter - just_audio (0.0.1): - Flutter + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) - path_provider_ios (0.0.1): - Flutter + - PromisesObjC (2.0.0) - record (0.0.1): - Flutter - sqflite (0.0.2): @@ -29,6 +91,8 @@ PODS: DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_vibrate (from `.symlinks/plugins/flutter_vibrate/ios`) @@ -42,12 +106,25 @@ DEPENDENCIES: SPEC REPOS: trunk: + - Firebase + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseInstallations + - FirebaseMessaging - FMDB + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC - TOCropViewController EXTERNAL SOURCES: audio_session: :path: ".symlinks/plugins/audio_session/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: :path: Flutter flutter_secure_storage: @@ -71,19 +148,30 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: audio_session: 4f3e461722055d21515cf3261b64c973c062f345 + Firebase: 44dd9724c84df18b486639e874f31436eaa9a20c + firebase_core: 08f6a85f62060111de5e98d6a214810d11365de9 + firebase_messaging: 36238f3d0b933af8c919aef608408aae06ba22e8 + FirebaseCore: 2f4f85b453cc8fea4bb2b37e370007d2bcafe3f0 + FirebaseCoreDiagnostics: 3b40dfadef5b90433a60ae01f01e90fe87aa76aa + FirebaseInstallations: 25764cf322e77f99449395870a65b2bef88e1545 + FirebaseMessaging: 02e248e8997f71fa8cc9d78e9d49ec1a701ba14a Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec flutter_vibrate: 9f4c2ab57008965f78969472367c329dd77eb801 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 image_cropper: 60c2789d1f1a78c873235d4319ca0c34a69f2d98 image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6 just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 + PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 record: 7ee2393532f8553bbb09fa19e95478323b7c0a99 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af -PODFILE CHECKSUM: a75497545d4391e2d394c3668e20cfb1c2bbd4aa +PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea COCOAPODS: 1.11.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index afb27d5..f2eb3d9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D194CE3E27D4A4740049AFC7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D194CE3D27D4A4740049AFC7 /* GoogleService-Info.plist */; }; E870A5F479A60D6704DD5DF2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75DBECA488F412614712FB74 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -47,6 +48,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 98ACB01D5FA5A78DB2686183 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; C97DED20C4A171F16FB949CD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D194CE3D27D4A4740049AFC7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,6 +114,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + D194CE3D27D4A4740049AFC7 /* GoogleService-Info.plist */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -189,6 +192,7 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + D194CE3E27D4A4740049AFC7 /* GoogleService-Info.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4..507864d 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,13 +1,30 @@ import UIKit import Flutter +import Firebase + @UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, MessagingDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + FirebaseApp.configure() + Messaging.messaging().delegate = self GeneratedPluginRegistrant.register(with: self) + if #available(iOS 10.0, *) { + // For iOS 10 display notification (sent via APNS) + UNUserNotificationCenter.current().delegate = self + let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] + UNUserNotificationCenter.current().requestAuthorization( + options: authOptions, + completionHandler: {_, _ in }) + } else { + let settings: UIUserNotificationSettings = + UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil) + application.registerUserNotificationSettings(settings) + } + application.registerForRemoteNotifications() return super.application(application, didFinishLaunchingWithOptions: launchOptions) } -} +} \ No newline at end of file diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..4bc4228 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,34 @@ + + + + + CLIENT_ID + 935017686266-54hu01v9cc5pqpgofo1gk2n3hegj4r2m.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.935017686266-54hu01v9cc5pqpgofo1gk2n3hegj4r2m + API_KEY + AIzaSyCMa-zg_uVhOfTnea5Klz6aPZlgHwVGj7U + GCM_SENDER_ID + 935017686266 + PLIST_VERSION + 1 + BUNDLE_ID + com.didvan.didvanapp + PROJECT_ID + didvan-9b7da + STORAGE_BUCKET + didvan-9b7da.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:935017686266:ios:de47638bd662463fd2d543 + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 4d67f3b..17f2a3a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -26,12 +26,14 @@ LaunchScreen UIMainStoryboardFile Main + FirebaseAppDelegateProxyEnabled + NSMicrophoneUsageDescription -We need to access to the microphone to record audio file -NSPhotoLibraryUsageDescription -We need to access to the user gallery to add user profile photo -NSCameraUsageDescription -We need to access to the user gallery to add user profile photo + We need to access to the microphone to record audio file + NSPhotoLibraryUsageDescription + We need to access to the user gallery to add user profile photo + NSCameraUsageDescription + We need to access to the user gallery to add user profile photo UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/lib/models/content.dart b/lib/models/content.dart index aa431e1..3ec25fe 100644 --- a/lib/models/content.dart +++ b/lib/models/content.dart @@ -4,6 +4,8 @@ class Content { final String? audio; final String? video; final String? image; + final String? largeImage; + final String? caption; const Content({ required this.order, @@ -11,6 +13,8 @@ class Content { required this.audio, required this.video, required this.image, + required this.largeImage, + required this.caption, }); factory Content.fromJson(Map json) => Content( @@ -19,6 +23,8 @@ class Content { audio: json['audio'], video: json['video'], image: json['image'], + largeImage: json['largeImage'], + caption: json['caption'], ); Map toJson() => { @@ -27,5 +33,7 @@ class Content { 'audio': audio, 'video': video, 'image': image, + 'largeImage': largeImage, + 'caption': caption, }; } diff --git a/lib/providers/server_data_provider.dart b/lib/providers/server_data_provider.dart index e9b02a5..3b76c8f 100644 --- a/lib/providers/server_data_provider.dart +++ b/lib/providers/server_data_provider.dart @@ -21,7 +21,7 @@ class ServerDataProvider { directTypes.add(MapEntry(types[i]['id'], types[i]['label'])); } } else { - throw 'Fetchin direct types failed!'; + throw 'Fetching direct types failed!'; } } } diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 9bf6300..205d9b7 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -25,8 +25,10 @@ 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_state.dart'; import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/routes/routes.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -57,6 +59,9 @@ class RouteGenerator { ChangeNotifierProvider( create: (context) => NewsState(), ), + ChangeNotifierProvider( + create: (context) => StudioState(), + ), ], child: const Home(), ), @@ -161,13 +166,20 @@ class RouteGenerator { static Route _createRoute(page) { return MaterialPageRoute( - builder: (context) => Container( - color: Theme.of(context).colorScheme.surface, - child: SafeArea( - child: page, - top: false, - ), - ), + builder: (context) { + final shortestSide = MediaQuery.of(context).size.shortestSide; + final bool useMobileLayout = shortestSide < 600; + if (kIsWeb && !useMobileLayout) { + return Center(child: AspectRatio(aspectRatio: 9 / 16, child: page)); + } + return Container( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + child: page, + top: false, + ), + ); + }, ); } } diff --git a/lib/services/app_initalizer.dart b/lib/services/app_initalizer.dart index 236fc19..7c21a4c 100644 --- a/lib/services/app_initalizer.dart +++ b/lib/services/app_initalizer.dart @@ -1,5 +1,7 @@ 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'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; @@ -10,6 +12,7 @@ class AppInitializer { StorageService.appDocsDir = (await getApplicationDocumentsDirectory()).path; StorageService.appTempsDir = (await getTemporaryDirectory()).path; + await _initializeFirebase(); MediaService.init(); } } @@ -42,4 +45,29 @@ class AppInitializer { return ThemeMode.light; } } + + static Future _initializeFirebase() async { + try { + await Firebase.initializeApp( + options: const FirebaseOptions( + apiKey: 'AIzaSyBp-UHjWeM0H0UHtX5yguFKG-riMzvvCzw', + appId: '1:935017686266:android:f9cbc9aba8e3d65ed2d543', + messagingSenderId: '935017686266', + projectId: 'didvan-9b7da', + ), + ); + } catch (e) { + Firebase.app(); + } + final FirebaseMessaging fcm = FirebaseMessaging.instance; + await fcm.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); + } } diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 7e173cf..fb0fe74 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -7,6 +7,8 @@ import 'package:just_audio/just_audio.dart'; class MediaService { static final AudioPlayer audioPlayer = AudioPlayer(); static String? audioPlayerTag; + static String? audioPlayerTitle; + static String? audioPlayerCover; static void init() { audioPlayer.positionStream.listen((event) { @@ -54,6 +56,11 @@ class MediaService { } } + static Future resetAudioPlayer() async { + audioPlayerTag = null; + MediaService.audioPlayer.stop(); + } + static Future pickImage({required ImageSource source}) async { final imagePicker = ImagePicker(); final XFile? pickedFile = await imagePicker.pickImage(source: source); diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index 1eb9a6f..58005be 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -34,7 +34,7 @@ class RequestHelper { _urlConcatGenerator([ MapEntry('limit', limit?.toString() ?? '3'), MapEntry('type', type), - MapEntry('id', itemId.toString()), + MapEntry('id', itemId?.toString() ?? '1'), MapEntry('tags', _urlListConcatGenerator(ids)) ]); diff --git a/lib/views/authentication/screens/username.dart b/lib/views/authentication/screens/username.dart index fcec2fd..e17f598 100644 --- a/lib/views/authentication/screens/username.dart +++ b/lib/views/authentication/screens/username.dart @@ -5,6 +5,7 @@ import 'package:didvan/views/widgets/didvan/text_field.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; class UsernameInput extends StatefulWidget { const UsernameInput({ @@ -70,7 +71,7 @@ class _UsernameInputState extends State { .textTheme .caption! .copyWith(color: Theme.of(context).colorScheme.primary), - recognizer: TapGestureRecognizer()..onTap = () {}, + recognizer: TapGestureRecognizer()..onTap = _openTermsOfUse, ), const TextSpan(text: 'و\n'), TextSpan( @@ -79,7 +80,7 @@ class _UsernameInputState extends State { .textTheme .caption! .copyWith(color: Theme.of(context).colorScheme.primary), - recognizer: TapGestureRecognizer()..onTap = () {}, + recognizer: TapGestureRecognizer()..onTap = _openTermsOfUse, ), const TextSpan(text: 'را می‌پذیرم'), ], @@ -92,4 +93,8 @@ class _UsernameInputState extends State { ], ); } + + void _openTermsOfUse() { + launch('https://didvan.app/termsOfUse.html'); + } } diff --git a/lib/views/home/direct/direct.dart b/lib/views/home/direct/direct.dart index e3cf43e..2af96ec 100644 --- a/lib/views/home/direct/direct.dart +++ b/lib/views/home/direct/direct.dart @@ -1,6 +1,7 @@ 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/services/media/media.dart'; import 'package:didvan/views/home/direct/direct_state.dart'; import 'package:didvan/views/home/direct/widgets/message.dart'; import 'package:didvan/views/home/direct/widgets/message_box.dart'; @@ -35,59 +36,65 @@ class _DirectState extends State { Widget build(BuildContext context) { final state = context.watch(); final d = MediaQuery.of(context); - return Material( - child: Stack( - children: [ - Positioned( - top: 0, - bottom: 56, - left: 0, - right: 0, - child: DidvanScaffold( - reverse: true, - backgroundColor: Theme.of(context).colorScheme.surface, - appBarData: AppBarData( - hasBack: true, - subtitle: 'ارتباط با سردبیر', - title: widget.pageData['type'] ?? 'پشتیبانی اپلیکیشن', + return WillPopScope( + onWillPop: () async { + MediaService.resetAudioPlayer(); + return true; + }, + child: Material( + child: Stack( + children: [ + Positioned( + top: 0, + bottom: 56, + left: 0, + right: 0, + child: DidvanScaffold( + reverse: true, + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: AppBarData( + hasBack: true, + subtitle: 'ارتباط با سردبیر', + title: widget.pageData['type'] ?? 'پشتیبانی اپلیکیشن', + ), + slivers: [ + if (state.appState != AppState.busy) + SliverPadding( + padding: state.replyRadar == null + ? EdgeInsets.zero + : const EdgeInsets.only(bottom: 68), + sliver: SliverStateHandler( + itemPadding: const EdgeInsets.only(bottom: 12), + state: state, + builder: (context, state, index) => Message( + message: state.messages[index], + ), + childCount: state.messages.length, + onRetry: state.getMessages, + ), + ), + ], + children: [ + if (state.appState == AppState.busy) + SizedBox( + height: d.size.height - kToolbarHeight - d.padding.top, + child: Center( + child: SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], ), - slivers: [ - if (state.appState != AppState.busy) - SliverPadding( - padding: state.replyRadar == null - ? EdgeInsets.zero - : const EdgeInsets.only(bottom: 68), - sliver: SliverStateHandler( - itemPadding: const EdgeInsets.only(bottom: 12), - state: state, - builder: (context, state, index) => Message( - message: state.messages[index], - ), - childCount: state.messages.length, - onRetry: state.getMessages, - ), - ), - ], - children: [ - if (state.appState == AppState.busy) - SizedBox( - height: d.size.height - kToolbarHeight - d.padding.top, - child: Center( - child: SpinKitSpinningLines( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], ), - ), - Positioned( - bottom: d.viewInsets.bottom, - right: 0, - left: 0, - child: const MessageBox(), - ), - ], + Positioned( + bottom: d.viewInsets.bottom, + right: 0, + left: 0, + child: const MessageBox(), + ), + ], + ), ), ); } diff --git a/lib/views/home/direct/direct_state.dart b/lib/views/home/direct/direct_state.dart index 86cf3a9..0e66965 100644 --- a/lib/views/home/direct/direct_state.dart +++ b/lib/views/home/direct/direct_state.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:didvan/models/enums.dart'; @@ -31,7 +32,7 @@ class DirectState extends CoreProvier { final messageDatas = service.result['messages']; for (var i = 0; i < messageDatas.length; i++) { messages.add(MessageData.fromJson(messageDatas[i])); - _addToDailyGrouped(); + _addToDailyGrouped(messages.last); } appState = AppState.idle; return; @@ -75,8 +76,8 @@ class DirectState extends CoreProvier { } } - void _addToDailyGrouped() { - final createdAt = messages.last.createdAt.split('T').first; + void _addToDailyGrouped(MessageData message) { + String createdAt = message.createdAt.replaceAll('T', ' ').split(' ').first; if (!dailyMessages.containsKey(createdAt)) { dailyMessages.addAll({ createdAt: [messages.last.id] @@ -88,7 +89,6 @@ class DirectState extends CoreProvier { Future sendMessage() async { if ((text == null || text!.isEmpty) && recordedFile == null) return; - replyRadar = null; messages.insert( 0, MessageData( @@ -104,7 +104,7 @@ class DirectState extends CoreProvier { audioDuration: audioDuration, ), ); - _addToDailyGrouped(); + _addToDailyGrouped(messages.first); final body = {}; if (text != null) { body.addAll({'text': text}); @@ -115,6 +115,7 @@ class DirectState extends CoreProvier { final uploadFile = recordedFile; text = null; recordedFile = null; + replyRadar = null; notifyListeners(); final service = RequestService(RequestHelper.sendDirectMessage(typeId), body: body); diff --git a/lib/views/home/direct/widgets/message.dart b/lib/views/home/direct/widgets/message.dart index 818b2ff..49ab63d 100644 --- a/lib/views/home/direct/widgets/message.dart +++ b/lib/views/home/direct/widgets/message.dart @@ -58,6 +58,7 @@ class Message extends StatelessWidget { _MessageContainer( writedByAdmin: message.writedByAdmin, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (message.text != null) DidvanText(message.text!), if (message.audio != null || message.audioFile != null) diff --git a/lib/views/home/hashtag/hashtag.dart b/lib/views/home/hashtag/hashtag.dart index eb95e99..2e94ce0 100644 --- a/lib/views/home/hashtag/hashtag.dart +++ b/lib/views/home/hashtag/hashtag.dart @@ -28,12 +28,19 @@ class _HashtagState extends State { @override Widget build(BuildContext context) { return DidvanScaffold( - appBarData: AppBarData(title: widget.tag.label, hasBack: true), + appBarData: AppBarData(title: '#' + widget.tag.label, hasBack: true), slivers: [ Consumer( builder: (context, state, child) => SliverStateHandler( + itemPadding: const EdgeInsets.only(bottom: 8), state: state, + placeholder: RadarOverview.placeholder, builder: (context, state, index) { + index++; + if (index % 15 == 0 && index / 15 >= state.page) { + state.getTagItems(page: index ~/ 15 + 1); + } + index--; final item = state.items[index]; final type = item.type; if (type == 'radar') { @@ -51,7 +58,7 @@ class _HashtagState extends State { return Container(); }, childCount: state.items.length, - onRetry: () {}, + onRetry: () => state.getTagItems(page: 1), ), ) ], diff --git a/lib/views/home/hashtag/hashtag_state.dart b/lib/views/home/hashtag/hashtag_state.dart index 3dcb2f3..a94cdbe 100644 --- a/lib/views/home/hashtag/hashtag_state.dart +++ b/lib/views/home/hashtag/hashtag_state.dart @@ -17,6 +17,8 @@ class HashtagState extends CoreProvier { } final service = RequestService(RequestHelper.tag( ids: [id], + itemId: 1, + type: 'radar', limit: 15, page: page, )); diff --git a/lib/views/home/radar/radar.dart b/lib/views/home/radar/radar.dart index 0c97ddb..0a2adb1 100644 --- a/lib/views/home/radar/radar.dart +++ b/lib/views/home/radar/radar.dart @@ -65,7 +65,7 @@ class _RadarState extends State { CustomScrollView( physics: _isAnimating ? const NeverScrollableScrollPhysics() - : const ScrollPhysics(), + : const ClampingScrollPhysics(), controller: _scrollController, slivers: [ const SliverToBoxAdapter(child: LogoAppBar()), 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 1e6c4e8..2095cec 100644 --- a/lib/views/home/radar/radar_details/radar_details_state.dart +++ b/lib/views/home/radar/radar_details/radar_details_state.dart @@ -8,7 +8,6 @@ import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/providers/core_provider.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; -import 'package:flutter/material.dart'; class RadarDetailsState extends CoreProvier { final List radars = []; diff --git a/lib/views/home/radar/widgets/category_item.dart b/lib/views/home/radar/widgets/category_item.dart index f8176a5..faee630 100644 --- a/lib/views/home/radar/widgets/category_item.dart +++ b/lib/views/home/radar/widgets/category_item.dart @@ -4,6 +4,7 @@ import 'package:didvan/models/view/radar_category.dart'; import 'package:didvan/views/home/radar/radar_state.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; @@ -18,6 +19,22 @@ class CategoryItem extends StatelessWidget { required this.category, }) : super(key: key); + double _width(context) { + final Size ds = MediaQuery.of(context).size; + if (kIsWeb) { + if (!_useWebMobileLayout(context)) { + return ds.height / 16 * 9 / 3; + } + } + return ds.width / 3; + } + + bool _useWebMobileLayout(context) { + final shortestSide = MediaQuery.of(context).size.shortestSide; + final bool useMobileLayout = shortestSide < 600; + return kIsWeb && useMobileLayout; + } + @override Widget build(BuildContext context) { final Size ds = MediaQuery.of(context).size; @@ -34,7 +51,7 @@ class CategoryItem extends StatelessWidget { child: AnimatedContainer( duration: DesignConfig.mediumAnimationDuration, padding: isColapsed ? const EdgeInsets.all(4) : EdgeInsets.zero, - width: isColapsed ? 100 : ds.width / 3, + width: isColapsed ? 100 : _width(context), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: DesignConfig.lowBorderRadius, @@ -48,8 +65,12 @@ class CategoryItem extends StatelessWidget { duration: DesignConfig.mediumAnimationDuration, isVisible: !isColapsed, child: Container( - width: ds.width / 5, - height: ds.width / 5, + width: !_useWebMobileLayout(context) + ? _width(context) / 2 + : ds.width / 5, + height: !_useWebMobileLayout(context) + ? _width(context) / 2 + : ds.width / 5, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, boxShadow: DesignConfig.defaultShadow, diff --git a/lib/views/home/studio/podcast_details/podcast_details.dart b/lib/views/home/studio/podcast_details/podcast_details.dart new file mode 100644 index 0000000..140d8d0 --- /dev/null +++ b/lib/views/home/studio/podcast_details/podcast_details.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class PodcastDetails extends StatelessWidget { + const PodcastDetails({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + ), + ), + ); + } +} diff --git a/lib/views/home/studio/studio.dart b/lib/views/home/studio/studio.dart index a6f675a..08c8e37 100644 --- a/lib/views/home/studio/studio.dart +++ b/lib/views/home/studio/studio.dart @@ -1,10 +1,17 @@ import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/home/studio/widgets/slider.dart'; import 'package:didvan/views/home/studio/widgets/tab_bar.dart'; import 'package:didvan/views/home/widgets/logo_app_bar.dart'; import 'package:didvan/views/home/widgets/search_field.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:flutter/material.dart'; +import 'package:provider/provider.dart'; class Studio extends StatefulWidget { const Studio({Key? key}) : super(key: key); @@ -51,7 +58,73 @@ class _StudioState extends State { const SliverToBoxAdapter( child: StudioSlider(), ), + const SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: DidvanDivider( + verticalPadding: 0, + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const ItemTitle(title: 'تازه‌ترین‌ها'), + DidvanIconButton( + gestureSize: 36, + icon: DidvanIcons.sort_regular, + onPressed: _showSortDialog, + ), + ], + ), + ), + ), ], ); } + + 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, + ), + ], + ), + ), + title: 'مرتب‌‌سازی', + titleIcon: DidvanIcons.sort_regular, + hasDismissButton: false, + confrimTitle: 'مرتب سازی', + onConfirmed: () {}, + ), + ); + } } diff --git a/lib/views/home/studio/studio_state.dart b/lib/views/home/studio/studio_state.dart index ffa0282..28ebd1e 100644 --- a/lib/views/home/studio/studio_state.dart +++ b/lib/views/home/studio/studio_state.dart @@ -1,5 +1,7 @@ import 'package:didvan/providers/core_provider.dart'; class StudioState extends CoreProvier { + int selectedSortTypeIndex = 0; + bool videosSelected = true; } diff --git a/lib/views/home/studio/video_details/video_details.dart b/lib/views/home/studio/video_details/video_details.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/views/home/studio/widgets/tab_bar.dart b/lib/views/home/studio/widgets/tab_bar.dart index c5caacb..7e11744 100644 --- a/lib/views/home/studio/widgets/tab_bar.dart +++ b/lib/views/home/studio/widgets/tab_bar.dart @@ -1,8 +1,10 @@ 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_state.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class StudioTabBar extends StatelessWidget { const StudioTabBar({ @@ -11,11 +13,16 @@ class StudioTabBar extends StatelessWidget { @override Widget build(BuildContext context) { + final state = context.watch(); return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(4), decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).colorScheme.border), + border: Border.all( + color: state.videosSelected + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).primaryColor, + ), borderRadius: DesignConfig.lowBorderRadius, ), child: Row( @@ -26,7 +33,7 @@ class StudioTabBar extends StatelessWidget { selectedColor: Theme.of(context).colorScheme.secondary, title: 'ویدئو', onTap: () {}, - isSelected: true, + isSelected: state.videosSelected, ), ), Container( @@ -40,7 +47,7 @@ class StudioTabBar extends StatelessWidget { selectedColor: Theme.of(context).colorScheme.focusedBorder, title: 'پادکست', onTap: () {}, - isSelected: true, + isSelected: !state.videosSelected, ), ), ], @@ -64,7 +71,8 @@ class _StudioTypeButton extends StatelessWidget { required this.isSelected, }) : super(key: key); - Color? get _color => isSelected ? selectedColor : null; + Color? _color(context) => + isSelected ? selectedColor : Theme.of(context).colorScheme.hint; @override Widget build(BuildContext context) { @@ -77,20 +85,20 @@ class _StudioTypeButton extends StatelessWidget { Icon( icon, size: 32, - color: _color, + color: _color(context), ), if (!isSelected) const SizedBox(height: 18), if (isSelected) Container( width: 88, height: 1, - color: _color, + color: _color(context), ), if (isSelected) DidvanText( title, style: Theme.of(context).textTheme.overline, - color: _color, + color: _color(context), ) ], ), diff --git a/lib/views/home/widgets/bnb.dart b/lib/views/home/widgets/bnb.dart index 42d8ad9..4c78de0 100644 --- a/lib/views/home/widgets/bnb.dart +++ b/lib/views/home/widgets/bnb.dart @@ -1,7 +1,10 @@ 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/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; class DidvanBNB extends StatelessWidget { @@ -14,54 +17,93 @@ class DidvanBNB extends StatelessWidget { @override Widget build(BuildContext context) { - return 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), - ), - ], - ), - ); + return StreamBuilder( + stream: MediaService.audioPlayer.playingStream, + builder: (context, snapshot) { + return Stack( + children: [ + AnimatedContainer( + duration: DesignConfig.lowAnimationDuration, + height: snapshot.data == true ? 120 : 72, + decoration: BoxDecoration( + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.navigation, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + child: Row( + children: [ + const DidvanIconButton( + icon: DidvanIcons.close_regular, + gestureSize: 24, + onPressed: MediaService.resetAudioPlayer, + ), + const SizedBox(width: 16), + if (MediaService.audioPlayerCover != null) + SkeletonImage(imageUrl: MediaService.audioPlayerCover!), + const SizedBox(width: 16), + ], + ), + ), + 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), + ), + ], + ), + ), + ), + ], + ); + }); } } diff --git a/lib/views/home/widgets/podcast_overview.dart b/lib/views/home/widgets/podcast_overview.dart new file mode 100644 index 0000000..fa626fb --- /dev/null +++ b/lib/views/home/widgets/podcast_overview.dart @@ -0,0 +1,138 @@ +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; + +class PodcastOverview extends StatelessWidget { + final OverviewData news; + final NewsRequestArgs? newsRequestArgs; + final void Function(int id, bool value) onMarkChanged; + final bool hasUnmarkConfirmation; + const PodcastOverview({ + Key? key, + required this.news, + required this.onMarkChanged, + this.newsRequestArgs, + this.hasUnmarkConfirmation = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + onTap: () => Navigator.of(context).pushNamed( + Routes.newsDetails, + arguments: { + 'onMarkChanged': onMarkChanged, + 'id': news.id, + 'args': newsRequestArgs, + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + }, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SkeletonImage( + imageUrl: news.image, + width: 64, + height: 64, + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 64, + child: DidvanText( + news.title, + style: Theme.of(context).textTheme.bodyText1, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + DidvanText( + news.description, + maxLines: 3, + ), + const DidvanDivider(verticalPadding: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + DidvanText( + news.reference!, + style: Theme.of(context).textTheme.caption, + ), + DidvanText( + ' - ' + DateTimeUtils.momentGenerator(news.createdAt), + style: Theme.of(context).textTheme.caption, + ), + ], + ), + BookmarkButton( + value: news.marked, + onMarkChanged: (value) => onMarkChanged(news.id, value), + askForConfirmation: hasUnmarkConfirmation, + ), + ], + ), + ], + ), + ); + } + + static Widget get placeholder => DidvanCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder(height: 64, width: 64), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerPlaceholder(height: 18, width: 200), + SizedBox(height: 8), + ShimmerPlaceholder(height: 18, width: 100), + ], + ), + ], + ), + const SizedBox(height: 12), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: 100, + ), + const DidvanDivider(verticalPadding: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + ShimmerPlaceholder(height: 12, width: 150), + ShimmerPlaceholder(height: 24, width: 24), + ], + ), + ], + ), + ); +} diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart index e42744c..799c62e 100644 --- a/lib/views/widgets/didvan/page_view.dart +++ b/lib/views/widgets/didvan/page_view.dart @@ -8,7 +8,6 @@ import 'package:didvan/views/home/widgets/tag_item.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; 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/ink_wrapper.dart'; import 'package:didvan/views/widgets/item_title.dart'; @@ -191,14 +190,17 @@ class _DidvanPageViewState extends State { margin: EdgeInsets.zero, padding: EdgeInsets.zero, ), - 'span': Style( - textAlign: TextAlign.center, - fontSize: FontSize.small, - alignment: Alignment.center, - ), }, ); } + if (content.caption != null) { + return Center( + child: DidvanText( + content.caption, + style: Theme.of(context).textTheme.caption, + ), + ); + } if (content.image != null) { return GestureDetector( onTap: () { @@ -211,19 +213,15 @@ class _DidvanPageViewState extends State { child: Center( child: SkeletonImage( width: MediaQuery.of(context).size.width, - height: 200, - imageUrl: content.image, + imageUrl: content.largeImage ?? content.image, ), ), ), ), - Positioned( - right: 16, - child: DidvanIconButton( - backgroundColor: Theme.of(context).colorScheme.primary, - icon: DidvanIcons.back_regular, - onPressed: Navigator.of(context).pop, - ), + const Positioned( + right: 24, + top: 24, + child: _BackButton(), ), ], ), @@ -231,7 +229,7 @@ class _DidvanPageViewState extends State { }, child: SkeletonImage( imageUrl: content.image!, - aspectRatio: 16 / 9, + width: double.infinity, ), ); } @@ -277,9 +275,8 @@ class _DidvanPageViewState extends State { } class _BackButton extends StatefulWidget { - final ScrollController scrollController; - const _BackButton({Key? key, required this.scrollController}) - : super(key: key); + final ScrollController? scrollController; + const _BackButton({Key? key, this.scrollController}) : super(key: key); @override __BackButtonState createState() => __BackButtonState(); @@ -290,20 +287,26 @@ class __BackButtonState extends State<_BackButton> { @override void didUpdateWidget(covariant _BackButton oldWidget) { - _isVisible = false; - _handleScroll(); + if (widget.scrollController != null) { + _isVisible = false; + _handleScroll(); + } super.didUpdateWidget(oldWidget); } @override void initState() { - _handleScroll(); + if (widget.scrollController != null) { + _handleScroll(); + } else { + _isVisible = true; + } super.initState(); } void _handleScroll() { - widget.scrollController.addListener(() { - final position = widget.scrollController.position.pixels; + widget.scrollController!.addListener(() { + final position = widget.scrollController!.position.pixels; final size = MediaQuery.of(context).size.height * 0.3; if (position > size && _isVisible == false) { setState(() { diff --git a/lib/views/widgets/didvan/radial_button.dart b/lib/views/widgets/didvan/radial_button.dart new file mode 100644 index 0000000..778b39d --- /dev/null +++ b/lib/views/widgets/didvan/radial_button.dart @@ -0,0 +1,54 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class DidvanRadialButton extends StatelessWidget { + final String title; + final VoidCallback onSelected; + final bool value; + const DidvanRadialButton({ + Key? key, + required this.title, + required this.onSelected, + required this.value, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: value ? null : onSelected, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(4), + height: 24, + width: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: value + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.text, + ), + ), + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: value + ? Theme.of(context).colorScheme.secondary + : Colors.transparent, + shape: BoxShape.circle, + ), + ), + ), + const SizedBox(width: 8), + DidvanText(title), + ], + ), + ), + ); + } +} diff --git a/lib/views/widgets/skeleton_image.dart b/lib/views/widgets/skeleton_image.dart index bf7db1c..c59f431 100644 --- a/lib/views/widgets/skeleton_image.dart +++ b/lib/views/widgets/skeleton_image.dart @@ -8,17 +8,17 @@ import 'package:cached_network_image_platform_interface/cached_network_image_pla class SkeletonImage extends StatelessWidget { final String imageUrl; - final double width; - final double height; + final double? width; + final double? height; final BorderRadius? borderRadius; final double? aspectRatio; const SkeletonImage({ Key? key, required this.imageUrl, - this.width = 300, - this.height = 140, this.borderRadius = DesignConfig.lowBorderRadius, this.aspectRatio, + this.width, + this.height, }) : super(key: key); @override @@ -30,13 +30,11 @@ class SkeletonImage extends StatelessWidget { width: width, height: height, imageUrl: RequestHelper.baseUrl + imageUrl, - imageBuilder: (context, imageProvider) => Container( - decoration: BoxDecoration( - borderRadius: borderRadius ?? DesignConfig.lowBorderRadius, - image: DecorationImage( - image: imageProvider, - fit: BoxFit.cover, - ), + imageBuilder: (context, imageProvider) => ClipRRect( + borderRadius: borderRadius ?? DesignConfig.lowBorderRadius, + child: Image( + image: imageProvider, + fit: BoxFit.cover, ), ), progressIndicatorBuilder: (context, url, progress) => diff --git a/pubspec.lock b/pubspec.lock index 6942b4d..1ae7618 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -148,6 +148,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.13.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.5" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + url: "https://pub.dartlang.org" + source: hosted + version: "11.2.8" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.9" flutter: dependency: "direct main" description: flutter @@ -762,5 +804,5 @@ packages: source: hosted version: "5.3.1" sdks: - dart: ">=2.15.0 <3.0.0" + dart: ">=2.16.0 <3.0.0" flutter: ">=2.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index 119a748..f732c01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,8 @@ dependencies: url_launcher: ^6.0.18 audio_video_progress_bar: ^0.10.0 image_cropper: ^1.5.0 + firebase_messaging: ^11.2.8 + firebase_core: ^1.13.1 dev_dependencies: