diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b12b914 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "didvan-app", + "request": "launch", + "type": "dart" + }, + { + "name": "didvan-app (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "didvan-app (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "didvan-app (web)", + "request": "launch", + "type": "dart", + "args": [ + "--web-port=8080" + ] + } + ] +} diff --git a/lib/assets/icons/Arrow - Right 2.svg b/lib/assets/icons/Arrow - Right 2.svg new file mode 100644 index 0000000..f263a21 --- /dev/null +++ b/lib/assets/icons/Arrow - Right 2.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/RiskRadar.svg b/lib/assets/icons/RiskRadar.svg new file mode 100644 index 0000000..96422b3 --- /dev/null +++ b/lib/assets/icons/RiskRadar.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/lib/assets/icons/login_img.png b/lib/assets/icons/login_img.png new file mode 100644 index 0000000..6e9f61b Binary files /dev/null and b/lib/assets/icons/login_img.png differ diff --git a/lib/assets/images/empty_states/Bookmark2.png b/lib/assets/images/empty_states/Bookmark2.png new file mode 100644 index 0000000..ca0146a Binary files /dev/null and b/lib/assets/images/empty_states/Bookmark2.png differ diff --git a/lib/assets/images/empty_states/Change_Password.png b/lib/assets/images/empty_states/Change_Password.png new file mode 100644 index 0000000..7619cbb Binary files /dev/null and b/lib/assets/images/empty_states/Change_Password.png differ diff --git a/lib/assets/images/empty_states/Connection.png b/lib/assets/images/empty_states/Connection.png new file mode 100644 index 0000000..0dbcee3 Binary files /dev/null and b/lib/assets/images/empty_states/Connection.png differ diff --git a/lib/assets/images/empty_states/Empty-Result.png b/lib/assets/images/empty_states/Empty-Result.png new file mode 100644 index 0000000..91b8eda Binary files /dev/null and b/lib/assets/images/empty_states/Empty-Result.png differ diff --git a/lib/assets/images/empty_states/Empty_List.png b/lib/assets/images/empty_states/Empty_List.png new file mode 100644 index 0000000..c96220d Binary files /dev/null and b/lib/assets/images/empty_states/Empty_List.png differ diff --git a/lib/assets/images/empty_states/Image1_fx.jpg b/lib/assets/images/empty_states/Image1_fx.jpg deleted file mode 100644 index 6a2d427..0000000 Binary files a/lib/assets/images/empty_states/Image1_fx.jpg and /dev/null differ diff --git a/lib/assets/images/empty_states/Image1_fx.png b/lib/assets/images/empty_states/Image1_fx.png deleted file mode 100644 index 5f1d379..0000000 Binary files a/lib/assets/images/empty_states/Image1_fx.png and /dev/null differ diff --git a/lib/assets/images/empty_states/Support.png b/lib/assets/images/empty_states/Support.png new file mode 100644 index 0000000..0e858f6 Binary files /dev/null and b/lib/assets/images/empty_states/Support.png differ diff --git a/lib/assets/images/empty_states/bookmark.png b/lib/assets/images/empty_states/bookmark.png new file mode 100644 index 0000000..8d41f92 Binary files /dev/null and b/lib/assets/images/empty_states/bookmark.png differ diff --git a/lib/assets/images/empty_states/change password.png b/lib/assets/images/empty_states/change password.png deleted file mode 100644 index 01d17f5..0000000 Binary files a/lib/assets/images/empty_states/change password.png and /dev/null differ diff --git a/lib/assets/images/onboarding/1.png b/lib/assets/images/onboarding/1.png new file mode 100644 index 0000000..8a42968 Binary files /dev/null and b/lib/assets/images/onboarding/1.png differ diff --git a/lib/assets/images/onboarding/2.png b/lib/assets/images/onboarding/2.png new file mode 100644 index 0000000..b834179 Binary files /dev/null and b/lib/assets/images/onboarding/2.png differ diff --git a/lib/assets/images/onboarding/3.png b/lib/assets/images/onboarding/3.png new file mode 100644 index 0000000..fea3aa7 Binary files /dev/null and b/lib/assets/images/onboarding/3.png differ diff --git a/lib/assets/images/onboarding/4.png b/lib/assets/images/onboarding/4.png new file mode 100644 index 0000000..2afefb9 Binary files /dev/null and b/lib/assets/images/onboarding/4.png differ diff --git a/lib/constants/assets.dart b/lib/constants/assets.dart index aaf4899..8b5ee28 100644 --- a/lib/constants/assets.dart +++ b/lib/constants/assets.dart @@ -75,7 +75,7 @@ class Assets { static String get emptyConnection => '$_baseEmptyStatesPath/connection-$_themeSuffix.svg'; static String get emptyResult => - '$_baseEmptyStatesPath/result-$_themeSuffix.svg'; + 'lib/assets/images/empty_states/Empty_List.png'; static String get fooladWorld => '$_baseFeaturesPath/foolad-world-$_themeSuffix.svg'; @@ -84,7 +84,8 @@ class Assets { static String get risk => '$_baseFeaturesPath/risk-$_themeSuffix.svg'; static String get saha => '$_baseFeaturesPath/saha-$_themeSuffix.svg'; static String get ai => '$_baseFeaturesPath/ai-$_themeSuffix.svg'; - static String get hugeideas => '$_baseFeaturesPath/hugeicons_idea-$_themeSuffix.svg'; + static String get hugeideas => + '$_baseFeaturesPath/hugeicons_idea-$_themeSuffix.svg'; static String get startup => '$_baseFeaturesPath/startup-$_themeSuffix.svg'; static String get stats => '$_baseFeaturesPath/stats-$_themeSuffix.svg'; static String get tech => '$_baseFeaturesPath/tech-$_themeSuffix.svg'; diff --git a/lib/models/didvan_voice_model.dart b/lib/models/didvan_voice_model.dart new file mode 100644 index 0000000..2d521da --- /dev/null +++ b/lib/models/didvan_voice_model.dart @@ -0,0 +1,25 @@ +class DidvanVoiceModel { + final int id; + final String title; + final String file; + final String image; + final String publishedAt; + + DidvanVoiceModel({ + required this.id, + required this.title, + required this.file, + required this.image, + required this.publishedAt, + }); + + factory DidvanVoiceModel.fromJson(Map json) { + return DidvanVoiceModel( + id: json['id'], + title: json['title'], + file: json['file'], + image: json['image'], + publishedAt: json['publishedAt'], + ); + } +} diff --git a/lib/models/onboarding_entity.dart b/lib/models/onboarding_entity.dart new file mode 100644 index 0000000..a702b20 --- /dev/null +++ b/lib/models/onboarding_entity.dart @@ -0,0 +1,11 @@ +class OnboardingEntity { + final String imagePath; + final String title; + final String description; + + OnboardingEntity({ + required this.imagePath, + required this.title, + required this.description, + }); +} diff --git a/lib/models/top_banner_model.dart b/lib/models/top_banner_model.dart new file mode 100644 index 0000000..eb6b1a4 --- /dev/null +++ b/lib/models/top_banner_model.dart @@ -0,0 +1,16 @@ +class TopBannerModel { + final int id; + final String image; + + TopBannerModel({ + required this.id, + required this.image, + }); + + factory TopBannerModel.fromJson(Map json) { + return TopBannerModel( + id: json['id'], + image: json['image'], + ); + } +} diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index a111787..4b27ab3 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -71,6 +71,7 @@ import 'package:didvan/views/podcasts/studio_details/studio_details.mobile.dart' import 'package:didvan/views/pdf_viewer/pdf_viewer_page.dart'; import 'package:didvan/views/monthly/monthly_list_page.dart'; import 'package:didvan/views/monthly/monthly_list_state.dart'; +import 'package:didvan/views/onboarding/onboarding_page.dart'; import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/story_viewer/story_viewer_page.dart'; @@ -124,6 +125,10 @@ class RouteGenerator { return _createRoute( const Splash(), ); + case Routes.onboarding: + return _createRoute( + const OnboardingPage(), + ); case Routes.aiSection: return _createRoute( const AiSectionPage(), diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 2e48e32..724f080 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -1,5 +1,6 @@ class Routes { static const String splash = '/'; + static const String onboarding = '/onboarding'; static const String aiChat = '/ai-chat'; static const String aiHistory = '/ai-history'; static const String botAssistants = '/bot-assistants-page'; diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index d4cd53f..2ffb706 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -24,6 +24,8 @@ class RequestHelper { static const String mainPageContent = _baseHomeUrl; static const String didvanPlus = '$baseUrl/didvanPlus'; + static const String didvanVoice = '$baseUrl/didvanVoice'; + static const String topBanner = '$baseUrl/topBanner'; static String searchAll({ required int page, diff --git a/lib/views/ai/ai.dart b/lib/views/ai/ai.dart index 7164423..b305bc3 100644 --- a/lib/views/ai/ai.dart +++ b/lib/views/ai/ai.dart @@ -60,7 +60,7 @@ class _AiState extends State { child: SingleChildScrollView( child: Padding( padding: - const EdgeInsets.only(bottom: 24, top: 70), + const EdgeInsets.only(bottom: 24, top: 0), child: Column( children: [ Consumer( @@ -68,8 +68,10 @@ class _AiState extends State { AiState aiState, Widget? child) { final tools = aiState.tools; if (tools != null && tools.isEmpty) { - return EmptyState( - asset: Assets.emptyResult, + return const EmptyState( + asset: + 'lib/assets/images/empty_states/Empty-Result.png', + height: 500, title: 'لیست خالی است', ); } diff --git a/lib/views/ai/history_ai_chat_page.dart b/lib/views/ai/history_ai_chat_page.dart index 8a08d47..2df5f5d 100644 --- a/lib/views/ai/history_ai_chat_page.dart +++ b/lib/views/ai/history_ai_chat_page.dart @@ -100,7 +100,9 @@ class _HistoryAiChatPageState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: DesignConfig.isDark? const Color.fromARGB(255, 0, 90, 119) : const Color.fromARGB(255, 0, 53, 70))), + color: DesignConfig.isDark + ? const Color.fromARGB(255, 0, 90, 119) + : const Color.fromARGB(255, 0, 53, 70))), ), // SearchField( // title: 'گفت‌و‌گو‌ها', @@ -153,11 +155,16 @@ class _HistoryAiChatPageState extends State { Consumer( builder: (context, state, child) { return SliverStateHandler( + paddingEmptyState: 0, state: state, centerEmptyState: false, emptyState: EmptyState( asset: Assets.emptyResult, + height: 550, title: 'لیست خالی است', + titleColor: const Color.fromARGB(255, 2, 126, 167), + subtitle: + 'در حال حاضر آیتمی در این بخش ثبت نشده است. هر زمان مورد جدیدی اضافه شود، در اینجا نمایش داده می‌شود.', ), enableEmptyState: archived ? state.archivedChats.isEmpty diff --git a/lib/views/authentication/screens/password.dart b/lib/views/authentication/screens/password.dart index fca1350..03e5f2b 100644 --- a/lib/views/authentication/screens/password.dart +++ b/lib/views/authentication/screens/password.dart @@ -6,8 +6,10 @@ import 'package:didvan/views/authentication/widgets/authentication_layout.dart'; import 'package:didvan/views/widgets/didvan/button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; 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_string.dart'; class PasswordInput extends StatefulWidget { const PasswordInput({Key? key}) : super(key: key); @@ -23,8 +25,8 @@ class _PasswordInputState extends State { Widget build(BuildContext context) { final AuthenticationState state = context.read(); return AuthenticationLayout( - appBarTitle: - 'ورود با ${state.username.contains('09') ? 'شماره موبایل' : 'نام کاربری'} ${state.username}', + // appBarTitle: + // 'ورود با ${state.username.contains('09') ? 'شماره موبایل' : 'نام کاربری'} ${state.username}', children: [ Form( key: _formKey, @@ -42,7 +44,7 @@ class _PasswordInputState extends State { ), ), const SizedBox( - height: 32, + height: 15, ), GestureDetector( onTap: () => state.currentPageIndex++, @@ -52,13 +54,54 @@ class _PasswordInputState extends State { color: Theme.of(context).colorScheme.primary, ), ), - const Spacer(), + const SizedBox( + height: 50, + ), DidvanButton( onPressed: () => _onLogin(context), - title: 'ورود', + title: 'تایید و ادامه', ), const SizedBox( - height: 48, + height: 15, + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 60), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.bodySmall, + children: [ + const TextSpan(text: 'با ورود به اپلیکیشن دیدوان،'), + TextSpan( + text: ' شرایط ', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).colorScheme.primary), + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrlString( + 'https://didvan.com/terms-of-use#conditions', + mode: LaunchMode.inAppWebView), + ), + const TextSpan(text: 'و\n'), + TextSpan( + text: ' قوانین حریم خصوصی ', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).colorScheme.primary), + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrlString( + 'https://didvan.com/terms-of-use#privacy', + mode: LaunchMode.inAppWebView), + ), + const TextSpan(text: 'را می‌پذیرم'), + ], + ), + ), ), ], ); diff --git a/lib/views/authentication/screens/username.dart b/lib/views/authentication/screens/username.dart index 70b7023..ed65b6d 100644 --- a/lib/views/authentication/screens/username.dart +++ b/lib/views/authentication/screens/username.dart @@ -1,14 +1,13 @@ +import 'package:didvan/config/theme_data.dart'; import 'package:didvan/utils/extension.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'; -import 'package:didvan/views/widgets/didvan/text.dart'; 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_string.dart'; -// import 'package:url_launcher/url_launcher.dart'; class UsernameInput extends StatefulWidget { const UsernameInput({ @@ -50,12 +49,16 @@ class _UsernameInputState extends State { }, ), ), - Padding( - padding: const EdgeInsets.all(8.0), - child: DidvanText( - 'نام کاربری می‌تواند شامل کاراکترهای انگلیسی و اعداد باشد.', - style: Theme.of(context).textTheme.labelSmall, - ), + // Padding( + // padding: const EdgeInsets.all(8.0), + // child: DidvanText( + // 'نام کاربری می‌تواند شامل کاراکترهای انگلیسی و اعداد باشد.', + // style: Theme.of(context).textTheme.labelSmall, + // ), + // ), + + const SizedBox( + height: 70, ), DidvanButton( title: 'ادامه', @@ -65,13 +68,18 @@ class _UsernameInputState extends State { } }, ), - const Spacer(), + const SizedBox( + height: 20, + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 60), child: RichText( textAlign: TextAlign.center, text: TextSpan( - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: Theme.of(context).colorScheme.caption), children: [ const TextSpan(text: 'با ورود به اپلیکیشن دیدوان،'), TextSpan( diff --git a/lib/views/authentication/screens/verification.dart b/lib/views/authentication/screens/verification.dart index aaf69e0..a4496b4 100644 --- a/lib/views/authentication/screens/verification.dart +++ b/lib/views/authentication/screens/verification.dart @@ -2,12 +2,9 @@ import 'dart:async'; -// START: Added Imports import 'package:flutter/gestures.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher_string.dart'; -// END: Added Imports - import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/providers/user.dart'; @@ -36,10 +33,7 @@ class _VerificationState extends State { bool _isResendButtonEnabled = false; bool _isConfirmButtonEnabled = false; String _completedCode = ""; - - // START: Added state for error bool _hasError = false; - // END: Added state @override void initState() { @@ -75,30 +69,26 @@ class _VerificationState extends State { Widget build(BuildContext context) { final AuthenticationState state = context.read(); - // START: Define colors for PinTheme final errorColor = Theme.of(context).colorScheme.error; final primaryColor = Theme.of(context).colorScheme.primary; final borderColor = Theme.of(context).colorScheme.border; final textColor = Theme.of(context).colorScheme.text; - // END: Define colors return WillPopScope( onWillPop: () async { _timer?.cancel(); - // منطق بازگشت هماهنگ با authentication.dart if (state.currentPageIndex == 0) { return true; } if (state.currentPageIndex == 2 && !state.hasPassword) { - state.currentPageIndex = 0; // برگرد به صفحه نام کاربری + state.currentPageIndex = 0; return false; } - state.currentPageIndex--; // در غیر این صورت یکی برگرد + state.currentPageIndex--; return false; }, child: AuthenticationLayout( - appBarTitle: 'تغییر رمز عبور', children: [ Row( mainAxisAlignment: MainAxisAlignment.center, @@ -125,8 +115,8 @@ class _VerificationState extends State { const SizedBox(height: 8), InkWell( onTap: () { - _timer?.cancel(); // لغو تایمر - state.currentPageIndex = 0; // بازگشت به صفحه ورود شماره + _timer?.cancel(); + state.currentPageIndex = 0; }, borderRadius: DesignConfig.lowBorderRadius, child: Padding( @@ -157,58 +147,46 @@ class _VerificationState extends State { keyboardType: TextInputType.number, animationType: AnimationType.scale, cursorColor: textColor, - // START: Added textStyle for error color textStyle: TextStyle( - fontSize: 20, // اندازه فونت را متناسب تنظیم کنید + fontSize: 20, color: _hasError ? const Color.fromARGB(255, 178, 4, 54) : textColor, ), - // END: Added textStyle pinTheme: PinTheme( fieldHeight: 58, fieldWidth: 58, - // START: Dynamic border/bg colors selectedColor: _hasError ? const Color.fromARGB(255, 178, 4, 54) : primaryColor, inactiveColor: _hasError ? const Color.fromARGB(255, 178, 4, 54) : borderColor, activeFillColor: _hasError ? const Color.fromARGB(255, 178, 4, 54) : primaryColor, // بستگی به طراحی شما دارد activeColor: _hasError ? const Color.fromARGB(255, 178, 4, 54) : primaryColor, - // END: Dynamic colors borderRadius: DesignConfig.lowBorderRadius, borderWidth: 1, shape: PinCodeFieldShape.box, ), appContext: context, length: 6, - // START: Modified onCompleted onCompleted: (value) async { final result = await state.verifyOtpToken(value); if (result) { _timer?.cancel(); } }, - // END: Modified onCompleted - // START: Modified onChanged onChanged: (value) { setState(() { _completedCode = value; _isConfirmButtonEnabled = value.length == 6; if (_hasError) { - _hasError = false; // پاک کردن خطا با شروع تایپ + _hasError = false; } }); }, - // END: Modified onChanged ), ), - // ویجت ارسال مجدد کد - const SizedBox( - height: 24, - ), _isResendButtonEnabled ? InkWell( onTap: () { - _handleTimer(); // ری‌استارت تایمر - state.sendOtpToken(); // ارسال مجدد کد + _handleTimer(); + state.sendOtpToken(); }, borderRadius: DesignConfig.lowBorderRadius, child: Padding( @@ -243,26 +221,22 @@ class _VerificationState extends State { ], ), - const Spacer(), // دکمه را به پایین صفحه می‌چسباند + const SizedBox(height: 10,), DidvanButton( enabled: _isConfirmButtonEnabled, - // START: Modified onPressed onPressed: () async { if (_completedCode.length == 6) { final result = await state.verifyOtpToken(_completedCode); if (!result) { if (mounted) setState(() => _hasError = true); } - // در صورت موفقیت، onCompleted خودش کار را انجام می‌دهد } }, - // END: Modified onPressed title: 'تایید کد', ), - // START: Added Terms and Conditions RichText const SizedBox( - height: 24, // فاصله بین دکمه و متن قوانین + height: 24, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 60), @@ -301,9 +275,8 @@ class _VerificationState extends State { ), ), const SizedBox( - height: 24, // پدینگ نهایی پایین صفحه + height: 24, ), - // END: Added RichText ], ), ); diff --git a/lib/views/authentication/widgets/authentication_layout.dart b/lib/views/authentication/widgets/authentication_layout.dart index 579ea4c..3331477 100644 --- a/lib/views/authentication/widgets/authentication_layout.dart +++ b/lib/views/authentication/widgets/authentication_layout.dart @@ -1,8 +1,8 @@ // ignore_for_file: deprecated_member_use import 'package:didvan/views/authentication/widgets/authentication_app_bar.dart'; -import 'package:didvan/views/widgets/logos/didvan_horizontal_logo.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class AuthenticationLayout extends StatelessWidget { final List children; @@ -26,7 +26,7 @@ class AuthenticationLayout extends StatelessWidget { padding: const EdgeInsets.only( left: 16, right: 16, - top: 7, + // top: 7, ), child: AuthenticationAppBar( title: appBarTitle, @@ -34,17 +34,28 @@ class AuthenticationLayout extends StatelessWidget { ), ), SliverPadding( - padding: EdgeInsets.only( - top: appBarTitle == null ? 100 : 44, - left: 100, - right: 100, - bottom: 40, - ), - sliver: const SliverToBoxAdapter( - child: DidvanVerticalLogo( - height: 145, - ), - ), + padding: const EdgeInsets.only( + // top: appBarTitle == null ? 100 : 44, + // left: 100, + // right: 100, + // bottom: 40, + ), + sliver: SliverToBoxAdapter( + child: Column( + children: [ + const SizedBox(height: 20), + SvgPicture.asset( + 'lib/assets/images/logos/logo-horizontal-light.svg', + height: 90, + ), + Image.asset( + 'lib/assets/icons/login_img.png', + height: 450, + width: double.infinity, + fit: BoxFit.fill, + ) + ], + )), ), SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 20), diff --git a/lib/views/direct/direct.dart b/lib/views/direct/direct.dart index d815824..653da6a 100644 --- a/lib/views/direct/direct.dart +++ b/lib/views/direct/direct.dart @@ -3,7 +3,6 @@ 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/enums.dart'; import 'package:didvan/providers/server_data.dart'; import 'package:didvan/services/media/voice.dart'; @@ -82,14 +81,17 @@ class _DirectState extends State { ? EdgeInsets.zero : const EdgeInsets.only(bottom: 68), sliver: SliverStateHandler( - itemPadding: const EdgeInsets.only(bottom: 12), state: state, enableEmptyState: state.messages.isEmpty, - emptyState: Padding( - padding: const EdgeInsets.only(bottom: 160), + paddingEmptyState: 0, + emptyState: const Padding( + padding: EdgeInsets.only(top: 1), child: EmptyState( - asset: Assets.emptyChat, - title: 'اولین پیام را بنویسید...', + asset: 'lib/assets/images/empty_states/Support.png', + height: 600, + title: 'پشتیبانی دیدوان', + titleColor: Color.fromARGB(255, 0, 126, 167), + subtitle: 'چطور می‌توانیم به شما کمک کنیم؟', ), ), builder: (context, state, index) => Message( @@ -139,7 +141,9 @@ class _DirectState extends State { .textTheme .headlineSmall ?.copyWith( - color: DesignConfig.isDark? const Color.fromARGB(255, 0, 90, 119) : const Color.fromARGB(255, 0, 53, 70), + color: DesignConfig.isDark + ? const Color.fromARGB(255, 0, 90, 119) + : const Color.fromARGB(255, 0, 53, 70), fontWeight: FontWeight.bold, fontSize: 19), ), diff --git a/lib/views/home/bookmarks/filtered_bookmark/filtered_bookmark.dart b/lib/views/home/bookmarks/filtered_bookmark/filtered_bookmark.dart index 12b806d..12e64a6 100644 --- a/lib/views/home/bookmarks/filtered_bookmark/filtered_bookmark.dart +++ b/lib/views/home/bookmarks/filtered_bookmark/filtered_bookmark.dart @@ -1,10 +1,10 @@ -import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/config/theme_data.dart'; import 'package:didvan/views/home/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart'; import 'package:didvan/views/widgets/overview/multitype.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'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:didvan/views/home/main/widgets/swot_bookmark.dart'; @@ -71,62 +71,106 @@ class _FilteredBookmarksState extends State { Widget build(BuildContext context) { final state = context.watch(); - return DidvanScaffold( - appBarData: AppBarData(title: _appBarTitle, hasBack: true), - padding: const EdgeInsets.all(16), - slivers: [ - if (widget.type == 8) - SliverStateHandler( - state: state, - enableEmptyState: - state.bookmarkedSwotItems.isEmpty && !state.swotItemsLoading, - emptyState: const EmptyList(), - placeholder: MultitypeOverview.placeholder, - builder: (context, state, index) { - final item = state.bookmarkedSwotItems[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: SwotBookmark( - item: item, - onSwotUnbookmarked: (postId) { - _onBookmarkChanged(postId, false, true, "swot"); - }, - ), - ); - }, - itemPadding: EdgeInsets.zero, - childCount: state.bookmarkedSwotItems.length, - onRetry: () => state.getBookmarks(page: 1), - ) - else - SliverStateHandler( - state: state, - enableEmptyState: state.bookmarks.isEmpty, - itemPadding: const EdgeInsets.only(bottom: 8), - placeholder: MultitypeOverview.placeholder, - emptyState: const EmptyList(), - builder: (context, state, index) { - if (index >= state.bookmarks.length) { - if (state.page < state.lastPage) { - state.getBookmarks(page: state.page + 1); - return MultitypeOverview.placeholder; - } - return const SizedBox.shrink(); - } - final item = state.bookmarks[index]; - return MultitypeOverview( - item: item, - enableCaption: true, - onMarkChanged: (id, value) => - _onBookmarkChanged(id, value, true, item.type), - enableBookmark: true, - ); - }, - childCount: - state.bookmarks.length + (state.page < state.lastPage ? 1 : 0), - onRetry: () => state.getBookmarks(page: state.page), + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(90.0), + child: AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: SafeArea( + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Center( + child: SvgPicture.asset( + 'lib/assets/images/logos/logo-horizontal-light.svg', + height: 55, + ), + ), + IconButton( + icon: SvgPicture.asset( + 'lib/assets/icons/arrow-left.svg', + color: Theme.of(context).colorScheme.caption, + height: 24, + ), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ), ), - ], + ), + ), + body: CustomScrollView( + slivers: [ + if (widget.type == 8) + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 0), + sliver: SliverStateHandler( + state: state, + enableEmptyState: state.bookmarkedSwotItems.isEmpty && + !state.swotItemsLoading, + emptyState: const EmptyList(), + placeholder: MultitypeOverview.placeholder, + paddingEmptyState: 0, + builder: (context, state, index) { + final item = state.bookmarkedSwotItems[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: SwotBookmark( + item: item, + onSwotUnbookmarked: (postId) { + _onBookmarkChanged(postId, false, true, "swot"); + }, + ), + ); + }, + itemPadding: EdgeInsets.zero, + childCount: state.bookmarkedSwotItems.length, + onRetry: () => state.getBookmarks(page: 1), + ), + ) + else + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 0), + sliver: SliverStateHandler( + state: state, + enableEmptyState: state.bookmarks.isEmpty, + itemPadding: const EdgeInsets.only(bottom: 8), + placeholder: MultitypeOverview.placeholder, + emptyState: const EmptyList(), + paddingEmptyState: 0, + builder: (context, state, index) { + if (index >= state.bookmarks.length) { + if (state.page < state.lastPage) { + state.getBookmarks(page: state.page + 1); + return MultitypeOverview.placeholder; + } + return const SizedBox.shrink(); + } + final item = state.bookmarks[index]; + return MultitypeOverview( + item: item, + enableCaption: true, + onMarkChanged: (id, value) => + _onBookmarkChanged(id, value, true, item.type), + enableBookmark: true, + ); + }, + childCount: state.bookmarks.length + + (state.page < state.lastPage ? 1 : 0), + onRetry: () => state.getBookmarks(page: state.page), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 16)), + ], + ), ); } } diff --git a/lib/views/home/explore/explore.dart b/lib/views/home/explore/explore.dart index 7c36593..f3449e0 100644 --- a/lib/views/home/explore/explore.dart +++ b/lib/views/home/explore/explore.dart @@ -376,7 +376,7 @@ class MainPageSection extends StatelessWidget { case 'technology': return 'lib/assets/icons/Technology.svg'; case 'risk': - return 'lib/assets/icons/risk-radar.svg'; //TODO:FIX ICON + return 'lib/assets/icons/RiskRadar.svg'; //TODO:FIX ICON case 'startup': return 'lib/assets/icons/Startup.svg'; case 'survey': diff --git a/lib/views/home/main/main_page.dart b/lib/views/home/main/main_page.dart index 626a05e..f049834 100644 --- a/lib/views/home/main/main_page.dart +++ b/lib/views/home/main/main_page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unnecessary_import + import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/models/home_page_content/content.dart'; @@ -11,6 +13,7 @@ import 'package:didvan/views/home/main/widgets/main_content.dart'; import 'package:didvan/views/home/main/widgets/story_section.dart'; import 'package:didvan/views/home/main/widgets/simple_explore_card.dart'; import 'package:didvan/views/home/main/widgets/didvan_plus_section.dart'; +import 'package:didvan/views/home/main/widgets/didvan_voice_section.dart'; import 'package:didvan/views/home/new_statistic/new_statistics_state.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; @@ -66,6 +69,10 @@ class _MainPageState extends State { onRetry: () => context.read().init(), state: context.watch(), builder: (context, state) { + debugPrint( + '🏠 MainPage build - didvanPlus: ${state.didvanPlus != null}'); + debugPrint( + '🏠 MainPage build - didvanVoice: ${state.didvanVoice != null}'); return Column( children: [ const HomeAppBar( @@ -116,6 +123,16 @@ class _MainPageState extends State { SwotSection(swotItems: state.swotItems) .animate() .fadeIn(delay: 1100.ms, duration: 500.ms), + if (state.didvanVoice != null) ...[ + const SizedBox(height: 16), + const _DidvanVoiceTitle() + .animate() + .fadeIn(delay: 1150.ms, duration: 500.ms), + const SizedBox(height: 12), + DidvanVoiceSection(didvanVoice: state.didvanVoice!) + .animate() + .fadeIn(delay: 1200.ms, duration: 500.ms), + ], // const _IndustryPulseTitle() // .animate() // .fadeIn(delay: 1100.ms, duration: 500.ms), @@ -165,6 +182,41 @@ class _DidvanSignalsTitle extends StatelessWidget { } } +class _DidvanVoiceTitle extends StatelessWidget { + const _DidvanVoiceTitle(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + SvgPicture.asset( + 'lib/assets/icons/voice-square.svg', + color: Theme.of(context).colorScheme.title, + width: 30, + height: 30, + ), + const SizedBox(width: 5), + DidvanText( + "صدای دیدوان ", + style: Theme.of(context).textTheme.titleMedium, + color: const Color.fromARGB(255, 0, 89, 119), + fontSize: 13, + fontWeight: FontWeight.bold, + ), + DidvanText( + "(خلاصه تازه‌ترین پویش افق)", + style: Theme.of(context).textTheme.bodySmall, + color: const Color.fromARGB(255, 0, 89, 119), + fontSize: 11, + ), + ], + ), + ); + } +} + class _ExploreLatestTitle extends StatelessWidget { const _ExploreLatestTitle(); @@ -192,7 +244,9 @@ class _ExploreLatestTitle extends StatelessWidget { DidvanText( "تازه‌ترین‌های کاوش", style: Theme.of(context).textTheme.titleMedium, - color: DesignConfig.isDark ? const Color.fromARGB(255, 0, 125, 166) : const Color.fromARGB(255, 0, 89, 119), + color: DesignConfig.isDark + ? const Color.fromARGB(255, 0, 125, 166) + : const Color.fromARGB(255, 0, 89, 119), fontSize: 13, ), ], @@ -227,8 +281,10 @@ class _ExploreLatestSlider extends StatelessWidget { @override Widget build(BuildContext context) { final List items = []; - final List<({String type, MainPageContentType? content, SwotItem? swotItem})> itemsData = []; - + final List< + ({String type, MainPageContentType? content, SwotItem? swotItem})> + itemsData = []; + for (var list in lists) { if (list.type == 'video' || list.type == 'podcast' || @@ -247,7 +303,8 @@ class _ExploreLatestSlider extends StatelessWidget { ), ), ); - itemsData.add((type: list.type, content: newestContent, swotItem: null)); + itemsData + .add((type: list.type, content: newestContent, swotItem: null)); } } @@ -285,11 +342,11 @@ class _ExploreLatestSlider extends StatelessWidget { ); } else if (data.content != null) { context.read().navigationHandler( - data.type, - data.content!.id, - data.content!.link, - description: data.content!.title, - ); + data.type, + data.content!.id, + data.content!.link, + description: data.content!.title, + ); } }, ); diff --git a/lib/views/home/main/main_page_state.dart b/lib/views/home/main/main_page_state.dart index 82b2ca9..9aed956 100644 --- a/lib/views/home/main/main_page_state.dart +++ b/lib/views/home/main/main_page_state.dart @@ -1,5 +1,7 @@ import 'package:didvan/main.dart'; import 'package:didvan/models/didvan_plus_model.dart'; +import 'package:didvan/models/didvan_voice_model.dart'; +import 'package:didvan/models/top_banner_model.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/home_page_content/content.dart'; import 'package:didvan/models/home_page_content/home_page_content.dart'; @@ -24,6 +26,8 @@ class MainPageState extends CoreProvier { List stories = []; List swotItems = []; DidvanPlusModel? didvanPlus; + DidvanVoiceModel? didvanVoice; + TopBannerModel? topBanner; Future _getMainPageContent() async { final service = RequestService(RequestHelper.mainPageContent); @@ -53,27 +57,83 @@ class MainPageState extends CoreProvier { Future _getDidvanPlus() async { debugPrint('🎬 Fetching Didvan Plus data...'); + debugPrint('🎬 URL: ${RequestHelper.didvanPlus}'); + debugPrint('🎬 Token exists: ${RequestService.token != null}'); + try { final service = RequestService(RequestHelper.didvanPlus); await service.httpGet(); - debugPrint('🎬 Didvan Plus response: ${service.isSuccess}'); - debugPrint('🎬 Didvan Plus result: ${service.result}'); - - if (service.isSuccess) { - final data = service.result['result'] ?? service.result; - didvanPlus = DidvanPlusModel.fromJson(data); - debugPrint('✅ Didvan Plus loaded: ${didvanPlus?.id}'); - debugPrint('✅ Video file: ${didvanPlus?.file}'); - debugPrint('✅ Image: ${didvanPlus?.image}'); - notifyListeners(); + + debugPrint('🎬 Didvan Plus statusCode: ${service.statusCode}'); + + // Check status code directly since response came + if (service.statusCode == 200) { + // Get the raw data using data() method + final rawData = service.data('result'); + debugPrint('🎬 Raw data type: ${rawData.runtimeType}'); + + if (rawData is List && rawData.isNotEmpty) { + debugPrint('🎬 Data is List with ${rawData.length} items, taking first'); + didvanPlus = DidvanPlusModel.fromJson(Map.from(rawData[0])); + debugPrint('✅ Didvan Plus loaded: ${didvanPlus?.id}'); + debugPrint('✅ Video file: ${didvanPlus?.file}'); + debugPrint('✅ Image: ${didvanPlus?.image}'); + notifyListeners(); + } else if (rawData is Map) { + debugPrint('🎬 Data is Map, using directly'); + didvanPlus = DidvanPlusModel.fromJson(Map.from(rawData)); + debugPrint('✅ Didvan Plus loaded: ${didvanPlus?.id}'); + notifyListeners(); + } else { + debugPrint('⚠️ Didvan Plus: Empty or unexpected data format'); + } } else { - debugPrint('⚠️ Didvan Plus: No result in response'); + debugPrint('⚠️ Didvan Plus: Request failed with status ${service.statusCode}'); } } catch (e) { debugPrint('❌ Failed to load Didvan Plus: $e'); } } + Future _getDidvanVoice() async { + debugPrint('🎙️ Fetching Didvan Voice data...'); + try { + final service = RequestService(RequestHelper.didvanVoice); + await service.httpGet(); + debugPrint('🎙️ Didvan Voice response: ${service.isSuccess}'); + debugPrint('🎙️ Didvan Voice result: ${service.result}'); + + if (service.isSuccess) { + final data = service.result['result'] ?? service.result; + didvanVoice = DidvanVoiceModel.fromJson(data); + debugPrint('✅ Didvan Voice loaded: ${didvanVoice?.id}'); + debugPrint('✅ Audio file: ${didvanVoice?.file}'); + notifyListeners(); + } else { + debugPrint('⚠️ Didvan Voice: No result in response'); + } + } catch (e) { + debugPrint('❌ Failed to load Didvan Voice: $e'); + } + } + + Future _getTopBanner() async { + try { + final service = RequestService(RequestHelper.topBanner); + await service.httpGet(); + if (service.statusCode == 200) { + final data = service.result['result'] ?? service.result; + topBanner = TopBannerModel.fromJson(data); + debugPrint('✅ Top Banner loaded: ${topBanner?.id}'); + notifyListeners(); + } else { + debugPrint('⚠️ Top Banner: No result in response'); + } + } catch (e) { + debugPrint('❌ Failed to load Top Banner: $e'); + } + } + Future _fetchStories() async { try { stories = await StoryService.getStories(); @@ -94,6 +154,8 @@ class MainPageState extends CoreProvier { _fetchStories(), _getSwotItems(), _getDidvanPlus(), + _getDidvanVoice(), + _getTopBanner(), ]); debugPrint("✅ All main page data loaded"); appState = AppState.idle; diff --git a/lib/views/home/main/widgets/didvan_voice_section.dart b/lib/views/home/main/widgets/didvan_voice_section.dart new file mode 100644 index 0000000..aa519a2 --- /dev/null +++ b/lib/views/home/main/widgets/didvan_voice_section.dart @@ -0,0 +1,243 @@ +import 'package:didvan/models/didvan_voice_model.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:just_audio/just_audio.dart'; + +class DidvanVoiceSection extends StatefulWidget { + final DidvanVoiceModel didvanVoice; + + const DidvanVoiceSection({ + super.key, + required this.didvanVoice, + }); + + @override + State createState() => _DidvanVoiceSectionState(); +} + +class _DidvanVoiceSectionState extends State { + late AudioPlayer _audioPlayer; + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + _initializeAudio(); + } + + void _initializeAudio() async { + _audioPlayer = AudioPlayer(); + final audioUrl = + '${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}'; + + debugPrint('🎙️ Didvan Voice Audio URL: $audioUrl'); + + try { + await _audioPlayer.setUrl(audioUrl); + setState(() { + _isInitialized = true; + }); + debugPrint('✅ Audio initialized successfully'); + } catch (e) { + debugPrint('❌ Audio initialization error: $e'); + } + } + + @override + void dispose() { + _audioPlayer.dispose(); + super.dispose(); + } + + String _formatDuration(Duration? duration) { + if (duration == null) return '00:00'; + final minutes = duration.inMinutes; + final seconds = duration.inSeconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + final imageUrl = '${RequestHelper.baseUrl}${widget.didvanVoice.image}'; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color.fromRGBO(0, 69, 92, 1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(8), topRight: Radius.circular(8)), + child: Image.network( + imageUrl, + width: 80, + height: 130, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 80, + height: 130, + color: Colors.grey.shade800, + child: const Icon(Icons.music_note, color: Colors.white), + ); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + widget.didvanVoice.title, + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + final newPosition = + _audioPlayer.position + const Duration(seconds: 10); + _audioPlayer.seek(newPosition); + }, + icon: const Icon(Icons.forward_10, color: Colors.white), + iconSize: 28, + ), + StreamBuilder( + stream: _audioPlayer.playerStateStream, + builder: (context, snapshot) { + final playerState = snapshot.data; + final processingState = playerState?.processingState ?? + ProcessingState.idle; + final playing = playerState?.playing ?? false; + + if (processingState == ProcessingState.loading || + processingState == ProcessingState.buffering) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + width: 48, + height: 48, + child: const CircularProgressIndicator( + color: Colors.white, + ), + ); + } else if (!playing) { + return IconButton( + onPressed: _audioPlayer.play, + icon: const Icon(Icons.play_circle_filled, + color: Colors.white), + iconSize: 48, + ); + } else { + return IconButton( + onPressed: _audioPlayer.pause, + icon: const Icon(Icons.pause_circle_filled, + color: Colors.white), + iconSize: 48, + ); + } + }, + ), + IconButton( + onPressed: () { + final newPosition = + _audioPlayer.position - const Duration(seconds: 10); + _audioPlayer.seek(newPosition < Duration.zero + ? Duration.zero + : newPosition); + }, + icon: const Icon(Icons.replay_10, color: Colors.white), + iconSize: 28, + ), + ], + ), + StreamBuilder( + stream: _audioPlayer.durationStream, + builder: (context, snapshot) { + final duration = snapshot.data; + return StreamBuilder( + stream: _audioPlayer.positionStream, + builder: (context, snapshot) { + final position = snapshot.data ?? Duration.zero; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + _formatDuration(duration), + fontSize: 12, + color: Colors.white70, + ), + DidvanText( + _formatDuration(position), + fontSize: 12, + color: Colors.white70, + ), + ], + ), + ); + }, + ); + }, + ), + const SizedBox(height: 4), + Directionality( + textDirection: TextDirection.ltr, + child: StreamBuilder( + stream: _audioPlayer.durationStream, + builder: (context, snapshot) { + final duration = snapshot.data ?? Duration.zero; + return StreamBuilder( + stream: _audioPlayer.positionStream, + builder: (context, snapshot) { + var position = snapshot.data ?? Duration.zero; + if (position > duration) { + position = duration; + } + return SliderTheme( + data: const SliderThemeData( + thumbShape: + RoundSliderThumbShape(enabledThumbRadius: 6), + overlayShape: + RoundSliderOverlayShape(overlayRadius: 12), + trackHeight: 4, + activeTrackColor: Colors.white70, + inactiveTrackColor: Colors.white30, + thumbColor: Colors.transparent, + overlayColor: Colors.transparent, + ), + child: Slider( + value: position.inMilliseconds.toDouble(), + max: duration.inMilliseconds.toDouble(), + onChanged: (value) { + _audioPlayer.seek( + Duration(milliseconds: value.toInt())); + }, + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/home/media/media_page.dart b/lib/views/home/media/media_page.dart index eaa5879..3787db0 100644 --- a/lib/views/home/media/media_page.dart +++ b/lib/views/home/media/media_page.dart @@ -71,8 +71,7 @@ class _MediaPageState extends State { ), Container( width: double.infinity, - padding: - const EdgeInsets.symmetric(horizontal: 7), + padding: const EdgeInsets.symmetric(horizontal: 7), child: Container( height: 2, color: const Color.fromRGBO(184, 184, 184, 1), @@ -89,14 +88,14 @@ class _MediaPageState extends State { children: [ ChangeNotifierProvider( create: (_) => PodcastsState(), - child: const PodcastTabPage(key: ValueKey('PodcastTabPage')), + child: + const PodcastTabPage(key: ValueKey('PodcastTabPage')), ), ChangeNotifierProvider( create: (_) => PodcastsState(), - child: - const VideoCastTabPage(key: ValueKey('VideoCastTabPage')), + child: const VideoCastTabPage( + key: ValueKey('VideoCastTabPage')), ), - const SingleChildScrollView( key: ValueKey('MainPageBanner'), child: Padding( @@ -124,4 +123,4 @@ class _MediaPageState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/views/home/media/widgets/media_banner_with_thumbnails.dart b/lib/views/home/media/widgets/media_banner_with_thumbnails.dart index 60ce9f5..e933bbe 100644 --- a/lib/views/home/media/widgets/media_banner_with_thumbnails.dart +++ b/lib/views/home/media/widgets/media_banner_with_thumbnails.dart @@ -27,6 +27,21 @@ class _MediaBannerWithThumbnailsState extends State { return Column( children: [ + // Top Banner - only show on first section + if (widget.isFirst && state.topBanner != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: SkeletonImage( + imageUrl: state.topBanner!.image, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + const SizedBox(height: 12), + ], + CarouselSlider.builder( carouselController: _carouselController, itemCount: banners.length, diff --git a/lib/views/onboarding/onboarding_indicator_painter.dart b/lib/views/onboarding/onboarding_indicator_painter.dart new file mode 100644 index 0000000..ccafffc --- /dev/null +++ b/lib/views/onboarding/onboarding_indicator_painter.dart @@ -0,0 +1,54 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class OnboardingIndicatorPainter extends CustomPainter { + final int pageCount; + final int currentPage; + final Color activeColor; + final Color inactiveColor; + final double strokeWidth; + + OnboardingIndicatorPainter({ + required this.pageCount, + required this.currentPage, + required this.activeColor, + required this.inactiveColor, + this.strokeWidth = 6.0, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = min(size.width, size.height) / 2; + + const double gapInDegrees = 30.0; + const double gapInRadians = gapInDegrees * (pi / 180); + + const double arcAngle = (pi / 2) - gapInRadians; + + final startAngles = [-pi / 2, 0.0, pi / 2, pi]; + + for (int i = 0; i < pageCount; i++) { + final paint = Paint() + ..color = (i == currentPage) ? activeColor : inactiveColor + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round; + + final correctedStartAngle = startAngles[i] + (gapInRadians / 2); + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + correctedStartAngle, + arcAngle, + false, + paint, + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/views/onboarding/onboarding_page.dart b/lib/views/onboarding/onboarding_page.dart new file mode 100644 index 0000000..e249b28 --- /dev/null +++ b/lib/views/onboarding/onboarding_page.dart @@ -0,0 +1,300 @@ +import 'package:flutter/material.dart'; +import 'package:didvan/models/onboarding_entity.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/services/storage/storage.dart'; +import 'package:didvan/views/onboarding/onboarding_indicator_painter.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +final List onboardingPages = [ + OnboardingEntity( + imagePath: 'lib/assets/images/onboarding/1.png', + title: 'چرا دیدوان؟', + description: + 'جغرافیای زمانی هیچ‌گاه نمی‌ایستد و سفر به آینده یک سفر شگفت‌انگیز به جایی است که قسمت‌هایی از آن را قبلاً در گذشته دیده‌ایم، قسمت‌هایی از آن را طی مسیر به تدریج می‌بینیم و قسمت‌هایی از آن کاملاً بدیع هستند. برای سفر مطمئن در جغرافیای زمانی ما به سیستم‌های پیش‌نگر نیاز داریم و یکی از ارکان این سیستم‌ها، سامانه‌های رصد راهبردی هستند.', + ), + OnboardingEntity( + imagePath: 'lib/assets/images/onboarding/2.png', + title: 'رادارهای استراتژیک', + description: + 'رادارهای استراتژیک، قلب دیدوان هستند. رادارهایی که قرار است دید 360 درجه‌ای برای مدیران ایجاد کنند و به صورت مستمر گزارش‌هایی تحلیلی را از محیط‌های دور و نزدیک هر کسب و کار در اختیار مدیران قرار ‌دهند. رادارها به صورت موضوعی شامل رادار روند، رادار ریسک، رادار تکنولوژی، رادار استارت‌آپ و ماژول فرصت و تهدید دسته بندی شده‌اند و مدیران می‌توانند پیرامون این گزارشات به بحث و تبادل نظر بپردازند.', + ), + OnboardingEntity( + imagePath: 'lib/assets/images/onboarding/3.png', + title: 'استودیو دیدوان', + description: + '''در استودیو آینده شما می‌توانید ویدیوهایی مستند از مسائلی مانند آموزش مفاهیم آینده‌گرا، تحلیل روندها، تحلیل صنعت و یا هر نوع اطلاعاتی که می‌تواند برای تصمیم‌گیری مدیران ارشد راهگشا باشد را ببینید. +همچنین در بخش دیگر این استودیو و در قالب پادکست، اطلاعاتی از جنس مسائل پیش گفته که ارزش تبدیل به فایل صوتی دارند را، می‌شنوید. + ''', + ), + OnboardingEntity( + imagePath: 'lib/assets/images/onboarding/4.png', + title: 'نبض صنعت', + description: 'در دنیای امروز، تصمیم‌گیری‌های کلیدی در صنایع مختلف به شدت وابسته به داده‌های دقیق و به‌روز شده‌اند. این داده‌ها شامل اطلاعات اقتصادی، مالی، خرید و فروش، بازار سرمایه، ارزهای دیجیتال، شاخص‌های کلان اقتصادی و دیگر حوزه‌ها هستند. استفاده هوشمندانه از داده‌ها این امکان را به مدیران می‌دهد تا تصمیماتی آگاهانه بگیرند، ریسک‌ها را کاهش دهند و عملکرد کلی خود را به سطحی بالاتر ارتقاء دهند.', + ), +]; + +class OnboardingPage extends StatefulWidget { + const OnboardingPage({super.key}); + + @override + State createState() => _OnboardingPageState(); +} + +class _OnboardingPageState extends State { + final PageController _pageController = PageController(); + int _currentPage = 0; + + @override + void initState() { + super.initState(); + _pageController.addListener(() { + if (_pageController.page != null) { + setState(() { + _currentPage = _pageController.page!.round(); + }); + } + }); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onNextTap() async { + if (_currentPage < onboardingPages.length - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + } else { + await _completeOnboarding(); + } + } + + Future _completeOnboarding() async { + await StorageService.setValue(key: 'hasSeenOnboarding', value: 'true'); + if (mounted) { + Navigator.of(context).pushReplacementNamed( + Routes.authenticaion, + arguments: {'isResetPassword': false}, + ); + } + } + + void _skipOnboarding() async { + await _completeOnboarding(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Scaffold( + backgroundColor: colorScheme.background, + body: SafeArea( + child: Stack( + children: [ + Directionality( + textDirection: TextDirection.ltr, + child: PageView.builder( + controller: _pageController, + itemCount: onboardingPages.length, + itemBuilder: (context, index) { + final page = onboardingPages[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: Column( + children: [ + Image.asset( + page.imagePath, + height: 400, + width: double.infinity, + fit: BoxFit.fitWidth, + errorBuilder: (context, error, stackTrace) { + return Container( + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(24), + ), + child: Center( + child: Icon( + Icons.image_outlined, + size: 80, + color: colorScheme.primary.withOpacity(0.3), + ), + ), + ); + }, + ), + const SizedBox(height: 20), + SizedBox( + width: 80, + height: 80, + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + size: const Size(80, 80), + painter: OnboardingIndicatorPainter( + pageCount: onboardingPages.length, + currentPage: _currentPage, + activeColor: + const Color.fromARGB(255, 1, 35, 72), + inactiveColor: + const Color.fromARGB(255, 0, 126, 167)), + ), + GestureDetector( + onTap: _onNextTap, + child: Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + color: Color.fromARGB(255, 1, 35, 72), + shape: BoxShape.circle, + ), + child: Center( + child: SvgPicture.asset( + 'lib/assets/icons/Arrow - Right 2.svg', + width: 35, + height: 35, + colorFilter: const ColorFilter.mode( + Colors.white, + BlendMode.srcIn, + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + page.title, + textAlign: TextAlign.start, + style: + theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: const Color.fromARGB(255, 0, 126, 167), + ), + ), + const SizedBox(height: 16), + Text( + page.description, + textDirection: TextDirection.rtl, + textAlign: TextAlign.start, + style: theme.textTheme.bodyLarge?.copyWith( + color: const Color.fromARGB(255, 41, 41, 41), + height: 1.6, + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ), + ), + + if (_currentPage > 0) + Positioned( + top: 16, + left: 16, + child: Container( + height: 40, + width: 40, + decoration: const BoxDecoration( + color: Color.fromARGB(230, 234, 234, 233), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + child: Center( + child: IconButton( + icon: SvgPicture.asset('lib/assets/icons/arrow-left.svg'), + onPressed: () { + _pageController.previousPage( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + }, + ), + ), + ), + ), + + Positioned( + bottom: 40, + left: 24, + right: 24, + child: Column( + children: [ + Row( + children: [ + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _onNextTap, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'بعدی', + style: theme.textTheme.titleMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 1, + child: OutlinedButton( + onPressed: _skipOnboarding, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + side: BorderSide( + color: colorScheme.primary.withOpacity(0.3), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Text( + 'صرف نظر کردن', + style: theme.textTheme.titleMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/podcasts/studio_details/studio_details.web.dart b/lib/views/podcasts/studio_details/studio_details.web.dart index 99ebb64..94fbe6e 100644 --- a/lib/views/podcasts/studio_details/studio_details.web.dart +++ b/lib/views/podcasts/studio_details/studio_details.web.dart @@ -1,6 +1,6 @@ // ignore_for_file: deprecated_member_use, undefined_prefixed_name -import 'dart:ui' as ui; +import 'dart:ui_web' as ui_web; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/services/media/media.dart'; @@ -48,7 +48,7 @@ class _StudioDetailsState extends State { if (state.studio.type == 'video') { debugPrint("Playing video from URL: ${state.studio.link}"); - ui.platformViewRegistry.registerViewFactory( + ui_web.platformViewRegistry.registerViewFactory( viewType, (int viewId) => html.IFrameElement() ..allowFullscreen = true diff --git a/lib/views/profile/change_password/widgets/new_password_screen.dart b/lib/views/profile/change_password/widgets/new_password_screen.dart index a8421bd..fa5685e 100644 --- a/lib/views/profile/change_password/widgets/new_password_screen.dart +++ b/lib/views/profile/change_password/widgets/new_password_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use + import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/assets.dart'; @@ -204,20 +206,22 @@ class _NewPasswordScreenState extends State child: FadeTransition( opacity: _fadeAnimation, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 25.0), + padding: const EdgeInsets.symmetric(horizontal: 0.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Image.asset( - 'lib/assets/images/empty_states/change password.png', - width: 300, - height: 300, + 'lib/assets/images/empty_states/Change_Password.png', + fit: BoxFit.fitWidth, + width: 500, + height: 400, ), ), const SizedBox(height: 24), Padding( - padding: const EdgeInsets.all(8.0), + padding: + const EdgeInsets.only(right: 25, bottom: 8), child: DidvanText( 'تغییر رمز عبور', style: Theme.of(context) diff --git a/lib/views/profile/change_password/widgets/verify_otp_screen.dart b/lib/views/profile/change_password/widgets/verify_otp_screen.dart index 9277014..c2c0111 100644 --- a/lib/views/profile/change_password/widgets/verify_otp_screen.dart +++ b/lib/views/profile/change_password/widgets/verify_otp_screen.dart @@ -235,24 +235,20 @@ class _VerifyOtpScreenState extends State ), ], children: [ - const SizedBox(height: 24), SlideTransition( position: _contentSlideAnimation, child: FadeTransition( opacity: _fadeAnimation, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 25.0), + padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: Image.asset( - 'lib/assets/images/empty_states/change password.png', - height: 320, - ), - ), + Image.asset( + 'lib/assets/images/empty_states/Change_Password.png', + fit: BoxFit.fitWidth, + width: double.infinity, + height: 400, ), const SizedBox(height: 24), DidvanText( diff --git a/lib/views/profile/direct_list/direct_list.dart b/lib/views/profile/direct_list/direct_list.dart index 8a7ba5b..93c7269 100644 --- a/lib/views/profile/direct_list/direct_list.dart +++ b/lib/views/profile/direct_list/direct_list.dart @@ -204,9 +204,14 @@ class _DirectListState extends State ), childCount: state.chatRooms.length, enableEmptyState: state.chatRooms.isEmpty, - emptyState: EmptyState( - asset: Assets.emptyBookmark, - title: 'پیامی وجود ندارد', + paddingEmptyState: 0, + emptyState: const EmptyState( + asset: 'lib/assets/images/empty_states/Empty_List.png', + height: 500, + title: 'لیست خالی است', + titleColor: Color.fromARGB(255, 0, 126, 167), + subtitle: + 'در حال حاضر آیتمی در این بخش ثبت نشده است. هر زمان مورد جدیدی اضافه شود، در اینجا نمایش داده می‌شود.', ), ), ], diff --git a/lib/views/profile/profile.dart b/lib/views/profile/profile.dart index afc0a24..b935e89 100644 --- a/lib/views/profile/profile.dart +++ b/lib/views/profile/profile.dart @@ -119,7 +119,9 @@ class _ProfilePageState extends State Navigator.of(context).pushNamed(Routes.bookmarks); }, icon: SvgPicture.asset( - 'lib/assets/icons/hugeicons_telescope-01.svg',color: Theme.of(context).colorScheme.caption,)), + 'lib/assets/icons/hugeicons_telescope-01.svg', + color: Theme.of(context).colorScheme.caption, + )), IconButton( onPressed: () => Navigator.of(context).pop(), icon: SvgPicture.asset( @@ -223,7 +225,9 @@ class _ProfilePageState extends State .textTheme .titleMedium ?.copyWith( - color: DesignConfig.isDark? Colors.white : Colors.black, + color: DesignConfig.isDark + ? Colors.white + : Colors.black, fontWeight: FontWeight.normal, fontSize: 15), ), @@ -233,8 +237,9 @@ class _ProfilePageState extends State MenuOption( titleWidget: DidvanText('ویرایش پروفایل', style: TextStyle( - color: - Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), onTap: () => Navigator.of(context) .pushNamed(Routes.editProfile), iconWidget: SvgPicture.asset( @@ -244,8 +249,9 @@ class _ProfilePageState extends State MenuOption( titleWidget: DidvanText('تغییر رمز عبور ', style: TextStyle( - color: - Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), iconWidget: SvgPicture.asset('lib/assets/icons/lock.svg'), onTap: () => Navigator.of(context).pushNamed( @@ -314,7 +320,9 @@ class _ProfilePageState extends State .textTheme .titleMedium ?.copyWith( - color: DesignConfig.isDark? Colors.white : Colors.black, + color: DesignConfig.isDark + ? Colors.white + : Colors.black, fontWeight: FontWeight.normal, fontSize: 15), ), @@ -324,8 +332,9 @@ class _ProfilePageState extends State MenuOption( titleWidget: DidvanText('تنظیمات اعلان‌ها', style: TextStyle( - color: - Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), onTap: () => Navigator.of(context) .pushNamed(Routes.notificationSettings), iconWidget: SvgPicture.asset( @@ -335,7 +344,9 @@ class _ProfilePageState extends State MenuOption( titleWidget: DidvanText('ظاهر اپلیکیشن', style: TextStyle( - color: Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), iconWidget: SvgPicture.asset( 'lib/assets/icons/fluent_paint-brush-16-regular.svg'), onTap: () { @@ -347,8 +358,7 @@ class _ProfilePageState extends State ? 'lib/assets/icons/arrow-up2.svg' : 'lib/assets/icons/arrow-down.svg', height: 25, - color: - Theme.of(context).colorScheme.caption, + color: Theme.of(context).colorScheme.caption, )), const SizedBox(height: 8), AnimatedVisibility( @@ -370,7 +380,9 @@ class _ProfilePageState extends State : 'ایران یکان', titleWidget: DidvanText('فونت', style: TextStyle( - color: Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), onTap: _showFontFamilyBottomSheet, iconWidget: SvgPicture.asset( 'lib/assets/icons/hugeicons_edit-01.svg'), @@ -382,10 +394,11 @@ class _ProfilePageState extends State child: MenuOption( suffix: fontScaleSuffix( state.fontSizeScale), - titleWidget: DidvanText( - 'اندازه متن', + titleWidget: DidvanText('اندازه متن', style: TextStyle( - color: Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), onTap: _showFontScaleBottomSheet, iconWidget: SvgPicture.asset( 'lib/assets/icons/smallcaps.svg'), @@ -404,7 +417,9 @@ class _ProfilePageState extends State const SizedBox(width: 4), DidvanText('تم برنامه', style: TextStyle( - color: Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), ], ), const SizedBox(height: 16), @@ -466,7 +481,9 @@ class _ProfilePageState extends State .textTheme .titleMedium ?.copyWith( - color: DesignConfig.isDark? Colors.white : Colors.black, + color: DesignConfig.isDark + ? Colors.white + : Colors.black, fontWeight: FontWeight.normal, fontSize: 15), ), @@ -478,8 +495,9 @@ class _ProfilePageState extends State 'lib/assets/icons/info-circle.svg'), titleWidget: DidvanText('درباره دیدوان', style: TextStyle( - color: - Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), onTap: () => launchUrlString( 'https://didvan.com/#info', mode: LaunchMode.inAppWebView), @@ -490,7 +508,9 @@ class _ProfilePageState extends State 'lib/assets/icons/call-calling.svg'), titleWidget: DidvanText('تماس با ما', style: TextStyle( - color: Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), onTap: () { Navigator.of(context) .pushNamed(Routes.directList) @@ -599,8 +619,9 @@ class _ProfilePageState extends State 'lib/assets/icons/security-safe.svg'), titleWidget: DidvanText('حریم خصوصی', style: TextStyle( - color: - Theme.of(context).colorScheme.caption)), + color: Theme.of(context) + .colorScheme + .caption)), onTap: () => launchUrlString( 'https://didvan.com/terms-of-use#privacy', mode: LaunchMode.inAppWebView), @@ -685,7 +706,8 @@ class _ProfilePageState extends State ), ), title: 'انتخاب فونت برنامه', - titleIcon: DidvanIcons.font_regular, + titleIconWidget: + SvgPicture.asset('lib/assets/icons/hugeicons_edit-01.svg'), onDismissed: () => state.fontFamily = family, onConfirmed: () => themeProvider.fontFamily = state.fontFamily, ), @@ -734,7 +756,7 @@ class _ProfilePageState extends State ), ), title: 'انتخاب اندازه متن', - titleIcon: DidvanIcons.font_regular, + titleIconWidget: SvgPicture.asset('lib/assets/icons/smallcaps.svg'), onDismissed: () => state.fontSizeScale = scale, onConfirmed: () => themeProvider.fontScale = state.fontSizeScale, ), @@ -760,7 +782,7 @@ class _ProfilePageState extends State width: 70, height: 37, decoration: BoxDecoration( - color: const Color.fromARGB(255, 200, 224, 244), // بکگراند آبی + color: const Color.fromARGB(255, 200, 224, 244), borderRadius: BorderRadius.circular(22), ), child: AnimatedAlign( diff --git a/lib/views/splash/splash.dart b/lib/views/splash/splash.dart index 7733bfc..6adeeff 100644 --- a/lib/views/splash/splash.dart +++ b/lib/views/splash/splash.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use, avoid_print + import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/assets.dart'; @@ -36,7 +38,6 @@ class _SplashState extends State with TickerProviderStateMixin { void initState() { super.initState(); - // Initialize animation controller _pulseController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, @@ -50,7 +51,6 @@ class _SplashState extends State with TickerProviderStateMixin { curve: Curves.easeInOut, )); - // Start pulse animation _pulseController.repeat(reverse: true); themeProvider = context.read(); @@ -69,7 +69,7 @@ class _SplashState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.light; + final isDark = Theme.of(context).brightness == Brightness.dark; return AnnotatedRegion( value: DesignConfig.systemUiOverlayStyle.copyWith( @@ -79,309 +79,136 @@ class _SplashState extends State with TickerProviderStateMixin { ), child: Scaffold( backgroundColor: colorScheme.background, - body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.background, - colorScheme.background.withOpacity(0.8), - colorScheme.primary.withOpacity(0.05), - ], - ), - ), - child: Stack( - children: [ - // Floating particles background - _buildFloatingParticles(colorScheme), - - SafeArea( - child: Column( - children: [ - // Top section with logo - Expanded( - flex: 3, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Logo with subtle animation - TweenAnimationBuilder( - duration: const Duration(milliseconds: 800), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.scale( - scale: 0.7 + (0.3 * value), - child: Opacity( - opacity: value, - child: AnimatedBuilder( - animation: _pulseAnimation, - builder: (context, child) { - return Transform.scale( - scale: _pulseAnimation.value, - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: RadialGradient( - colors: [ - colorScheme.primary.withOpacity(0.05), - Colors.transparent, - ], - ), - boxShadow: [ - BoxShadow( - color: colorScheme.primary.withOpacity(0.15), - blurRadius: 30, - spreadRadius: 8, - offset: const Offset(0, 15), - ), - ], - ), - child: SvgPicture.asset( - Assets.horizontalLogoWithText, - height: 80, - ), - ), - ); - }, - ), - ), - ); - }, - ), - ], - ), - ), - ), - - // Bottom section with loading/error - Expanded( - flex: 2, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 48), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!_errorOccured) ...[ - // Modern loading indicator - _buildModernLoader(colorScheme), - const SizedBox(height: 24), - // Loading text with fade animation - AnimatedBuilder( - animation: _pulseController, - builder: (context, child) { - return Opacity( - opacity: 0.4 + (0.4 * _pulseController.value), - child: Text( - 'در حال بارگذاری...', - style: TextStyle( - fontSize: 16, - color: colorScheme.checkFav.withOpacity(0.7), - fontWeight: FontWeight.w400, - letterSpacing: 0.8, - ), - ), - ); - }, - ), - ], - - if (_errorOccured) ...[ - // Error state with simple and clean design - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Simple error icon - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: colorScheme.error.withOpacity(0.1), - borderRadius: BorderRadius.circular(30), - ), - child: Icon( - Icons.wifi_off_rounded, - size: 28, - color: colorScheme.error, - ), - ), - const SizedBox(height: 20), - // Clean error message - Text( - 'مشکل در اتصال', - style: TextStyle( - fontSize: 16, - color: colorScheme.checkFav, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - Text( - 'لطفاً اتصال خود را بررسی کنید', - style: TextStyle( - fontSize: 14, - color: colorScheme.checkFav.withOpacity(0.6), - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 24), - // Simple retry button - _buildRetryButton(colorScheme), - ], - ), - ], - ], - ), - ), - ), - - // Brand tagline at bottom - Padding( - padding: const EdgeInsets.only(bottom: 32), - child: TweenAnimationBuilder( - duration: const Duration(milliseconds: 1000), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Opacity( - opacity: value * 0.6, - child: Text( - 'توسعه یافته توسط فرتاک', - style: TextStyle( - fontSize: 14, - color: colorScheme.checkFav, - fontWeight: FontWeight.w500, - letterSpacing: 1, - ), + body: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(flex: 2), + + TweenAnimationBuilder( + duration: const Duration(milliseconds: 800), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Transform.scale( + scale: _pulseAnimation.value, + child: SvgPicture.asset( + Assets.horizontalLogoWithText, + height: 80, ), ); }, ), + ); + }, + ), + + const SizedBox(height: 60), + + if (!_errorOccured) ...[ + SizedBox( + width: 60, + height: 60, + child: Image.asset( + Assets.loadingAnimation, + fit: BoxFit.contain, ), - ], - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildFloatingParticles(ColorScheme colorScheme) { - return AnimatedBuilder( - animation: _pulseController, - builder: (context, child) { - return Stack( - children: [ - // Particle 1 - Positioned( - top: 100 + (20 * _pulseController.value), - right: 50 + (10 * _pulseController.value), - child: Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: colorScheme.primary.withOpacity(0.3), - shape: BoxShape.circle, - ), - ), - ), - // Particle 2 - Positioned( - top: 200 + (15 * (1 - _pulseController.value)), - left: 80 + (8 * _pulseController.value), - child: Container( - width: 4, - height: 4, - decoration: BoxDecoration( - color: colorScheme.secondary.withOpacity(0.4), - shape: BoxShape.circle, - ), - ), - ), - // Particle 3 - Positioned( - bottom: 180 + (25 * _pulseController.value), - right: 100 + (12 * (1 - _pulseController.value)), - child: Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: colorScheme.primary.withOpacity(0.2), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: colorScheme.primary.withOpacity(0.1), - blurRadius: 10, - spreadRadius: 3, + ), + const SizedBox(height: 24), + AnimatedBuilder( + animation: _pulseController, + builder: (context, child) { + return Opacity( + opacity: 0.4 + (0.4 * _pulseController.value), + child: Text( + 'در حال بارگذاری...', + style: TextStyle( + fontSize: 14, + color: colorScheme.onBackground.withOpacity(0.6), + fontWeight: FontWeight.w400, + ), + ), + ); + }, + ), + ] else ...[ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: colorScheme.error.withOpacity(0.1), + shape: BoxShape.circle, ), - ], + child: Icon( + Icons.wifi_off_rounded, + size: 24, + color: colorScheme.error, + ), + ), + const SizedBox(height: 16), + Text( + 'مشکل در اتصال', + style: TextStyle( + fontSize: 16, + color: colorScheme.onBackground, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'لطفاً اتصال اینترنت خود را بررسی کنید', + style: TextStyle( + fontSize: 13, + color: colorScheme.onBackground.withOpacity(0.6), + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + setState(() { + _errorOccured = false; + }); + _initialize(themeProvider, userProvider, mediaProvider); + }, + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'تلاش مجدد', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + + const Spacer(flex: 3), + + Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Text( + 'توسعه یافته توسط فرتاک', + style: TextStyle( + fontSize: 12, + color: colorScheme.onBackground.withOpacity(0.4), + fontWeight: FontWeight.w400, + ), + ), ), - ), + ], ), - // Particle 4 - Positioned( - bottom: 120 + (18 * (1 - _pulseController.value)), - left: 60 + (15 * _pulseController.value), - child: Container( - width: 5, - height: 5, - decoration: BoxDecoration( - color: colorScheme.tertiary.withOpacity(0.35), - shape: BoxShape.circle, - ), - ), - ), - ], - ); - }, - ); - } - - Widget _buildModernLoader(ColorScheme colorScheme) { - // استفاده از loading animation اصلی و قدیمی دیدوان - return SizedBox( - width: 60, - height: 60, - child: Image.asset( - Assets.loadingAnimation, - width: 60, - height: 60, - fit: BoxFit.contain, - ), - ); - } - - Widget _buildRetryButton(ColorScheme colorScheme) { - return SizedBox( - width: 140, - height: 44, - child: ElevatedButton( - onPressed: () { - setState(() { - _errorOccured = false; - }); - _initialize(themeProvider, userProvider, mediaProvider); - }, - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - elevation: 0, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(22), - ), - ), - child: const Text( - 'تلاش مجدد', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, ), ), ), @@ -408,6 +235,13 @@ class _SplashState extends State with TickerProviderStateMixin { await AppInitializer.setupServices(navigatorKey.currentContext!); + final String? hasSeenOnboarding = await StorageService.getValue(key: 'hasSeenOnboarding'); + + if (hasSeenOnboarding == null || hasSeenOnboarding != 'true') { + await navigatorKey.currentState!.pushReplacementNamed(Routes.onboarding); + return; + } + final String? token = await userProvider.setAndGetToken(); print("detected token as $token"); @@ -442,7 +276,6 @@ class _SplashState extends State with TickerProviderStateMixin { print("token route is $token"); String extractedPath = initialURI?.path.toString() == '/' ? Routes.home : initialURI?.path.toString() ?? Routes.home; final String destinationRoute = token == null ? Routes.authenticaion : extractedPath; - // Set showDialogs to false to prevent welcome popups dynamic routeArguments = token == null ? {'isResetPassword': false} : {'showDialogs': false}; diff --git a/lib/views/widgets/ai_chat_dialog.dart b/lib/views/widgets/ai_chat_dialog.dart index 3e602f2..5486ef3 100644 --- a/lib/views/widgets/ai_chat_dialog.dart +++ b/lib/views/widgets/ai_chat_dialog.dart @@ -444,10 +444,13 @@ class _AiChatDialogState extends State ), shape: BoxShape.circle, ), - child: IconButton( - icon: const Icon(Icons.headset_mic_rounded, - color: Colors.white, size: 20), - onPressed: () { + child: GestureDetector( + child: Padding( + padding: const EdgeInsets.all(6.0), + child: SvgPicture.asset('lib/assets/icons/voice-square.svg', + color: Colors.white, height: 35), + ), + onTap: () { Navigator.pop(context); showDialog( context: context, @@ -455,8 +458,6 @@ class _AiChatDialogState extends State builder: (context) => const AiVoiceChatDialog(), ); }, - splashRadius: 20, - tooltip: 'گفتگوی صوتی', ), ), Container( diff --git a/lib/views/widgets/state_handlers/empty_connection.dart b/lib/views/widgets/state_handlers/empty_connection.dart index c6543bb..cc353f6 100644 --- a/lib/views/widgets/state_handlers/empty_connection.dart +++ b/lib/views/widgets/state_handlers/empty_connection.dart @@ -1,4 +1,3 @@ -import 'package:didvan/constants/assets.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:flutter/material.dart'; @@ -8,11 +7,21 @@ class EmptyConnection extends StatelessWidget { @override Widget build(BuildContext context) { - return EmptyState( - asset: Assets.emptyConnection, - title: 'ارتباط با اینترنت قطع شد...', - action: onRetry, - buttonTitle: 'تلاش دوباره', + return Column( + children: [ + const SizedBox( + height: 65, + ), + EmptyState( + asset: 'lib/assets/images/empty_states/Connection.png', + height: 550, + title: 'اتصال اینترنت برقرار نیست', + titleColor: const Color.fromARGB(255, 0, 126, 167), + action: onRetry, + subtitle: 'لطفاً اتصال وای‌فای یا داده تلفن همراه خود را بررسی کنید.', + buttonTitle: 'تلاش مجدد', + ), + ], ); } } diff --git a/lib/views/widgets/state_handlers/empty_list.dart b/lib/views/widgets/state_handlers/empty_list.dart index 9be66b6..8976bff 100644 --- a/lib/views/widgets/state_handlers/empty_list.dart +++ b/lib/views/widgets/state_handlers/empty_list.dart @@ -1,4 +1,3 @@ -import 'package:didvan/constants/assets.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:flutter/material.dart'; @@ -7,9 +6,13 @@ class EmptyList extends StatelessWidget { @override Widget build(BuildContext context) { - return EmptyState( - asset: Assets.emptyBookmark, - title: 'لیست خالی است', + return const EmptyState( + asset: 'lib/assets/images/empty_states/Bookmark2.png', + height: 600, + title: 'رصدخانه شما هنوز خالی است', + titleColor: Color.fromARGB(255, 0, 126, 167), + subtitle: + "اینجا فضای شخصی شما برای ذخیره و دسترسی سریع به مهم‌ترین تحلیل‌ها، گزارش‌ها و رسانه‌هاست. برای افزودن محتوا، کافیست روی آیکون بوکمارک در کنار هر مطلب کلیک کنید.", ); } } diff --git a/lib/views/widgets/state_handlers/empty_result.dart b/lib/views/widgets/state_handlers/empty_result.dart index 1ae6b8f..738e714 100644 --- a/lib/views/widgets/state_handlers/empty_result.dart +++ b/lib/views/widgets/state_handlers/empty_result.dart @@ -1,4 +1,3 @@ -import 'package:didvan/constants/assets.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:flutter/material.dart'; @@ -9,10 +8,15 @@ class EmptyResult extends StatelessWidget { @override Widget build(BuildContext context) { return EmptyState( - asset: Assets.emptyResult, - title: 'نتیجه‌ای پیدا نشد', - buttonTitle: 'تغییر جستجو', - action: onNewSearch, + asset: 'lib/assets/images/empty_states/Empty-Result.png', + height: 600, + title: 'نتیجه‌ای یافت نشد', + subtitle: + 'لطفاً از کلمات کلیدی دیگری استفاده کنید یا املای کلمه را بررسی نمایید.', + buttonTitle: 'بازگشت به صفحه اصلی', + action: () { + Navigator.of(context).popUntil((route) => route.isFirst); + }, ); } } diff --git a/lib/views/widgets/state_handlers/empty_state.dart b/lib/views/widgets/state_handlers/empty_state.dart index ce76662..d52311d 100644 --- a/lib/views/widgets/state_handlers/empty_state.dart +++ b/lib/views/widgets/state_handlers/empty_state.dart @@ -26,29 +26,50 @@ class EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { + final isSvg = asset.toLowerCase().endsWith('.svg'); + return Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: height ?? 210, child: SvgPicture.asset(asset)), - const SizedBox(height: 16), - DidvanText( - title, - style: Theme.of(context).textTheme.displaySmall, - color: titleColor ?? Theme.of(context).colorScheme.caption, + SizedBox( + width: double.infinity, + height: height ?? 300, + child: isSvg + ? SvgPicture.asset(asset, fit: BoxFit.contain) + : Image.asset(asset, + fit: BoxFit.fitWidth, height: height, width: double.infinity), + ), + Padding( + padding: const EdgeInsets.only(right: 20, left: 20), + child: DidvanText( + title, + style: Theme.of(context).textTheme.displaySmall, + color: titleColor ?? Theme.of(context).colorScheme.caption, + textAlign: TextAlign.start, + ), ), if (subtitle != null) const SizedBox(height: 8), if (subtitle != null) - DidvanText( - subtitle!, - color: Theme.of(context).colorScheme.caption, + Padding( + padding: const EdgeInsets.only(right: 20, left: 20), + child: DidvanText( + subtitle!, + color: Theme.of(context).colorScheme.caption, + ), ), if (action != null) const SizedBox(height: 16), if (action != null) - DidvanButton( - height: 40, - onPressed: action, - title: buttonTitle, - width: 112, + Center( + child: Padding( + padding: const EdgeInsets.all(25.0), + child: DidvanButton( + height: 50, + onPressed: action, + title: buttonTitle, + width: double.infinity, + ), + ), ), ], ); diff --git a/pubspec.yaml b/pubspec.yaml index d6f4284..1e3f80f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -148,6 +148,7 @@ flutter: - lib/assets/images/records/ - lib/assets/images/empty_states/ - lib/assets/images/features/ + - lib/assets/images/onboarding/ - lib/assets/icons/ - lib/assets/animations/ - lib/assets/js/