From d54d466e3d577e84be284415c67683f70eb4ae1c Mon Sep 17 00:00:00 2001 From: "Mr.Jebelli" Date: Mon, 22 Dec 2025 09:44:06 +0330 Subject: [PATCH] fixed some bugs for v5 --- android/app/build.gradle | 32 +- lib/main.dart | 66 +-- lib/providers/user.dart | 24 +- .../home_widget_repository.dart | 375 ++++++++++++------ lib/services/notification/firebase_api.dart | 40 +- .../notification/notification_service.dart | 34 +- lib/views/ai/bot_assistants_page.dart | 1 + lib/views/ai/history_ai_chat_page.dart | 1 + lib/views/ai/info_page.dart | 1 + lib/views/authentication/authentication.dart | 6 +- lib/views/comments/comments.dart | 2 +- .../didvan_plus/didvan_plus_video_player.dart | 1 + .../filtered_bookmark/filtered_bookmark.dart | 1 + .../home/infography/infography_screen.dart | 1 + .../home/main/didvan_voice_list_page.dart | 27 +- lib/views/home/main/main_page.dart | 18 + lib/views/home/main/main_page_state.dart | 152 ++++--- .../widgets/didvan_voice_detail_card.dart | 24 +- .../main/widgets/didvan_voice_section.dart | 319 +++++++-------- lib/views/home/media/media_page.dart | 1 + lib/views/home/media/video_details_page.dart | 2 + .../home/new_statistic/new_statistic.dart | 1 + .../stat_cats_general_screen.dart | 1 + .../home/new_statistic/stock/new_stock.dart | 1 + lib/views/news/news_details/news_details.dart | 1 + lib/views/onboarding/onboarding_page.dart | 1 + lib/views/pdf_viewer/pdf_viewer_page.dart | 1 + .../studio_details/studio_details.mobile.dart | 2 + .../studio_details/studio_details.web.dart | 1 + .../change_password/change_password.dart | 1 + .../radar/radar_details/radar_details.dart | 1 + lib/views/splash/splash.dart | 27 +- lib/views/story_viewer/story_viewer_page.dart | 1 + lib/views/webview/web_view.dart | 1 + lib/views/widgets/ai_voice_chat_dialog.dart | 213 +++++----- lib/views/widgets/carousel_3d.dart | 16 +- lib/views/widgets/didvan/page_view.dart | 98 +++++ lib/views/widgets/didvan/scaffold.dart | 5 +- .../ephemeral/Flutter-Generated.xcconfig | 2 +- .../ephemeral/flutter_export_environment.sh | 2 +- pubspec.yaml | 2 +- web/index.html | 28 +- 42 files changed, 956 insertions(+), 578 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f5ce5a0..ad87f28 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,10 +8,9 @@ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) + localProperties.load(reader) + } } -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { @@ -22,11 +21,6 @@ def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } -def keystoreProperties = new Properties() -def keystorePropertiesFile = rootProject.file('key.properties') -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) -} android { namespace "com.didvan.didvanapp" @@ -34,27 +28,22 @@ android { ndkVersion "28.2.13676358" compileOptions { - // تغییر 1.8 به VERSION_17 sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 - coreLibraryDesugaringEnabled true } kotlinOptions { - // تغییر '1.8' به '17' jvmTarget = '17' } - + sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.didvan.didvanapp" minSdkVersion 24 - //noinspection ExpiredTargetSdkVersion targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -62,12 +51,14 @@ android { signingConfigs { release { - storeFile file("keystore.jks") + // فایل keystore.jks باید داخل پوشه android/app باشد + storeFile file("keystore.jks") storePassword "12799721" keyAlias "upload" keyPassword "12799721" } } + buildTypes { release { signingConfig signingConfigs.release @@ -81,24 +72,18 @@ android { disable 'InvalidPackage' checkReleaseBuilds false } + buildFeatures { viewBinding true } splits { - //configure apks based on ABI abi { enable true reset() include "x86", "x86_64", "armeabi-v7a", "arm64-v8a" universalApk true - } -// density { -// enable true -// reset() -// include "mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi" -// } } } @@ -107,7 +92,6 @@ flutter { } dependencies { - // implementation platform('com.google.firebase:firebase-bom:29.1.0') implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.squareup.picasso:picasso:2.8' implementation "androidx.room:room-runtime:2.2.5" @@ -115,4 +99,4 @@ dependencies { implementation "androidx.sqlite:sqlite:2.1.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' implementation 'androidx.appcompat:appcompat:1.6.1' -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 2503327..c8b4d58 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,6 +34,7 @@ import 'package:didvan/views/ai/bot_assistants_state.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/podcasts/podcasts_state.dart'; import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart'; +import 'package:universal_html/html.dart' as html; final GlobalKey navigatorKey = GlobalKey(); Uri? initialURI; @@ -50,6 +51,13 @@ void main() async { () async { WidgetsFlutterBinding.ensureInitialized(); + if (kIsWeb) { + final loader = html.document.getElementById('loading_indicator'); + if (loader != null) { + loader.remove(); + } + } + try { if (!kIsWeb) { // ignore: deprecated_member_use @@ -214,34 +222,36 @@ class _DidvanState extends State with WidgetsBindingObserver { ), ); - return Container( - color: Theme.of(context).colorScheme.surface, - child: SafeArea( - child: MaterialApp( - scrollBehavior: MyCustomScrollBehavior(), - navigatorKey: navigatorKey, - debugShowCheckedModeBanner: false, - title: 'Didvan', - theme: lightTheme, - darkTheme: darkTheme, - color: lightTheme.primaryColor, - themeMode: themeProvider.themeMode, - onGenerateRoute: (settings) => - RouteGenerator.generateRoute(settings), - builder: BotToastInit(), - navigatorObservers: [BotToastNavigatorObserver()], - initialRoute: "/", - localizationsDelegates: const [ - GlobalCupertinoLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: const [ - Locale("fa", "IR"), - ], - locale: const Locale("fa", "IR"), - ), - ), + return MaterialApp( + scrollBehavior: MyCustomScrollBehavior(), + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + title: 'Didvan', + theme: lightTheme, + darkTheme: darkTheme, + color: lightTheme.primaryColor, + themeMode: themeProvider.themeMode, + onGenerateRoute: (settings) => + RouteGenerator.generateRoute(settings), + builder: (context, child) { + return BotToastInit()( + context, + SafeArea( + child: child!, + ), + ); + }, + navigatorObservers: [BotToastNavigatorObserver()], + initialRoute: "/", + localizationsDelegates: const [ + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: const [ + Locale("fa", "IR"), + ], + locale: const Locale("fa", "IR"), ); }, ), diff --git a/lib/providers/user.dart b/lib/providers/user.dart index 1618f11..34ca549 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -1,6 +1,8 @@ // ignore: depend_on_referenced_packages // ignore_for_file: avoid_print +import 'dart:io'; + import 'package:collection/collection.dart'; import 'package:didvan/main.dart'; import 'package:didvan/models/enums.dart'; @@ -12,6 +14,7 @@ import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/notification/firebase_api.dart'; import 'package:didvan/services/storage/storage.dart'; import 'package:didvan/utils/action_sheet.dart'; +import 'package:flutter/foundation.dart'; class UserProvider extends CoreProvier { late User user; @@ -106,7 +109,6 @@ class UserProvider extends CoreProvier { final RequestService service = RequestService(RequestHelper.userInfo); await service.httpGet(); - // اگر توکن نامعتبر است (401)، فالس برمی‌گردانیم تا توکن پاک شود if (service.statusCode == 401) { print("UserProvider: getUserInfo failed - Unauthorized (401)."); isAuthenticated = false; @@ -149,10 +151,9 @@ class UserProvider extends CoreProvier { "UserProvider: getUserInfo failed. Status: ${service.statusCode}, Error: ${service.errorMessage}"); isAuthenticated = false; - // اصلاح مهم: اگر خطا 401 نیست (مثلاً مشکل سرور یا اینترنت)، Exception پرتاب می‌کنیم - // تا در Splash وارد بخش catch شود و توکن پاک نشود. if (service.statusCode != 401) { - throw Exception("Server Error or Connection Issue: ${service.statusCode}"); + throw Exception( + "Server Error or Connection Issue: ${service.statusCode}"); } return false; @@ -160,9 +161,20 @@ class UserProvider extends CoreProvier { Future _registerFirebaseToken() async { if (FirebaseApi.fcmToken != null) { + String platform = 'unknown'; + if (kIsWeb) { + platform = 'web'; + } else if (Platform.isAndroid) { + platform = 'android'; + } else if (Platform.isIOS) { + platform = 'ios'; + } + final service = RequestService(RequestHelper.firebaseToken, body: { 'token': FirebaseApi.fcmToken, + 'platform': platform, }); + await service.put(); } } @@ -329,7 +341,7 @@ class UserProvider extends CoreProvier { 'new_password': newPassword, }, ); - + await service.post(); if (service.isSuccess) { @@ -342,4 +354,4 @@ class UserProvider extends CoreProvier { return false; } } -} \ No newline at end of file +} diff --git a/lib/services/app_home_widget/home_widget_repository.dart b/lib/services/app_home_widget/home_widget_repository.dart index 1c69c4f..b6fa4e8 100644 --- a/lib/services/app_home_widget/home_widget_repository.dart +++ b/lib/services/app_home_widget/home_widget_repository.dart @@ -147,156 +147,266 @@ class HomeWidgetRepository { } } await HomeWidget.saveWidgetData("uri", ""); - data = null; return; } static NotificationMessage? data; static Future decideWhereToGoNotif() async { - NotificationMessage? data = HomeWidgetRepository.data; - + NotificationMessage? localData = HomeWidgetRepository.data; + + if (localData == null) { + if (kDebugMode) { + print("=== NAVIGATION ABORTED ==="); + print("Reason: Notification data is null"); + print("==========================="); + } + return; + } + + if (RequestService.token == null || + RequestService.token.toString().isEmpty) { + if (kDebugMode) { + print("⏳ Token not loaded yet. Deferring navigation to Home/MainPage."); + } + return; + } + + HomeWidgetRepository.data = null; + if (kDebugMode) { print("=== NAVIGATION DECISION ==="); - print("Notification Data: ${data?.toJson()}"); - print("Type: ${data?.type}"); - print("ID: ${data?.id}"); - print("Link: ${data?.link}"); - print("Notification Type: ${data?.notificationType}"); + print("Notification Data: ${localData.toJson()}"); + print("Type: ${localData.type}"); + print("ID: ${localData.id}"); + print("Link: ${localData.link}"); + print("Notification Type: ${localData.notificationType}"); } - + String route = ""; dynamic args; - bool openComments = data!.notificationType.toString() == "2"; + bool openComments = localData.notificationType.toString() == "2"; - if (data.link.toString().isEmpty || data.link.toString() == "null") { - switch (data.type!) { - case "infography": - route = Routes.infography; - args = { - 'id': int.parse(data.id.toString()), - 'args': const InfographyRequestArgs(page: 0), - 'hasUnmarkConfirmation': false, - 'goToComment': openComments - }; - break; - case "news": - route = Routes.newsDetails; - args = { - 'id': int.parse(data.id.toString()), - 'args': const NewsRequestArgs(page: 0), - 'hasUnmarkConfirmation': false, - 'goToComment': openComments - }; - if (kDebugMode) { - print("News navigation - ID: ${data.id}"); - } - break; - case "radar": - route = Routes.radarDetails; - args = { - 'id': int.parse(data.id.toString()), - 'args': const RadarRequestArgs(page: 0), - 'hasUnmarkConfirmation': false, - 'goToComment': openComments - }; - break; - case "studio": - route = Routes.studioDetails; - args = { - 'type': 'podcast', - 'id': int.parse(data.id.toString()), - 'goToComment': openComments - }; - break; - case "video": - route = Routes.studioDetails; - args = { - 'type': 'podcast', - 'id': int.parse(data.id.toString()), - 'goToComment': openComments - }; - break; - case "podcast": - route = Routes.podcasts; - args = { - 'type': 'podcast', - 'id': int.parse(data.id.toString()), - 'goToComment': openComments - }; - break; - case "startup": - case "technology": - case "trend": - if (data.link != null && data.link!.isNotEmpty && data.link! != "null") { - if (kDebugMode) { - print("Opening external link for ${data.type}: ${data.link}"); - } - AppInitializer.openWebLink( - navigatorKey.currentContext!, - data.link!, - mode: LaunchMode.inAppWebView, - ); - } else if (data.id != null && data.id.toString().isNotEmpty) { - String url = ""; - String title = data.title?.split(" ").join("-") ?? ""; - - switch (data.type) { - case "startup": - url = "https://startup.didvan.app/startup/${data.id}/${RequestService.token}"; - break; - case "technology": - url = "https://tech.didvan.app/technology/${data.id}/$title/${RequestService.token}"; - break; - case "trend": - url = "https://trend.didvan.app/trend/${data.id}/$title/${RequestService.token}"; - break; - } - - if (url.isNotEmpty) { + if (localData.link.toString().isEmpty || + localData.link.toString() == "null") { + if (localData.type == null || localData.type!.isEmpty) { + if (kDebugMode) { + print("=== NAVIGATION ABORTED ==="); + print("Reason: Notification type is null or empty"); + print("Defaulting to home route"); + print("==========================="); + } + route = Routes.home; + } else { + switch (localData.type!) { + case "infography": + if (localData.id == null || localData.id.toString().isEmpty) { if (kDebugMode) { - print("Opening constructed URL for ${data.type}: $url"); + print( + "WARNING: Infography notification without ID - navigating to home"); + } + route = Routes.home; + } else { + route = Routes.infography; + args = { + 'id': int.parse(localData.id.toString()), + 'args': const InfographyRequestArgs(page: 0), + 'hasUnmarkConfirmation': false, + 'goToComment': openComments + }; + } + break; + case "news": + if (localData.id == null || localData.id.toString().isEmpty) { + if (kDebugMode) { + print( + "WARNING: News notification without ID - navigating to home"); + } + route = Routes.home; + } else { + route = Routes.newsDetails; + args = { + 'id': int.parse(localData.id.toString()), + 'args': const NewsRequestArgs(page: 0), + 'hasUnmarkConfirmation': false, + 'goToComment': openComments + }; + if (kDebugMode) { + print("News navigation - ID: ${localData.id}"); + } + } + break; + case "radar": + if (localData.id == null || localData.id.toString().isEmpty) { + if (kDebugMode) { + print( + "WARNING: Radar notification without ID - navigating to home"); + } + route = Routes.home; + } else { + route = Routes.radarDetails; + args = { + 'id': int.parse(localData.id.toString()), + 'args': const RadarRequestArgs(page: 0), + 'hasUnmarkConfirmation': false, + 'goToComment': openComments + }; + } + break; + case "studio": + if (localData.id == null || localData.id.toString().isEmpty) { + if (kDebugMode) { + print( + "WARNING: Studio notification without ID - navigating to home"); + } + route = Routes.home; + } else { + route = Routes.studioDetails; + args = { + 'type': 'podcast', + 'id': int.parse(localData.id.toString()), + 'goToComment': openComments + }; + } + break; + case "video": + if (localData.id == null || localData.id.toString().isEmpty) { + if (kDebugMode) { + print( + "WARNING: Video notification without ID - navigating to home"); + } + route = Routes.home; + } else { + route = Routes.studioDetails; + args = { + 'type': 'podcast', + 'id': int.parse(localData.id.toString()), + 'goToComment': openComments + }; + } + break; + case "podcast": + if (localData.id == null || localData.id.toString().isEmpty) { + if (kDebugMode) { + print( + "WARNING: Podcast notification without ID - navigating to home"); + } + route = Routes.home; + } else { + route = Routes.podcasts; + args = { + 'type': 'podcast', + 'id': int.parse(localData.id.toString()), + 'goToComment': openComments + }; + } + break; + + case "monthly": + route = Routes.monthlyList; + break; + + case "didvanplus": + case "didvan_plus": + route = Routes.didvanPlusList; + args = []; + break; + + case "didvanvoice": + case "didvan_voice": + route = Routes.didvanVoiceList; + args = []; + break; + case "startup": + case "technology": + case "trend": + if (localData.link != null && + localData.link!.isNotEmpty && + localData.link! != "null") { + if (kDebugMode) { + print( + "Opening external link for ${localData.type}: ${localData.link}"); } AppInitializer.openWebLink( navigatorKey.currentContext!, - url, + localData.link!, mode: LaunchMode.inAppWebView, ); + } else if (localData.id != null && + localData.id.toString().isNotEmpty) { + String url = ""; + String title = localData.title?.split(" ").join("-") ?? ""; + + switch (localData.type) { + case "startup": + url = + "https://startup.didvan.app/startup/${localData.id}?accessToken=${RequestService.token}"; + break; + case "technology": + url = + "https://tech.didvan.app/technology/${localData.id}/$title?accessToken=${RequestService.token}"; + break; + case "trend": + url = + "https://trend.didvan.app/trend/${localData.id}/$title?accessToken=${RequestService.token}"; + break; + } + + if (url.isNotEmpty) { + if (kDebugMode) { + print("Opening constructed URL for ${localData.type}: $url"); + } + AppInitializer.openWebLink( + navigatorKey.currentContext!, + url, + mode: LaunchMode.inAppWebView, + ); + } else { + route = Routes.home; + if (kDebugMode) { + print( + "Unable to construct URL for ${localData.type} - navigating to home"); + } + } } else { route = Routes.home; if (kDebugMode) { - print("Unable to construct URL for ${data.type} - navigating to home"); + print( + "No ID or link available for ${localData.type} - navigating to home"); } } - } else { + break; + default: route = Routes.home; if (kDebugMode) { - print("No ID or link available for ${data.type} - navigating to home"); + print( + "Unknown notification type: ${localData.type} - navigating to home"); } - } - break; - default: - route = Routes.home; - if (kDebugMode) { - print("Unknown notification type: ${data.type} - navigating to home"); - } - break; + break; + } } } else { if (kDebugMode) { - print("External link detected: ${data.link}"); + print("External link detected: ${localData.link}"); } - if (data.link!.startsWith('http')) { - String linkWithToken = data.link!; + if (localData.type == 'monthly') { + route = Routes.pdfViewer; + args = { + 'pdfUrl': localData.link, + 'title': localData.title ?? 'ماهنامه', + }; + } else if (localData.link!.startsWith('http')) { + String linkWithToken = localData.link!; if (RequestService.token != null && RequestService.token!.isNotEmpty) { - String separator = data.link!.contains('?') ? '&' : '?'; - linkWithToken = "${data.link}${separator}accessToken=${RequestService.token}"; + String separator = localData.link!.contains('?') ? '&' : '?'; + linkWithToken = + "${localData.link}${separator}accessToken=${RequestService.token}"; } - + if (kDebugMode) { print("Opening external link with token: $linkWithToken"); } - + AppInitializer.openWebLink( navigatorKey.currentContext!, linkWithToken, @@ -304,18 +414,45 @@ class HomeWidgetRepository { ); } } - + if (kDebugMode) { print("Final navigation decision:"); print("Route: $route"); print("Args: $args"); print("==========================="); } - + if (route.isNotEmpty) { - navigatorKey.currentState!.pushNamed(route, arguments: args); + await Future.delayed(const Duration(milliseconds: 1000)); + + if (kDebugMode) { + print("Attempting navigation after delay..."); + print("Navigator ready: ${navigatorKey.currentState != null}"); + } + + int retryCount = 0; + while (navigatorKey.currentState == null && retryCount < 10) { + if (kDebugMode) { + print( + "Navigator not ready, waiting... (attempt ${retryCount + 1}/10)"); + } + await Future.delayed(const Duration(milliseconds: 500)); + retryCount++; + } + + if (navigatorKey.currentState != null) { + if (kDebugMode) { + print("Navigator is ready, performing navigation to: $route"); + } + navigatorKey.currentState!.pushNamed(route, arguments: args); + } else { + if (kDebugMode) { + print( + "ERROR: Navigator still not ready after waiting. Navigation aborted."); + } + } } return; } -} \ No newline at end of file +} diff --git a/lib/services/notification/firebase_api.dart b/lib/services/notification/firebase_api.dart index 4221cbb..93780d8 100644 --- a/lib/services/notification/firebase_api.dart +++ b/lib/services/notification/firebase_api.dart @@ -14,7 +14,15 @@ class FirebaseApi { Future initNotification() async { try { - fcmToken = await _firebaseMessaging.getToken(); + if (kIsWeb) { + fcmToken = await _firebaseMessaging.getToken( + vapidKey: + "BMXHGd93t_htpS7c62ceuuLVVmia2cEDmqxp46g9Vt0B3OxNMKIqN9nupsUMtv2Vq8Yy2sQGIqgCm9FxUSKvssU", + ); + } else { + fcmToken = await _firebaseMessaging.getToken(); + } + if (kDebugMode) { print("fCMToken: $fcmToken"); } @@ -45,16 +53,24 @@ class FirebaseApi { } print("================================================"); } - + try { NotificationMessage data = NotificationMessage.fromJson(initMsg.data); HomeWidgetRepository.data = data; if (kDebugMode) { print("Parsed NotificationMessage: ${data.toJson()}"); + print("Scheduling navigation from terminated state..."); } - await HomeWidgetRepository.decideWhereToGoNotif(); - await StorageService.delete( - key: 'notification${AppInitializer.createNotificationId(data)}'); + // Schedule navigation to happen after app is fully initialized + // This ensures navigatorKey is ready + // Future.delayed(const Duration(milliseconds: 1500), () async { + // if (kDebugMode) { + // print("Executing delayed navigation from terminated state"); + // } + // await HomeWidgetRepository.decideWhereToGoNotif(); + // await StorageService.delete( + // key: 'notification${AppInitializer.createNotificationId(data)}'); + // }); } catch (e) { if (kDebugMode) { print("Error handling initial message: $e"); @@ -74,13 +90,15 @@ class FirebaseApi { } print("================================================"); } - + try { NotificationMessage data = NotificationMessage.fromJson(initMsg.data); HomeWidgetRepository.data = data; if (kDebugMode) { print("Parsed NotificationMessage: ${data.toJson()}"); + print("Scheduling navigation from background state..."); } + await Future.delayed(const Duration(milliseconds: 300)); await HomeWidgetRepository.decideWhereToGoNotif(); await StorageService.delete( key: 'notification${AppInitializer.createNotificationId(data)}'); @@ -97,7 +115,7 @@ class FirebaseApi { void handleMessage(RemoteMessage? message) async { if (message == null) return; - + if (kDebugMode) { print("=== NOTIFICATION RECEIVED (FOREGROUND) ==="); print("Message ID: ${message.messageId}"); @@ -106,7 +124,7 @@ class FirebaseApi { print("TTL: ${message.ttl}"); print("Message Type: ${message.messageType}"); print("Category: ${message.category}"); - + if (message.notification != null) { print("--- NOTIFICATION PAYLOAD ---"); print("Title: ${message.notification!.title}"); @@ -114,7 +132,7 @@ class FirebaseApi { print("Android Image: ${message.notification!.android?.imageUrl}"); print("Apple Image: ${message.notification!.apple?.imageUrl}"); } - + print("--- DATA PAYLOAD ---"); print("Raw Data: ${message.data}"); try { @@ -123,10 +141,10 @@ class FirebaseApi { } catch (e) { print("Error parsing NotificationData: $e"); } - + print("=========================================="); } - + try { await NotificationService.showFirebaseNotification(message); } catch (e) { diff --git a/lib/services/notification/notification_service.dart b/lib/services/notification/notification_service.dart index 83f3e32..7c3a0c4 100644 --- a/lib/services/notification/notification_service.dart +++ b/lib/services/notification/notification_service.dart @@ -16,6 +16,7 @@ class NotificationService { static Future initializeNotification() async { const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); + const InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid); @@ -24,18 +25,20 @@ class NotificationService { onDidReceiveNotificationResponse: _onNotificationResponse, ); - const AndroidNotificationChannel channel = AndroidNotificationChannel( - 'content', - 'Content Notification', - description: 'Notification channel', - importance: Importance.max, - playSound: true, - ); + if (!kIsWeb) { + const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'content', + 'Content Notification', + description: 'Notification channel', + importance: Importance.max, + playSound: true, + ); - await _flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.createNotificationChannel(channel); + await _flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(channel); + } } static Future _onNotificationResponse( @@ -49,7 +52,7 @@ class NotificationService { print("Payload: ${response.payload}"); print("==================================="); } - + try { final payload = response.payload; if (payload != null) { @@ -75,16 +78,17 @@ class NotificationService { if (kDebugMode) { print("=== SHOWING FIREBASE NOTIFICATION ==="); print("Message Data: ${message.data}"); - print("Notification: ${message.notification?.title} - ${message.notification?.body}"); + print( + "Notification: ${message.notification?.title} - ${message.notification?.body}"); } - + try { final data = NotificationMessage.fromJson(message.data); if (kDebugMode) { print("Parsed NotificationMessage: ${data.toJson()}"); print("Notification Type: ${data.notificationType}"); } - + if (data.notificationType!.contains('3')) { if (kDebugMode) { print("Widget notification - calling fetchWidget()"); diff --git a/lib/views/ai/bot_assistants_page.dart b/lib/views/ai/bot_assistants_page.dart index 39b5c75..422c868 100644 --- a/lib/views/ai/bot_assistants_page.dart +++ b/lib/views/ai/bot_assistants_page.dart @@ -28,6 +28,7 @@ class _BotAssistantsPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, appBar: HoshanAppBar( onBack: () => Navigator.pop(context), withActions: false, diff --git a/lib/views/ai/history_ai_chat_page.dart b/lib/views/ai/history_ai_chat_page.dart index 257a80a..99d6ad7 100644 --- a/lib/views/ai/history_ai_chat_page.dart +++ b/lib/views/ai/history_ai_chat_page.dart @@ -61,6 +61,7 @@ class _HistoryAiChatPageState extends State { return true; }, child: Scaffold( + resizeToAvoidBottomInset: true, key: scaffKey, appBar: HoshanAppBar( onBack: () { diff --git a/lib/views/ai/info_page.dart b/lib/views/ai/info_page.dart index 0ca2600..5fa9b45 100644 --- a/lib/views/ai/info_page.dart +++ b/lib/views/ai/info_page.dart @@ -27,6 +27,7 @@ class _InfoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, appBar: HoshanAppBar( withActions: false, withInfo: false, diff --git a/lib/views/authentication/authentication.dart b/lib/views/authentication/authentication.dart index 1d58f43..49a6e66 100644 --- a/lib/views/authentication/authentication.dart +++ b/lib/views/authentication/authentication.dart @@ -42,15 +42,15 @@ class _AuthenticationState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, body: Consumer( builder: (context, state, child) => WillPopScope( onWillPop: () async { - if (state.currentPageIndex == 0) { + if (state.currentPageIndex == 0) { return true; } - // Check if on OTP screen and no password exists if (state.currentPageIndex == 2 && !state.hasPassword) { - state.currentPageIndex = 0; // Go back to username screen + state.currentPageIndex = 0; return false; } state.currentPageIndex--; diff --git a/lib/views/comments/comments.dart b/lib/views/comments/comments.dart index 6b864ed..b097cb1 100644 --- a/lib/views/comments/comments.dart +++ b/lib/views/comments/comments.dart @@ -341,4 +341,4 @@ class _CommentPlaceholder extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/views/didvan_plus/didvan_plus_video_player.dart b/lib/views/didvan_plus/didvan_plus_video_player.dart index e8a2528..2864187 100644 --- a/lib/views/didvan_plus/didvan_plus_video_player.dart +++ b/lib/views/didvan_plus/didvan_plus_video_player.dart @@ -87,6 +87,7 @@ class _DidvanPlusVideoPlayerState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, backgroundColor: Colors.black, body: SafeArea( child: Column( diff --git a/lib/views/home/bookmarks/filtered_bookmark/filtered_bookmark.dart b/lib/views/home/bookmarks/filtered_bookmark/filtered_bookmark.dart index df52de8..0e32c0a 100644 --- a/lib/views/home/bookmarks/filtered_bookmark/filtered_bookmark.dart +++ b/lib/views/home/bookmarks/filtered_bookmark/filtered_bookmark.dart @@ -72,6 +72,7 @@ class _FilteredBookmarksState extends State { final state = context.watch(); return Scaffold( + resizeToAvoidBottomInset: true, appBar: PreferredSize( preferredSize: const Size.fromHeight(90.0), child: AppBar( diff --git a/lib/views/home/infography/infography_screen.dart b/lib/views/home/infography/infography_screen.dart index 428ad7d..8afeb59 100644 --- a/lib/views/home/infography/infography_screen.dart +++ b/lib/views/home/infography/infography_screen.dart @@ -169,6 +169,7 @@ class _InfographyScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.surface, elevation: 0.0, diff --git a/lib/views/home/main/didvan_voice_list_page.dart b/lib/views/home/main/didvan_voice_list_page.dart index 5fafca7..847b597 100644 --- a/lib/views/home/main/didvan_voice_list_page.dart +++ b/lib/views/home/main/didvan_voice_list_page.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:didvan/services/media/voice.dart'; class DidvanVoiceListPage extends StatefulWidget { final List voices; @@ -30,6 +31,12 @@ class _DidvanVoiceListPageState extends State { } } + @override + void dispose() { + VoiceService.audioPlayer.stop(); + super.dispose(); + } + void _onVoiceSelected(DidvanVoiceModel voice) { setState(() { _selectedVoice = voice; @@ -42,6 +49,7 @@ class _DidvanVoiceListPageState extends State { widget.voices.where((v) => v.id != _selectedVoice.id).toList(); return Scaffold( + resizeToAvoidBottomInset: true, appBar: AppBar( title: const DidvanText( 'یک لقمه استراتژی', @@ -73,20 +81,11 @@ class _DidvanVoiceListPageState extends State { const SizedBox(height: 20), if (listItems.isNotEmpty) Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 8), - child: Row( - children: [ - DidvanText( - 'سایر قسمت‌ها', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + color: Colors.grey[300], + thickness: 2, + height: 20, ), ), ListView.builder( diff --git a/lib/views/home/main/main_page.dart b/lib/views/home/main/main_page.dart index e5ce79e..dc9f0b5 100644 --- a/lib/views/home/main/main_page.dart +++ b/lib/views/home/main/main_page.dart @@ -301,6 +301,7 @@ class _ExploreLatestSlider extends StatelessWidget { @override Widget build(BuildContext context) { + debugPrint('🟢🟢🟢 _ExploreLatestSlider build called 🟢🟢🟢'); final List items = []; final List< ({String type, MainPageContentType? content, SwotItem? swotItem})> @@ -312,6 +313,12 @@ class _ExploreLatestSlider extends StatelessWidget { } if (list.contents.isNotEmpty) { final newestContent = list.contents.first; + + // Debug for monthly items + if (list.type == 'monthly') { + debugPrint('🔍 MONTHLY in carousel - ID: ${newestContent.id}, File: ${newestContent.file}, Link: ${newestContent.link}'); + } + items.add( Padding( padding: const EdgeInsets.symmetric(horizontal: 4), @@ -341,6 +348,12 @@ class _ExploreLatestSlider extends StatelessWidget { if (items.isEmpty) return const SizedBox.shrink(); + debugPrint('📱 Building Carousel3D with ${items.length} items'); + debugPrint('📱 ItemsData length: ${itemsData.length}'); + for (var i = 0; i < itemsData.length; i++) { + debugPrint(' [$i] Type: ${itemsData[i].type}, ID: ${itemsData[i].content?.id}'); + } + return Carousel3D( items: items, height: 220, @@ -350,11 +363,16 @@ class _ExploreLatestSlider extends StatelessWidget { onItemTap: (index) { final data = itemsData[index]; if (data.content != null) { + // استخراج ایمن فایل + final String? rawFile = data.content!.file; + final String fileString = (rawFile != null) ? rawFile.toString() : ''; + context.read().navigationHandler( data.type, data.content!.id, data.content!.link ?? '', description: data.content!.title, + file: fileString, // ارسال فایل به هندلر ); } }, diff --git a/lib/views/home/main/main_page_state.dart b/lib/views/home/main/main_page_state.dart index 6ee4ead..b97b23e 100644 --- a/lib/views/home/main/main_page_state.dart +++ b/lib/views/home/main/main_page_state.dart @@ -31,6 +31,8 @@ class MainPageState extends CoreProvier { List didvanVoiceList = []; TopBannerModel? topBanner; + // ... (سایر متدها بدون تغییر) ... + DidvanVoiceModel? _pickLatestVoice(List items) { if (items.isEmpty) return null; items.sort((a, b) { @@ -57,62 +59,35 @@ class MainPageState extends CoreProvier { } } - int getStoryStartIndex(List stories) { - final firstUnreadIndex = stories.indexWhere((story) => !story.isViewed); - return firstUnreadIndex != -1 ? firstUnreadIndex : 0; - } + // ... (سایر متدهای دریافت اطلاعات بدون تغییر) ... Future _getSwotItems() async { try { swotItems = await SwotService.fetchSwotItems(); } catch (e) { - // ignore: avoid_print print(e); } } 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 statusCode: ${service.statusCode}'); - if (service.statusCode == 200) { 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'); didvanPlusList = rawData .map((item) => DidvanPlusModel.fromJson(Map.from(item))) .toList(); didvanPlus = didvanPlusList.first; - debugPrint('✅ Didvan Plus loaded: ${didvanPlus?.id}'); - debugPrint('✅ Total items in list: ${didvanPlusList.length}'); - debugPrint('✅ List items:'); - for (var item in didvanPlusList) { - debugPrint(' - ${item.title} (${item.id})'); - } notifyListeners(); } else if (rawData is Map) { - debugPrint('🎬 Data is Map, using directly'); didvanPlus = DidvanPlusModel.fromJson(Map.from(rawData)); didvanPlusList = [didvanPlus!]; - debugPrint('✅ Didvan Plus loaded: ${didvanPlus?.id}'); notifyListeners(); - } else { - debugPrint('⚠️ Didvan Plus: Empty or unexpected data format'); } - } else { - debugPrint( - '⚠️ Didvan Plus: Request failed with status ${service.statusCode}'); } } catch (e) { debugPrint('❌ Failed to load Didvan Plus: $e'); @@ -120,50 +95,27 @@ class MainPageState extends CoreProvier { } Future _getDidvanVoice() async { - debugPrint('🎙️ Fetching Didvan Voice data...'); - debugPrint('🎙️ URL: ${RequestHelper.didvanVoice}'); - debugPrint('🎙️ Token: ${RequestService.token}'); try { final service = RequestService(RequestHelper.didvanVoice); await service.httpGet(); - debugPrint('🎙️ Didvan Voice statusCode: ${service.statusCode}'); - if (service.statusCode == 200) { final rawData = service.data('result'); - debugPrint('🎙️ Raw data type: ${rawData.runtimeType}'); - debugPrint( - '🎙️ Raw data length: ${rawData is List ? rawData.length : "N/A"}'); - if (rawData is List && rawData.isNotEmpty) { - debugPrint('🎙️ Data is List with ${rawData.length} items'); didvanVoiceList = rawData .map((e) => DidvanVoiceModel.fromJson( Map.from(e as Map))) .toList(); didvanVoice = _pickLatestVoice(List.from(didvanVoiceList)); - debugPrint( - '✅ Didvan Voice list loaded: ${didvanVoiceList.length} items'); - debugPrint('✅ Latest Didvan Voice id: ${didvanVoice?.id}'); - debugPrint( - '✅ All voice IDs: ${didvanVoiceList.map((v) => v.id).toList()}'); notifyListeners(); } else if (rawData is Map) { - debugPrint('🎙️ Data is Map, using directly'); didvanVoice = DidvanVoiceModel.fromJson(Map.from(rawData)); didvanVoiceList = [didvanVoice!]; - debugPrint('✅ Didvan Voice single item loaded: ${didvanVoice?.id}'); notifyListeners(); - } else { - debugPrint('⚠️ Didvan Voice: Empty or unexpected data format'); } - } else { - debugPrint( - '⚠️ Didvan Voice: Request failed with status ${service.statusCode}'); } - } catch (e, stackTrace) { + } catch (e) { debugPrint('❌ Failed to load Didvan Voice: $e'); - debugPrint('Stack trace: $stackTrace'); } } @@ -174,10 +126,7 @@ class MainPageState extends CoreProvier { 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'); @@ -187,7 +136,6 @@ class MainPageState extends CoreProvier { Future _fetchStories() async { try { stories = await StoryService.getStories(); - // print("Fetched ${stories.length} stories."); } catch (e) { stories = []; debugPrint("Could not fetch stories: $e"); @@ -207,7 +155,6 @@ class MainPageState extends CoreProvier { _getDidvanVoice(), _getTopBanner(), ]); - debugPrint("✅ All main page data loaded"); appState = AppState.idle; } catch (e) { debugPrint("❌ Main page init failed: $e"); @@ -226,11 +173,66 @@ class MainPageState extends CoreProvier { notifyListeners(); } + /// متد جدید: دریافت جزئیات ماهنامه برای پیدا کردن فایل PDF + Future _fetchMonthlyDetailsAndNavigate(int id, String? title) async { + final context = navigatorKey.currentContext!; + debugPrint('🔄 Fetching monthly details for ID: $id'); + + // می‌توانید اینجا یک لودینگ نمایش دهید + // showDialog(context: context, builder: (_) => const Center(child: CircularProgressIndicator())); + + try { + final url = '${RequestHelper.baseUrl}/monthly/$id'; + final service = RequestService(url); + await service.httpGet(); + + if (service.isSuccess) { + // بسته به ساختار جیسون، ممکن است دیتا داخل result باشد یا مستقیم + final data = service.result.containsKey('result') ? service.result['result'] : service.result; + + debugPrint('📥 Monthly details fetched: $data'); + + // استخراج فایل + final fetchedFile = data['file']; + final fetchedLink = data['link']; + + // اولویت ۱: لینک + if (fetchedLink != null && fetchedLink.toString().isNotEmpty) { + Navigator.of(context).pushNamed( + Routes.web, + arguments: fetchedLink.toString(), + ); + return; + } + + // اولویت ۲: فایل + if (fetchedFile != null && fetchedFile.toString().isNotEmpty) { + debugPrint('✅ Found file in details: $fetchedFile'); + Navigator.of(context).pushNamed( + Routes.pdfViewer, + arguments: { + 'pdfUrl': fetchedFile.toString(), + 'title': title ?? '', + }, + ); + return; + } + } + } catch (e) { + debugPrint('❌ Error fetching monthly details: $e'); + } + + // اولویت ۳: اگر هیچکدام پیدا نشد، برو به لیست + debugPrint('⚠️ No file/link found even after fetch, going to list'); + Navigator.of(context).pushNamed(Routes.monthlyList); + } + void navigationHandler( String type, int id, String? link, { String? description, + String? file, }) { link = link ?? ''; dynamic args; @@ -281,19 +283,47 @@ class MainPageState extends CoreProvier { } case 'monthly': { - if (link.isNotEmpty) { + debugPrint('=== Monthly Navigation Logic ==='); + debugPrint('ID: $id'); + debugPrint('Link: "$link"'); + debugPrint('File provided initially: "$file"'); + + // ۱. بررسی لینک + if (link!.isNotEmpty) { + debugPrint('Opening WebView for Monthly'); Navigator.of(navigatorKey.currentContext!).pushNamed( Routes.web, arguments: link, ); + return; } + + // ۲. بررسی فایل موجود + // چک میکنیم فایل نال نباشد، رشته "null" نباشد و خالی نباشد + if (file != null && file.isNotEmpty && file != 'null') { + debugPrint('Opening PDF Viewer for Monthly: $file'); + Navigator.of(navigatorKey.currentContext!).pushNamed( + Routes.pdfViewer, + arguments: { + 'pdfUrl': file, + 'title': description ?? '', + }, + ); + return; + } + + // ۳. اگر فایل نبود، اطلاعات را فچ کن (راهکار جدید) + debugPrint('File missing, trying to fetch details...'); + _fetchMonthlyDetailsAndNavigate(id, description); return; } } + + // هندل کردن سایر لینک‌های عمومی if (link == '') { return; } - if (link.startsWith('http')) { + if (link!.startsWith('http')) { AppInitializer.openWebLink( navigatorKey.currentContext!, '$link?accessToken=${RequestService.token}', @@ -301,6 +331,6 @@ class MainPageState extends CoreProvier { ); return; } - Navigator.of(navigatorKey.currentContext!).pushNamed(link, arguments: args); + Navigator.of(navigatorKey.currentContext!).pushNamed(link!, arguments: args); } -} +} \ No newline at end of file diff --git a/lib/views/home/main/widgets/didvan_voice_detail_card.dart b/lib/views/home/main/widgets/didvan_voice_detail_card.dart index 277391a..be3251f 100644 --- a/lib/views/home/main/widgets/didvan_voice_detail_card.dart +++ b/lib/views/home/main/widgets/didvan_voice_detail_card.dart @@ -3,6 +3,7 @@ import 'package:didvan/config/theme_data.dart'; 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/services/media/voice.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; @@ -23,7 +24,6 @@ class DidvanVoiceDetailCard extends StatefulWidget { class _DidvanVoiceDetailCardState extends State { late AudioPlayer _audioPlayer; - // ignore: unused_field bool _isInitialized = false; @override @@ -33,18 +33,21 @@ class _DidvanVoiceDetailCardState extends State { } void _initializeAudio() async { - _audioPlayer = AudioPlayer(); + _audioPlayer = VoiceService.audioPlayer; final audioUrl = '${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}'; debugPrint('🎙️ Didvan Voice Audio URL: $audioUrl'); try { + VoiceService.src = audioUrl; await _audioPlayer.setUrl(audioUrl); - setState(() { - _isInitialized = true; - }); - debugPrint('✅ Audio initialized successfully'); + + if (mounted) { + setState(() { + _isInitialized = true; + }); + } } catch (e) { debugPrint('❌ Audio initialization error: $e'); } @@ -52,7 +55,6 @@ class _DidvanVoiceDetailCardState extends State { @override void dispose() { - _audioPlayer.dispose(); super.dispose(); } @@ -144,8 +146,10 @@ class _DidvanVoiceDetailCardState extends State { overlayShape: RoundSliderOverlayShape( overlayRadius: 12), trackHeight: 4, - thumbColor: Color.fromARGB(255, 0, 126 , 167), - activeTrackColor: Color.fromARGB(255, 0, 126 , 167), + thumbColor: + Color.fromARGB(255, 0, 126, 167), + activeTrackColor: + Color.fromARGB(255, 0, 126, 167), inactiveTrackColor: Colors.grey, ), child: Slider( @@ -266,4 +270,4 @@ class _DidvanVoiceDetailCardState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/views/home/main/widgets/didvan_voice_section.dart b/lib/views/home/main/widgets/didvan_voice_section.dart index 270f255..f5837c0 100644 --- a/lib/views/home/main/widgets/didvan_voice_section.dart +++ b/lib/views/home/main/widgets/didvan_voice_section.dart @@ -1,6 +1,7 @@ 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/services/media/voice.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; @@ -19,8 +20,6 @@ class DidvanVoiceSection extends StatefulWidget { class _DidvanVoiceSectionState extends State { late AudioPlayer _audioPlayer; - // ignore: unused_field - bool _isInitialized = false; @override void initState() { @@ -28,28 +27,8 @@ class _DidvanVoiceSectionState extends State { _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(); + void _initializeAudio() { + _audioPlayer = VoiceService.audioPlayer; } String _formatDuration(Duration? duration) { @@ -62,6 +41,8 @@ class _DidvanVoiceSectionState extends State { @override Widget build(BuildContext context) { final imageUrl = '${RequestHelper.baseUrl}${widget.didvanVoice.image}'; + final audioUrl = + '${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}'; return Container( margin: const EdgeInsets.symmetric(horizontal: 16), @@ -92,148 +73,176 @@ class _DidvanVoiceSectionState extends State { ), 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; + child: StreamBuilder( + stream: _audioPlayer.playerStateStream, + builder: (context, snapshot) { + final isMyVoice = VoiceService.src == audioUrl; - if (processingState == ProcessingState.loading || - processingState == ProcessingState.buffering) { - return Container( + final playerState = snapshot.data; + final processingState = + playerState?.processingState ?? ProcessingState.idle; + final playing = isMyVoice && (playerState?.playing ?? false); + + final isLoading = isMyVoice && + (processingState == ProcessingState.loading || + processingState == ProcessingState.buffering); + + return 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: () async { + if (!isMyVoice) { + if (_audioPlayer.playing) + await _audioPlayer.stop(); + VoiceService.src = audioUrl; + await _audioPlayer.setUrl(audioUrl); + _audioPlayer.play(); + } else { + final newPosition = _audioPlayer.position + + const Duration(seconds: 10); + _audioPlayer.seek(newPosition); + } + }, + icon: + const Icon(Icons.forward_10, color: Colors.white), + iconSize: 28, + ), + if (isLoading) + Container( margin: const EdgeInsets.symmetric(horizontal: 8), width: 48, height: 48, child: const CircularProgressIndicator( + color: Colors.white), + ) + else + IconButton( + onPressed: () async { + if (playing) { + _audioPlayer.pause(); + } else { + if (!isMyVoice) { + // لود کردن صدای جدید اگر سورس فرق دارد + VoiceService.src = audioUrl; + await _audioPlayer.setUrl(audioUrl); + } + _audioPlayer.play(); + } + }, + icon: Icon( + playing + ? Icons.pause_circle_filled + : Icons.play_circle_filled, 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, - ), - ], ), + IconButton( + onPressed: () { + if (isMyVoice) { + 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, durationSnapshot) { + // اگر صدای من نیست، مدت زمان و پوزیشن را صفر نشان بده + final duration = isMyVoice + ? (durationSnapshot.data ?? Duration.zero) + : Duration.zero; + + return StreamBuilder( + stream: _audioPlayer.positionStream, + builder: (context, positionSnapshot) { + var position = isMyVoice + ? (positionSnapshot.data ?? Duration.zero) + : Duration.zero; + + if (position > duration) position = duration; + + return Column( + children: [ + 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: 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() > + 0 + ? duration.inMilliseconds.toDouble() + : 1.0, + onChanged: (value) { + if (isMyVoice) { + _audioPlayer.seek(Duration( + milliseconds: value.toInt())); + } + }, + ), + ), + ), + ], + ); + }, ); }, - ); - }, - ), - 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 8d8f7c1..7cb1d42 100644 --- a/lib/views/home/media/media_page.dart +++ b/lib/views/home/media/media_page.dart @@ -54,6 +54,7 @@ class _MediaPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, // ignore: deprecated_member_use backgroundColor: Theme.of(context).colorScheme.background, body: SafeArea( diff --git a/lib/views/home/media/video_details_page.dart b/lib/views/home/media/video_details_page.dart index 925797d..d1eed86 100644 --- a/lib/views/home/media/video_details_page.dart +++ b/lib/views/home/media/video_details_page.dart @@ -147,6 +147,7 @@ class _VideoDetailsPageState extends State builder: (context, state) { if (!state.isStudioLoaded) { return Scaffold( + resizeToAvoidBottomInset: true, body: Center( child: Image.asset( Assets.loadingAnimation, @@ -166,6 +167,7 @@ class _VideoDetailsPageState extends State return true; }, child: Scaffold( + resizeToAvoidBottomInset: true, backgroundColor: Theme.of(context).colorScheme.surface, appBar: PreferredSize( preferredSize: const Size.fromHeight(90.0), diff --git a/lib/views/home/new_statistic/new_statistic.dart b/lib/views/home/new_statistic/new_statistic.dart index fd93264..2247a6c 100644 --- a/lib/views/home/new_statistic/new_statistic.dart +++ b/lib/views/home/new_statistic/new_statistic.dart @@ -44,6 +44,7 @@ class _NewStatisticState extends State { const double headerHeight = 150.0; return Scaffold( + resizeToAvoidBottomInset: true, body: CustomScrollView( slivers: [ SliverPersistentHeader( diff --git a/lib/views/home/new_statistic/statistics_details/stat_cats_general_screen.dart b/lib/views/home/new_statistic/statistics_details/stat_cats_general_screen.dart index c6434db..48b880e 100644 --- a/lib/views/home/new_statistic/statistics_details/stat_cats_general_screen.dart +++ b/lib/views/home/new_statistic/statistics_details/stat_cats_general_screen.dart @@ -74,6 +74,7 @@ class _StatGeneralScreenState extends State { var source = context.read().source; return Scaffold( + resizeToAvoidBottomInset: true, appBar: AppBar( elevation: 0.0, scrolledUnderElevation: 0.0, diff --git a/lib/views/home/new_statistic/stock/new_stock.dart b/lib/views/home/new_statistic/stock/new_stock.dart index 054ad40..bfb9b66 100644 --- a/lib/views/home/new_statistic/stock/new_stock.dart +++ b/lib/views/home/new_statistic/stock/new_stock.dart @@ -75,6 +75,7 @@ class _NewStockState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.surface, elevation: 0.0, diff --git a/lib/views/news/news_details/news_details.dart b/lib/views/news/news_details/news_details.dart index 6329b49..21cf6d0 100644 --- a/lib/views/news/news_details/news_details.dart +++ b/lib/views/news/news_details/news_details.dart @@ -41,6 +41,7 @@ class _NewsDetailsState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, body: Consumer( builder: (context, state, child) => StateHandler( onRetry: () => state.getNewsDetails(widget.pageData['id']), diff --git a/lib/views/onboarding/onboarding_page.dart b/lib/views/onboarding/onboarding_page.dart index 3442fda..30057a3 100644 --- a/lib/views/onboarding/onboarding_page.dart +++ b/lib/views/onboarding/onboarding_page.dart @@ -94,6 +94,7 @@ class _OnboardingPageState extends State { final colorScheme = theme.colorScheme; return Scaffold( + resizeToAvoidBottomInset: true, backgroundColor: colorScheme.background, body: SafeArea( child: Stack( diff --git a/lib/views/pdf_viewer/pdf_viewer_page.dart b/lib/views/pdf_viewer/pdf_viewer_page.dart index 2abda31..e868715 100644 --- a/lib/views/pdf_viewer/pdf_viewer_page.dart +++ b/lib/views/pdf_viewer/pdf_viewer_page.dart @@ -40,6 +40,7 @@ class _PdfViewerPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, appBar: AppBar( title: DidvanText( widget.title, diff --git a/lib/views/podcasts/studio_details/studio_details.mobile.dart b/lib/views/podcasts/studio_details/studio_details.mobile.dart index 782d905..4b6dd43 100644 --- a/lib/views/podcasts/studio_details/studio_details.mobile.dart +++ b/lib/views/podcasts/studio_details/studio_details.mobile.dart @@ -288,6 +288,7 @@ class _StudioDetailsState extends State builder: (context, state) { if (!state.isStudioLoaded) { return Scaffold( + resizeToAvoidBottomInset: true, body: Center( child: Image.asset( Assets.loadingAnimation, @@ -307,6 +308,7 @@ class _StudioDetailsState extends State return true; }, child: Scaffold( + resizeToAvoidBottomInset: true, backgroundColor: Theme.of(context).colorScheme.surface, appBar: PreferredSize( preferredSize: const Size.fromHeight(90.0), diff --git a/lib/views/podcasts/studio_details/studio_details.web.dart b/lib/views/podcasts/studio_details/studio_details.web.dart index 82c49e0..5904278 100644 --- a/lib/views/podcasts/studio_details/studio_details.web.dart +++ b/lib/views/podcasts/studio_details/studio_details.web.dart @@ -236,6 +236,7 @@ class _StudioDetailsState extends State return true; }, child: Scaffold( + resizeToAvoidBottomInset: true, backgroundColor: Theme.of(context).colorScheme.surface, appBar: PreferredSize( preferredSize: const Size.fromHeight(90.0), diff --git a/lib/views/profile/change_password/change_password.dart b/lib/views/profile/change_password/change_password.dart index 6493af3..9ed4eda 100644 --- a/lib/views/profile/change_password/change_password.dart +++ b/lib/views/profile/change_password/change_password.dart @@ -29,6 +29,7 @@ class _ChangePasswordPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, body: Consumer( // ignore: deprecated_member_use builder: (context, state, child) => WillPopScope( diff --git a/lib/views/radar/radar_details/radar_details.dart b/lib/views/radar/radar_details/radar_details.dart index 49e2d9c..c181d25 100644 --- a/lib/views/radar/radar_details/radar_details.dart +++ b/lib/views/radar/radar_details/radar_details.dart @@ -42,6 +42,7 @@ class _RadarDetailsState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, body: Consumer( builder: (context, state, child) => StateHandler( onRetry: () => state.getRadarDetails(widget.pageData['id']), diff --git a/lib/views/splash/splash.dart b/lib/views/splash/splash.dart index ee7e9ae..20335ec 100644 --- a/lib/views/splash/splash.dart +++ b/lib/views/splash/splash.dart @@ -9,6 +9,7 @@ import 'package:didvan/providers/server_data.dart'; import 'package:didvan/providers/theme.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; +import 'package:didvan/services/app_home_widget/home_widget_repository.dart'; import 'package:didvan/services/app_initalizer.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -39,6 +40,13 @@ class _SplashState extends State with SingleTickerProviderStateMixin { void initState() { super.initState(); + if (kIsWeb) { + final loader = html.document.getElementById('loading_indicator'); + if (loader != null) { + loader.remove(); + } + } + _setupAnimations(); WidgetsBinding.instance.addPostFrameCallback((_) { _startInitialization(); @@ -87,6 +95,7 @@ class _SplashState extends State with SingleTickerProviderStateMixin { systemNavigationBarColor: colorScheme.background, ), child: Scaffold( + resizeToAvoidBottomInset: true, backgroundColor: colorScheme.background, body: SafeArea( child: Center( @@ -279,7 +288,7 @@ class _SplashState extends State with SingleTickerProviderStateMixin { } } - void _navigateToNextScreen(String? token) { + void _navigateToNextScreen(String? token) async { if (!mounted) return; String extractedPath = initialURI?.path == '/' @@ -300,6 +309,22 @@ class _SplashState extends State with SingleTickerProviderStateMixin { routeArguments = false; } + if (token != null && HomeWidgetRepository.data != null) { + Navigator.of(context).pushReplacementNamed( + Routes.home, + arguments: {'showDialogs': false}, + ); + + await HomeWidgetRepository.decideWhereToGoNotif(); + + if (HomeWidgetRepository.data != null) { + await StorageService.delete( + key: + 'notification${AppInitializer.createNotificationId(HomeWidgetRepository.data!)}'); + } + return; + } + Navigator.of(context).pushReplacementNamed( destinationRoute, arguments: routeArguments, diff --git a/lib/views/story_viewer/story_viewer_page.dart b/lib/views/story_viewer/story_viewer_page.dart index 9883d51..b828d46 100644 --- a/lib/views/story_viewer/story_viewer_page.dart +++ b/lib/views/story_viewer/story_viewer_page.dart @@ -47,6 +47,7 @@ class _StoryViewerPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, body: Directionality( textDirection: TextDirection.ltr, child: PageView.builder( diff --git a/lib/views/webview/web_view.dart b/lib/views/webview/web_view.dart index e1da64d..a1bbb29 100644 --- a/lib/views/webview/web_view.dart +++ b/lib/views/webview/web_view.dart @@ -80,6 +80,7 @@ class _WebViewState extends State { } }, child: Scaffold( + resizeToAvoidBottomInset: true, appBar: AppBar( title: const DidvanText( 'بازگشت', diff --git a/lib/views/widgets/ai_voice_chat_dialog.dart b/lib/views/widgets/ai_voice_chat_dialog.dart index 309a077..c8b4f13 100644 --- a/lib/views/widgets/ai_voice_chat_dialog.dart +++ b/lib/views/widgets/ai_voice_chat_dialog.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'dart:ui'; import 'dart:math' as math; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:record/record.dart'; import 'package:flutter_sound/flutter_sound.dart' hide Codec; @@ -34,20 +35,22 @@ class _AiVoiceChatDialogState extends State static const int inputSampleRate = 16000; static const int geminiSampleRate = 24000; - // --- VAD Settings --- + // --- VAD Settings --- static const double vadThreshold = 0.05; static const double speechThreshold = 0.1; // --- روش ساده و مطمئن: آستانه ثابت --- // صدای AI از اسپیکر معمولاً RMS حدود 0.05-0.12 دارد // صدای کاربر مستقیم به میکروفون معمولاً RMS بالای 0.15 دارد - static const double userInterruptThreshold = 0.25; // آستانه پایین‌تر - حساسیت بیشتر + static const double userInterruptThreshold = + 0.25; // آستانه پایین‌تر - حساسیت بیشتر static const int ignoreInitialMs = 800; // 800ms اول نادیده گرفته شود - static const int sustainedChunksRequired = 4; // 4 chunk متوالی برای تایید اینتراپت - + static const int sustainedChunksRequired = + 4; // 4 chunk متوالی برای تایید اینتراپت + int _interruptChunkCount = 0; // شمارنده chunk‌های متوالی با صدای بالا DateTime? _aiPlaybackStartTime; // زمان شروع پخش AI - + static const int vadSustainMs = 150; static const int silenceTimeoutMs = 1000; @@ -111,58 +114,62 @@ class _AiVoiceChatDialogState extends State vsync: this, duration: const Duration(milliseconds: 50), )..addListener(() { - if (_isRecording || _isAiSpeaking) { - setState(() { - final random = math.Random(); + if (_isRecording || _isAiSpeaking) { + setState(() { + final random = math.Random(); + for (int i = 0; i < _audioWaveHeights.length; i++) { + double target = 0.1 + (random.nextDouble() * 0.4); + if (_isAiSpeaking) target *= 1.5; + if (_isSpeechActive) target *= 2.0; + _audioWaveHeights[i] = + _audioWaveHeights[i] + (target - _audioWaveHeights[i]) * 0.2; + } + }); + } else { for (int i = 0; i < _audioWaveHeights.length; i++) { - double target = 0.1 + (random.nextDouble() * 0.4); - if (_isAiSpeaking) target *= 1.5; - if (_isSpeechActive) target *= 2.0; - _audioWaveHeights[i] = - _audioWaveHeights[i] + (target - _audioWaveHeights[i]) * 0.2; + _audioWaveHeights[i] = _audioWaveHeights[i] * 0.9; } - }); - } else { - for (int i = 0; i < _audioWaveHeights.length; i++) { - _audioWaveHeights[i] = _audioWaveHeights[i] * 0.9; } - } - }); + }); } Future _initAudio() async { try { // تنظیم AudioSession برای پخش از طریق MEDIA نه CALL final session = await AudioSession.instance; - await session.configure(AudioSessionConfiguration( + await session.configure(AudioSessionConfiguration( // 1. حالت PlayAndRecord برای استفاده همزمان avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, - + // 2. تنظیمات کلیدی: // defaultToSpeaker: صدا حتما از اسپیکر بیاید (نه گوشی) // allowBluetooth: اجازه استفاده از هندزفری بلوتوث - avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | - AVAudioSessionCategoryOptions.defaultToSpeaker, - + avAudioSessionCategoryOptions: + AVAudioSessionCategoryOptions.allowBluetooth | + AVAudioSessionCategoryOptions.defaultToSpeaker, + // 3. حالت VoiceChat: این حالت پردازشگر سیگنال (DSP) موبایل را برای حذف اکو فعال می‌کند avAudioSessionMode: AVAudioSessionMode.voiceChat, - + androidAudioAttributes: const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, flags: AndroidAudioFlags.none, - usage: AndroidAudioUsage.voiceCommunication, // در اندروید هم حالت مکالمه باشد + usage: AndroidAudioUsage + .voiceCommunication, // در اندروید هم حالت مکالمه باشد ), androidAudioFocusGainType: AndroidAudioFocusGainType.gain, )); await _audioPlayer.openPlayer(); - await _audioPlayer.setSubscriptionDuration(const Duration(milliseconds: 10)); + await _audioPlayer + .setSubscriptionDuration(const Duration(milliseconds: 10)); _isPlayerInitialized = true; debugPrint('✅ Audio player initialized with VOICE CHAT + SPEAKER'); } catch (e) { debugPrint('❌ Error initializing audio player: $e'); } } + // --------------------------------------------------------------------------- // Helper: Stop AI Playback (Internal Logic Only) // --------------------------------------------------------------------------- @@ -274,7 +281,7 @@ class _AiVoiceChatDialogState extends State sampleRate: 16000, numChannels: 1, // روشن بودن این گزینه‌ها حیاتی است - echoCancel: true, + echoCancel: true, noiseSuppress: true, autoGain: false, ), @@ -341,20 +348,22 @@ class _AiVoiceChatDialogState extends State // ۲. بررسی اینتراپت - روش ساده و مطمئن if (_ignoreAudioDuringAIPlayback) { if (_aiPlaybackStartTime != null) { - final elapsed = DateTime.now().difference(_aiPlaybackStartTime!).inMilliseconds; - + final elapsed = + DateTime.now().difference(_aiPlaybackStartTime!).inMilliseconds; + // در 500ms اول، همه چیز را نادیده بگیر (زمان برای استقرار صدا) if (elapsed < ignoreInitialMs) { return; } - + // اگر صدا از آستانه بالاتر بود if (rms > userInterruptThreshold) { _interruptChunkCount++; - + // اگر چند chunk متوالی صدای بالا داشتیم = کاربر واقعاً صحبت می‌کند if (_interruptChunkCount >= sustainedChunksRequired) { - debugPrint('🧯 User speaking detected! RMS: ${rms.toStringAsFixed(3)}, Chunks: $_interruptChunkCount - Interrupting AI'); + debugPrint( + '🧯 User speaking detected! RMS: ${rms.toStringAsFixed(3)}, Chunks: $_interruptChunkCount - Interrupting AI'); _wasStoppedByUser = true; _interruptChunkCount = 0; _flushAiBuffers(); @@ -441,49 +450,51 @@ class _AiVoiceChatDialogState extends State // --------------------------------------------------------------------------- // Receive and Play (AI Output) // --------------------------------------------------------------------------- - + /// کاهش حجم صدای PCM16 به صورت مستقیم Uint8List _reduceAudioVolume(Uint8List audioData, double volumeFactor) { final result = Uint8List(audioData.length); - - debugPrint('🔉 Reducing volume: ${audioData.length} bytes, factor: $volumeFactor'); - + + debugPrint( + '🔉 Reducing volume: ${audioData.length} bytes, factor: $volumeFactor'); + int maxOriginal = 0; int maxReduced = 0; - + for (int i = 0; i < audioData.length - 1; i += 2) { // خواندن sample به صورت Little Endian 16-bit signed integer int low = audioData[i]; int high = audioData[i + 1]; int sample = (high << 8) | low; - + // تبدیل به signed if (sample >= 32768) { sample -= 65536; } - + if (sample.abs() > maxOriginal) maxOriginal = sample.abs(); - + // کاهش شدید حجم - ضرب در ضریب خیلی کوچک double reduced = sample * volumeFactor; int newSample = reduced.round(); - + // محدود کردن newSample = newSample.clamp(-32768, 32767); - + if (newSample.abs() > maxReduced) maxReduced = newSample.abs(); - + // تبدیل به unsigned if (newSample < 0) { newSample += 65536; } - + // نوشتن Little Endian result[i] = newSample & 0xFF; result[i + 1] = (newSample >> 8) & 0xFF; } - - debugPrint('✅ Volume reduced - Max original: $maxOriginal, Max reduced: $maxReduced'); + + debugPrint( + '✅ Volume reduced - Max original: $maxOriginal, Max reduced: $maxReduced'); return result; } @@ -491,7 +502,7 @@ class _AiVoiceChatDialogState extends State try { _ignoreAudioDuringAIPlayback = true; _aiPlaybackStartTime = DateTime.now(); // ثبت زمان شروع دریافت - + String base64String; if (data is String) { base64String = data; @@ -529,6 +540,7 @@ class _AiVoiceChatDialogState extends State } } + // متد اصلی پخش صدا با منطق جداگانه برای وب و موبایل Future _playAccumulatedAudio() async { if (_isSpeechActive) { debugPrint('⚠️ User is speaking, cancelling AI playback'); @@ -562,48 +574,35 @@ class _AiVoiceChatDialogState extends State setState(() => _statusText = '🔊 AI در حال صحبت...'); } - final tempDir = await getTemporaryDirectory(); - final tempFile = File( - '${tempDir.path}/ai_response_${DateTime.now().millisecondsSinceEpoch}.pcm'); - await tempFile.writeAsBytes(reducedAudioData); + // --- تغییرات برای وب --- + if (kIsWeb) { + // در وب به جای ذخیره فایل، مستقیم از بافر پخش می‌کنیم + await _audioPlayer.startPlayer( + fromDataBuffer: reducedAudioData, + codec: fsp.Codec.pcm16, + sampleRate: geminiSampleRate, + numChannels: 1, + whenFinished: () { + _onPlaybackFinished(); + }, + ); + } else { + // در موبایل (کد قبلی): ذخیره در فایل موقت و پخش + final tempDir = await getTemporaryDirectory(); + final tempFile = File( + '${tempDir.path}/ai_response_${DateTime.now().millisecondsSinceEpoch}.pcm'); + await tempFile.writeAsBytes(reducedAudioData); - await _audioPlayer.startPlayer( - fromURI: tempFile.path, - codec: fsp.Codec.pcm16, - sampleRate: geminiSampleRate, - numChannels: 1, - whenFinished: () { - debugPrint('✅ Playback finished'); - - if (!_wasStoppedByUser) { - _ignoreAudioDuringAIPlayback = false; - _aiPlaybackStartTime = null; // ریست زمان پخش - } - - if (!_wasStoppedByUser) { - try { - if (tempFile.existsSync()) tempFile.deleteSync(); - } catch (e) { - /* ignore */ - } - - if (mounted) { - setState(() { - _isAiSpeaking = false; - _isPlayingFromQueue = false; - _statusText = _isRecording ? '👂 در حال گوش دادن...' : 'آماده'; - }); - } - } else { - _wasStoppedByUser = false; - try { - if (tempFile.existsSync()) tempFile.deleteSync(); - } catch (e) { - /* ignore */ - } - } - }, - ); + await _audioPlayer.startPlayer( + fromURI: tempFile.path, + codec: fsp.Codec.pcm16, + sampleRate: geminiSampleRate, + numChannels: 1, + whenFinished: () { + _onPlaybackFinished(tempFile: tempFile); + }, + ); + } } catch (e) { debugPrint('❌ Playback Error: $e'); _isPlayingFromQueue = false; @@ -616,6 +615,37 @@ class _AiVoiceChatDialogState extends State } } + // متد کمکی برای پایان پخش و پاکسازی + void _onPlaybackFinished({File? tempFile}) { + debugPrint('✅ Playback finished'); + + if (!_wasStoppedByUser) { + _ignoreAudioDuringAIPlayback = false; + _aiPlaybackStartTime = null; // ریست زمان پخش + } + + // پاک کردن فایل فقط در موبایل (چون در وب فایلی نیست) + if (tempFile != null) { + try { + if (tempFile.existsSync()) tempFile.deleteSync(); + } catch (e) { + /* ignore */ + } + } + + if (!_wasStoppedByUser) { + if (mounted) { + setState(() { + _isAiSpeaking = false; + _isPlayingFromQueue = false; + _statusText = _isRecording ? '👂 در حال گوش دادن...' : 'آماده'; + }); + } + } else { + _wasStoppedByUser = false; + } + } + @override void dispose() { _stopAiPlayback(); @@ -701,7 +731,6 @@ class _AiVoiceChatDialogState extends State child: Container(color: Colors.transparent), ), ), - SafeArea( child: Column( children: [ @@ -787,7 +816,6 @@ class _AiVoiceChatDialogState extends State ); }, ), - AnimatedBuilder( animation: _waveController, builder: (context, child) { @@ -800,7 +828,6 @@ class _AiVoiceChatDialogState extends State ); }, ), - AnimatedBuilder( animation: _orbController, builder: (context, child) { @@ -969,4 +996,4 @@ class RipplePainter extends CustomPainter { @override bool shouldRepaint(covariant RipplePainter oldDelegate) => true; -} \ No newline at end of file +} diff --git a/lib/views/widgets/carousel_3d.dart b/lib/views/widgets/carousel_3d.dart index 65b52d3..bfbbb40 100644 --- a/lib/views/widgets/carousel_3d.dart +++ b/lib/views/widgets/carousel_3d.dart @@ -199,12 +199,16 @@ class _Carousel3DState extends State alignment: Alignment.center, child: Opacity( opacity: opacity, - child: IgnorePointer( - ignoring: relativePos != 0, - child: GestureDetector( - onTap: relativePos == 0 - ? () => widget.onItemTap?.call(index) - : null, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + debugPrint('🎯 Item tapped - Index: $index, RelativePos: $relativePos, IsCentered: ${relativePos == 0}'); + if (relativePos == 0) { + widget.onItemTap?.call(index); + } + }, + child: IgnorePointer( + ignoring: false, child: Container( width: screenWidth * 0.7, decoration: BoxDecoration( diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart index 2254e47..df1496a 100644 --- a/lib/views/widgets/didvan/page_view.dart +++ b/lib/views/widgets/didvan/page_view.dart @@ -953,7 +953,105 @@ class _DidvanPageViewState extends State { Widget _contentBuilder(dynamic item, int index) { final content = item.contents[index]; + final text = content.text?.toLowerCase() ?? ''; + + final bool isSourcesSection = (index == item.contents.length - 1) && + (text.contains(']*href="([^"]*)"[^>]*>(.*?)<\/a>', + caseSensitive: false, + dotAll: true); + final matches = linkRegExp.allMatches(content.text!).toList(); + + String title = ""; + int firstLinkIndex = content.text!.toLowerCase().indexOf(' 0) { + String preText = content.text!.substring(0, firstLinkIndex); + title = preText.replaceAll(RegExp(r'<[^>]*>'), ''); + title = + title.replaceAll(RegExp(r' ', caseSensitive: false), ' '); + title = title.trim(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (title.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 8.0, top: 8.0), + child: DidvanText( + title, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.right, + ), + ), + Directionality( + textDirection: TextDirection.ltr, + child: Wrap( + spacing: 6.0, + runSpacing: 8.0, + alignment: WrapAlignment.start, + children: List.generate(matches.length, (i) { + final match = matches[i]; + final href = match.group(1) ?? ''; + final linkText = + match.group(2)?.replaceAll(RegExp(r'<[^>]*>'), '') ?? ''; + + return Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: Colors.transparent, + border: Border.all( + color: const Color.fromARGB(255, 0, 126, 167) + .withOpacity(0.5), + width: 1, + ), + borderRadius: BorderRadius.circular(12), + ), + child: InkWell( + onTap: () { + if (href.isNotEmpty) { + AppInitializer.openWebLink(context, href, + mode: LaunchMode.inAppWebView); + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${i + 1}- ", + style: const TextStyle( + color: Color.fromARGB(255, 0, 126, 167), + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + Text( + linkText, + style: const TextStyle( + color: Color.fromARGB(255, 0, 126, 167), + fontSize: 12, + decoration: TextDecoration.none, + ), + ), + ], + ), + ), + ); + }), + ), + ), + ], + ); + } + return Html( data: content.text, onAnchorTap: (href, _, element) { diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index 7e9067a..a5657d7 100644 --- a/lib/views/widgets/didvan/scaffold.dart +++ b/lib/views/widgets/didvan/scaffold.dart @@ -58,7 +58,10 @@ class _DidvanScaffoldState extends State { final double statusBarHeight = MediaQuery.of(context).padding.top; final double systemNavigationBarHeight = MediaQuery.of(context).padding.bottom; + final double keyboardHeight = MediaQuery.of(context).viewInsets.bottom; + return Scaffold( + resizeToAvoidBottomInset: true, backgroundColor: widget.backgroundColor, floatingActionButton: widget.floatingActionButton, body: Padding( @@ -68,7 +71,7 @@ class _DidvanScaffoldState extends State { child: SizedBox( height: MediaQuery.of(context).size.height - statusBarHeight - - systemNavigationBarHeight, + (systemNavigationBarHeight - keyboardHeight), child: Stack( children: [ CustomScrollView( diff --git a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig index 90041d8..1d33b86 100644 --- a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -4,7 +4,7 @@ FLUTTER_APPLICATION_PATH=C:\Flutter Projects\didvan-app\didvan-app COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_BUILD_DIR=build FLUTTER_BUILD_NAME=5.0.0 -FLUTTER_BUILD_NUMBER=6000 +FLUTTER_BUILD_NUMBER=7002 DART_OBFUSCATION=false TRACK_WIDGET_CREATION=true TREE_SHAKE_ICONS=false diff --git a/macos/Flutter/ephemeral/flutter_export_environment.sh b/macos/Flutter/ephemeral/flutter_export_environment.sh index 99e93e9..d268b7d 100644 --- a/macos/Flutter/ephemeral/flutter_export_environment.sh +++ b/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -5,7 +5,7 @@ export "FLUTTER_APPLICATION_PATH=C:\Flutter Projects\didvan-app\didvan-app" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_BUILD_DIR=build" export "FLUTTER_BUILD_NAME=5.0.0" -export "FLUTTER_BUILD_NUMBER=6000" +export "FLUTTER_BUILD_NUMBER=7002" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" diff --git a/pubspec.yaml b/pubspec.yaml index 5668bb7..4666831 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 5.0.0+6000 +version: 5.0.0+7005 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/web/index.html b/web/index.html index aebfb11..5d3a7cc 100644 --- a/web/index.html +++ b/web/index.html @@ -1,32 +1,17 @@ - - - - + \ No newline at end of file