fixed some bugs for v5

This commit is contained in:
Mr.Jebelli 2025-12-22 09:44:06 +03:30
parent e9032d1022
commit d54d466e3d
42 changed files with 956 additions and 578 deletions

View File

@ -8,10 +8,9 @@ def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader -> localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader) localProperties.load(reader)
}
} }
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
@ -22,11 +21,6 @@ def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) { if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android { android {
namespace "com.didvan.didvanapp" namespace "com.didvan.didvanapp"
@ -34,27 +28,22 @@ android {
ndkVersion "28.2.13676358" ndkVersion "28.2.13676358"
compileOptions { compileOptions {
// تغییر 1.8 به VERSION_17
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
} }
kotlinOptions { kotlinOptions {
// تغییر '1.8' به '17'
jvmTarget = '17' jvmTarget = '17'
} }
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.didvan.didvanapp" applicationId "com.didvan.didvanapp"
minSdkVersion 24 minSdkVersion 24
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 34 targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
@ -62,12 +51,14 @@ android {
signingConfigs { signingConfigs {
release { release {
storeFile file("keystore.jks") // فایل keystore.jks باید داخل پوشه android/app باشد
storeFile file("keystore.jks")
storePassword "12799721" storePassword "12799721"
keyAlias "upload" keyAlias "upload"
keyPassword "12799721" keyPassword "12799721"
} }
} }
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release signingConfig signingConfigs.release
@ -81,24 +72,18 @@ android {
disable 'InvalidPackage' disable 'InvalidPackage'
checkReleaseBuilds false checkReleaseBuilds false
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
splits { splits {
//configure apks based on ABI
abi { abi {
enable true enable true
reset() reset()
include "x86", "x86_64", "armeabi-v7a", "arm64-v8a" include "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
universalApk true universalApk true
} }
// density {
// enable true
// reset()
// include "mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
// }
} }
} }
@ -107,7 +92,6 @@ flutter {
} }
dependencies { dependencies {
// implementation platform('com.google.firebase:firebase-bom:29.1.0')
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.squareup.picasso:picasso:2.8' implementation 'com.squareup.picasso:picasso:2.8'
implementation "androidx.room:room-runtime:2.2.5" implementation "androidx.room:room-runtime:2.2.5"
@ -115,4 +99,4 @@ dependencies {
implementation "androidx.sqlite:sqlite:2.1.0" implementation "androidx.sqlite:sqlite:2.1.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
} }

View File

@ -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/ai/history_ai_chat_state.dart';
import 'package:didvan/views/podcasts/podcasts_state.dart'; import 'package:didvan/views/podcasts/podcasts_state.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart'; import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:universal_html/html.dart' as html;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Uri? initialURI; Uri? initialURI;
@ -50,6 +51,13 @@ void main() async {
() async { () async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (kIsWeb) {
final loader = html.document.getElementById('loading_indicator');
if (loader != null) {
loader.remove();
}
}
try { try {
if (!kIsWeb) { if (!kIsWeb) {
// ignore: deprecated_member_use // ignore: deprecated_member_use
@ -214,34 +222,36 @@ class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
), ),
); );
return Container( return MaterialApp(
color: Theme.of(context).colorScheme.surface, scrollBehavior: MyCustomScrollBehavior(),
child: SafeArea( navigatorKey: navigatorKey,
child: MaterialApp( debugShowCheckedModeBanner: false,
scrollBehavior: MyCustomScrollBehavior(), title: 'Didvan',
navigatorKey: navigatorKey, theme: lightTheme,
debugShowCheckedModeBanner: false, darkTheme: darkTheme,
title: 'Didvan', color: lightTheme.primaryColor,
theme: lightTheme, themeMode: themeProvider.themeMode,
darkTheme: darkTheme, onGenerateRoute: (settings) =>
color: lightTheme.primaryColor, RouteGenerator.generateRoute(settings),
themeMode: themeProvider.themeMode, builder: (context, child) {
onGenerateRoute: (settings) => return BotToastInit()(
RouteGenerator.generateRoute(settings), context,
builder: BotToastInit(), SafeArea(
navigatorObservers: [BotToastNavigatorObserver()], child: child!,
initialRoute: "/", ),
localizationsDelegates: const [ );
GlobalCupertinoLocalizations.delegate, },
GlobalMaterialLocalizations.delegate, navigatorObservers: [BotToastNavigatorObserver()],
GlobalWidgetsLocalizations.delegate, initialRoute: "/",
], localizationsDelegates: const [
supportedLocales: const [ GlobalCupertinoLocalizations.delegate,
Locale("fa", "IR"), GlobalMaterialLocalizations.delegate,
], GlobalWidgetsLocalizations.delegate,
locale: const Locale("fa", "IR"), ],
), supportedLocales: const [
), Locale("fa", "IR"),
],
locale: const Locale("fa", "IR"),
); );
}, },
), ),

View File

@ -1,6 +1,8 @@
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
// ignore_for_file: avoid_print // ignore_for_file: avoid_print
import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:didvan/main.dart'; import 'package:didvan/main.dart';
import 'package:didvan/models/enums.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/notification/firebase_api.dart';
import 'package:didvan/services/storage/storage.dart'; import 'package:didvan/services/storage/storage.dart';
import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/utils/action_sheet.dart';
import 'package:flutter/foundation.dart';
class UserProvider extends CoreProvier { class UserProvider extends CoreProvier {
late User user; late User user;
@ -106,7 +109,6 @@ class UserProvider extends CoreProvier {
final RequestService service = RequestService(RequestHelper.userInfo); final RequestService service = RequestService(RequestHelper.userInfo);
await service.httpGet(); await service.httpGet();
// اگر توکن نامعتبر است (401)، فالس برمیگردانیم تا توکن پاک شود
if (service.statusCode == 401) { if (service.statusCode == 401) {
print("UserProvider: getUserInfo failed - Unauthorized (401)."); print("UserProvider: getUserInfo failed - Unauthorized (401).");
isAuthenticated = false; isAuthenticated = false;
@ -149,10 +151,9 @@ class UserProvider extends CoreProvier {
"UserProvider: getUserInfo failed. Status: ${service.statusCode}, Error: ${service.errorMessage}"); "UserProvider: getUserInfo failed. Status: ${service.statusCode}, Error: ${service.errorMessage}");
isAuthenticated = false; isAuthenticated = false;
// اصلاح مهم: اگر خطا 401 نیست (مثلاً مشکل سرور یا اینترنت)، Exception پرتاب میکنیم
// تا در Splash وارد بخش catch شود و توکن پاک نشود.
if (service.statusCode != 401) { if (service.statusCode != 401) {
throw Exception("Server Error or Connection Issue: ${service.statusCode}"); throw Exception(
"Server Error or Connection Issue: ${service.statusCode}");
} }
return false; return false;
@ -160,9 +161,20 @@ class UserProvider extends CoreProvier {
Future<void> _registerFirebaseToken() async { Future<void> _registerFirebaseToken() async {
if (FirebaseApi.fcmToken != null) { 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: { final service = RequestService(RequestHelper.firebaseToken, body: {
'token': FirebaseApi.fcmToken, 'token': FirebaseApi.fcmToken,
'platform': platform,
}); });
await service.put(); await service.put();
} }
} }
@ -329,7 +341,7 @@ class UserProvider extends CoreProvier {
'new_password': newPassword, 'new_password': newPassword,
}, },
); );
await service.post(); await service.post();
if (service.isSuccess) { if (service.isSuccess) {
@ -342,4 +354,4 @@ class UserProvider extends CoreProvier {
return false; return false;
} }
} }
} }

View File

@ -147,156 +147,266 @@ class HomeWidgetRepository {
} }
} }
await HomeWidget.saveWidgetData("uri", ""); await HomeWidget.saveWidgetData("uri", "");
data = null;
return; return;
} }
static NotificationMessage? data; static NotificationMessage? data;
static Future<void> decideWhereToGoNotif() async { static Future<void> 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) { if (kDebugMode) {
print("=== NAVIGATION DECISION ==="); print("=== NAVIGATION DECISION ===");
print("Notification Data: ${data?.toJson()}"); print("Notification Data: ${localData.toJson()}");
print("Type: ${data?.type}"); print("Type: ${localData.type}");
print("ID: ${data?.id}"); print("ID: ${localData.id}");
print("Link: ${data?.link}"); print("Link: ${localData.link}");
print("Notification Type: ${data?.notificationType}"); print("Notification Type: ${localData.notificationType}");
} }
String route = ""; String route = "";
dynamic args; dynamic args;
bool openComments = data!.notificationType.toString() == "2"; bool openComments = localData.notificationType.toString() == "2";
if (data.link.toString().isEmpty || data.link.toString() == "null") { if (localData.link.toString().isEmpty ||
switch (data.type!) { localData.link.toString() == "null") {
case "infography": if (localData.type == null || localData.type!.isEmpty) {
route = Routes.infography; if (kDebugMode) {
args = { print("=== NAVIGATION ABORTED ===");
'id': int.parse(data.id.toString()), print("Reason: Notification type is null or empty");
'args': const InfographyRequestArgs(page: 0), print("Defaulting to home route");
'hasUnmarkConfirmation': false, print("===========================");
'goToComment': openComments }
}; route = Routes.home;
break; } else {
case "news": switch (localData.type!) {
route = Routes.newsDetails; case "infography":
args = { if (localData.id == null || localData.id.toString().isEmpty) {
'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 (kDebugMode) { 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( AppInitializer.openWebLink(
navigatorKey.currentContext!, navigatorKey.currentContext!,
url, localData.link!,
mode: LaunchMode.inAppWebView, 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 { } else {
route = Routes.home; route = Routes.home;
if (kDebugMode) { 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; route = Routes.home;
if (kDebugMode) { if (kDebugMode) {
print("No ID or link available for ${data.type} - navigating to home"); print(
"Unknown notification type: ${localData.type} - navigating to home");
} }
} break;
break; }
default:
route = Routes.home;
if (kDebugMode) {
print("Unknown notification type: ${data.type} - navigating to home");
}
break;
} }
} else { } else {
if (kDebugMode) { if (kDebugMode) {
print("External link detected: ${data.link}"); print("External link detected: ${localData.link}");
} }
if (data.link!.startsWith('http')) { if (localData.type == 'monthly') {
String linkWithToken = data.link!; 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) { if (RequestService.token != null && RequestService.token!.isNotEmpty) {
String separator = data.link!.contains('?') ? '&' : '?'; String separator = localData.link!.contains('?') ? '&' : '?';
linkWithToken = "${data.link}${separator}accessToken=${RequestService.token}"; linkWithToken =
"${localData.link}${separator}accessToken=${RequestService.token}";
} }
if (kDebugMode) { if (kDebugMode) {
print("Opening external link with token: $linkWithToken"); print("Opening external link with token: $linkWithToken");
} }
AppInitializer.openWebLink( AppInitializer.openWebLink(
navigatorKey.currentContext!, navigatorKey.currentContext!,
linkWithToken, linkWithToken,
@ -304,18 +414,45 @@ class HomeWidgetRepository {
); );
} }
} }
if (kDebugMode) { if (kDebugMode) {
print("Final navigation decision:"); print("Final navigation decision:");
print("Route: $route"); print("Route: $route");
print("Args: $args"); print("Args: $args");
print("==========================="); print("===========================");
} }
if (route.isNotEmpty) { 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; return;
} }
} }

View File

@ -14,7 +14,15 @@ class FirebaseApi {
Future<void> initNotification() async { Future<void> initNotification() async {
try { try {
fcmToken = await _firebaseMessaging.getToken(); if (kIsWeb) {
fcmToken = await _firebaseMessaging.getToken(
vapidKey:
"BMXHGd93t_htpS7c62ceuuLVVmia2cEDmqxp46g9Vt0B3OxNMKIqN9nupsUMtv2Vq8Yy2sQGIqgCm9FxUSKvssU",
);
} else {
fcmToken = await _firebaseMessaging.getToken();
}
if (kDebugMode) { if (kDebugMode) {
print("fCMToken: $fcmToken"); print("fCMToken: $fcmToken");
} }
@ -45,16 +53,24 @@ class FirebaseApi {
} }
print("================================================"); print("================================================");
} }
try { try {
NotificationMessage data = NotificationMessage.fromJson(initMsg.data); NotificationMessage data = NotificationMessage.fromJson(initMsg.data);
HomeWidgetRepository.data = data; HomeWidgetRepository.data = data;
if (kDebugMode) { if (kDebugMode) {
print("Parsed NotificationMessage: ${data.toJson()}"); print("Parsed NotificationMessage: ${data.toJson()}");
print("Scheduling navigation from terminated state...");
} }
await HomeWidgetRepository.decideWhereToGoNotif(); // Schedule navigation to happen after app is fully initialized
await StorageService.delete( // This ensures navigatorKey is ready
key: 'notification${AppInitializer.createNotificationId(data)}'); // 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) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
print("Error handling initial message: $e"); print("Error handling initial message: $e");
@ -74,13 +90,15 @@ class FirebaseApi {
} }
print("================================================"); print("================================================");
} }
try { try {
NotificationMessage data = NotificationMessage.fromJson(initMsg.data); NotificationMessage data = NotificationMessage.fromJson(initMsg.data);
HomeWidgetRepository.data = data; HomeWidgetRepository.data = data;
if (kDebugMode) { if (kDebugMode) {
print("Parsed NotificationMessage: ${data.toJson()}"); print("Parsed NotificationMessage: ${data.toJson()}");
print("Scheduling navigation from background state...");
} }
await Future.delayed(const Duration(milliseconds: 300));
await HomeWidgetRepository.decideWhereToGoNotif(); await HomeWidgetRepository.decideWhereToGoNotif();
await StorageService.delete( await StorageService.delete(
key: 'notification${AppInitializer.createNotificationId(data)}'); key: 'notification${AppInitializer.createNotificationId(data)}');
@ -97,7 +115,7 @@ class FirebaseApi {
void handleMessage(RemoteMessage? message) async { void handleMessage(RemoteMessage? message) async {
if (message == null) return; if (message == null) return;
if (kDebugMode) { if (kDebugMode) {
print("=== NOTIFICATION RECEIVED (FOREGROUND) ==="); print("=== NOTIFICATION RECEIVED (FOREGROUND) ===");
print("Message ID: ${message.messageId}"); print("Message ID: ${message.messageId}");
@ -106,7 +124,7 @@ class FirebaseApi {
print("TTL: ${message.ttl}"); print("TTL: ${message.ttl}");
print("Message Type: ${message.messageType}"); print("Message Type: ${message.messageType}");
print("Category: ${message.category}"); print("Category: ${message.category}");
if (message.notification != null) { if (message.notification != null) {
print("--- NOTIFICATION PAYLOAD ---"); print("--- NOTIFICATION PAYLOAD ---");
print("Title: ${message.notification!.title}"); print("Title: ${message.notification!.title}");
@ -114,7 +132,7 @@ class FirebaseApi {
print("Android Image: ${message.notification!.android?.imageUrl}"); print("Android Image: ${message.notification!.android?.imageUrl}");
print("Apple Image: ${message.notification!.apple?.imageUrl}"); print("Apple Image: ${message.notification!.apple?.imageUrl}");
} }
print("--- DATA PAYLOAD ---"); print("--- DATA PAYLOAD ---");
print("Raw Data: ${message.data}"); print("Raw Data: ${message.data}");
try { try {
@ -123,10 +141,10 @@ class FirebaseApi {
} catch (e) { } catch (e) {
print("Error parsing NotificationData: $e"); print("Error parsing NotificationData: $e");
} }
print("=========================================="); print("==========================================");
} }
try { try {
await NotificationService.showFirebaseNotification(message); await NotificationService.showFirebaseNotification(message);
} catch (e) { } catch (e) {

View File

@ -16,6 +16,7 @@ class NotificationService {
static Future<void> initializeNotification() async { static Future<void> initializeNotification() async {
const AndroidInitializationSettings initializationSettingsAndroid = const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher'); AndroidInitializationSettings('@mipmap/ic_launcher');
const InitializationSettings initializationSettings = const InitializationSettings initializationSettings =
InitializationSettings(android: initializationSettingsAndroid); InitializationSettings(android: initializationSettingsAndroid);
@ -24,18 +25,20 @@ class NotificationService {
onDidReceiveNotificationResponse: _onNotificationResponse, onDidReceiveNotificationResponse: _onNotificationResponse,
); );
const AndroidNotificationChannel channel = AndroidNotificationChannel( if (!kIsWeb) {
'content', const AndroidNotificationChannel channel = AndroidNotificationChannel(
'Content Notification', 'content',
description: 'Notification channel', 'Content Notification',
importance: Importance.max, description: 'Notification channel',
playSound: true, importance: Importance.max,
); playSound: true,
);
await _flutterLocalNotificationsPlugin await _flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation< .resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>() AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel); ?.createNotificationChannel(channel);
}
} }
static Future<void> _onNotificationResponse( static Future<void> _onNotificationResponse(
@ -49,7 +52,7 @@ class NotificationService {
print("Payload: ${response.payload}"); print("Payload: ${response.payload}");
print("==================================="); print("===================================");
} }
try { try {
final payload = response.payload; final payload = response.payload;
if (payload != null) { if (payload != null) {
@ -75,16 +78,17 @@ class NotificationService {
if (kDebugMode) { if (kDebugMode) {
print("=== SHOWING FIREBASE NOTIFICATION ==="); print("=== SHOWING FIREBASE NOTIFICATION ===");
print("Message Data: ${message.data}"); print("Message Data: ${message.data}");
print("Notification: ${message.notification?.title} - ${message.notification?.body}"); print(
"Notification: ${message.notification?.title} - ${message.notification?.body}");
} }
try { try {
final data = NotificationMessage.fromJson(message.data); final data = NotificationMessage.fromJson(message.data);
if (kDebugMode) { if (kDebugMode) {
print("Parsed NotificationMessage: ${data.toJson()}"); print("Parsed NotificationMessage: ${data.toJson()}");
print("Notification Type: ${data.notificationType}"); print("Notification Type: ${data.notificationType}");
} }
if (data.notificationType!.contains('3')) { if (data.notificationType!.contains('3')) {
if (kDebugMode) { if (kDebugMode) {
print("Widget notification - calling fetchWidget()"); print("Widget notification - calling fetchWidget()");

View File

@ -28,6 +28,7 @@ class _BotAssistantsPageState extends State<BotAssistantsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
appBar: HoshanAppBar( appBar: HoshanAppBar(
onBack: () => Navigator.pop(context), onBack: () => Navigator.pop(context),
withActions: false, withActions: false,

View File

@ -61,6 +61,7 @@ class _HistoryAiChatPageState extends State<HistoryAiChatPage> {
return true; return true;
}, },
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true,
key: scaffKey, key: scaffKey,
appBar: HoshanAppBar( appBar: HoshanAppBar(
onBack: () { onBack: () {

View File

@ -27,6 +27,7 @@ class _InfoPageState extends State<InfoPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
appBar: HoshanAppBar( appBar: HoshanAppBar(
withActions: false, withActions: false,
withInfo: false, withInfo: false,

View File

@ -42,15 +42,15 @@ class _AuthenticationState extends State<Authentication> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
body: Consumer<AuthenticationState>( body: Consumer<AuthenticationState>(
builder: (context, state, child) => WillPopScope( builder: (context, state, child) => WillPopScope(
onWillPop: () async { onWillPop: () async {
if (state.currentPageIndex == 0) { if (state.currentPageIndex == 0) {
return true; return true;
} }
// Check if on OTP screen and no password exists
if (state.currentPageIndex == 2 && !state.hasPassword) { if (state.currentPageIndex == 2 && !state.hasPassword) {
state.currentPageIndex = 0; // Go back to username screen state.currentPageIndex = 0;
return false; return false;
} }
state.currentPageIndex--; state.currentPageIndex--;

View File

@ -341,4 +341,4 @@ class _CommentPlaceholder extends StatelessWidget {
], ],
); );
} }
} }

View File

@ -87,6 +87,7 @@ class _DidvanPlusVideoPlayerState extends State<DidvanPlusVideoPlayer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: SafeArea( body: SafeArea(
child: Column( child: Column(

View File

@ -72,6 +72,7 @@ class _FilteredBookmarksState extends State<FilteredBookmarks> {
final state = context.watch<FilteredBookmarksState>(); final state = context.watch<FilteredBookmarksState>();
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(90.0), preferredSize: const Size.fromHeight(90.0),
child: AppBar( child: AppBar(

View File

@ -169,6 +169,7 @@ class _InfographyScreenState extends State<InfographyScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0.0, elevation: 0.0,

View File

@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:didvan/services/media/voice.dart';
class DidvanVoiceListPage extends StatefulWidget { class DidvanVoiceListPage extends StatefulWidget {
final List<DidvanVoiceModel> voices; final List<DidvanVoiceModel> voices;
@ -30,6 +31,12 @@ class _DidvanVoiceListPageState extends State<DidvanVoiceListPage> {
} }
} }
@override
void dispose() {
VoiceService.audioPlayer.stop();
super.dispose();
}
void _onVoiceSelected(DidvanVoiceModel voice) { void _onVoiceSelected(DidvanVoiceModel voice) {
setState(() { setState(() {
_selectedVoice = voice; _selectedVoice = voice;
@ -42,6 +49,7 @@ class _DidvanVoiceListPageState extends State<DidvanVoiceListPage> {
widget.voices.where((v) => v.id != _selectedVoice.id).toList(); widget.voices.where((v) => v.id != _selectedVoice.id).toList();
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
title: const DidvanText( title: const DidvanText(
'یک لقمه استراتژی', 'یک لقمه استراتژی',
@ -73,20 +81,11 @@ class _DidvanVoiceListPageState extends State<DidvanVoiceListPage> {
const SizedBox(height: 20), const SizedBox(height: 20),
if (listItems.isNotEmpty) if (listItems.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16),
horizontal: 16, vertical: 8), child: Divider(
child: Row( color: Colors.grey[300],
children: [ thickness: 2,
DidvanText( height: 20,
'سایر قسمت‌ها',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
), ),
), ),
ListView.builder( ListView.builder(

View File

@ -301,6 +301,7 @@ class _ExploreLatestSlider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('🟢🟢🟢 _ExploreLatestSlider build called 🟢🟢🟢');
final List<Widget> items = []; final List<Widget> items = [];
final List< final List<
({String type, MainPageContentType? content, SwotItem? swotItem})> ({String type, MainPageContentType? content, SwotItem? swotItem})>
@ -312,6 +313,12 @@ class _ExploreLatestSlider extends StatelessWidget {
} }
if (list.contents.isNotEmpty) { if (list.contents.isNotEmpty) {
final newestContent = list.contents.first; 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( items.add(
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
@ -341,6 +348,12 @@ class _ExploreLatestSlider extends StatelessWidget {
if (items.isEmpty) return const SizedBox.shrink(); 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( return Carousel3D(
items: items, items: items,
height: 220, height: 220,
@ -350,11 +363,16 @@ class _ExploreLatestSlider extends StatelessWidget {
onItemTap: (index) { onItemTap: (index) {
final data = itemsData[index]; final data = itemsData[index];
if (data.content != null) { if (data.content != null) {
// استخراج ایمن فایل
final String? rawFile = data.content!.file;
final String fileString = (rawFile != null) ? rawFile.toString() : '';
context.read<MainPageState>().navigationHandler( context.read<MainPageState>().navigationHandler(
data.type, data.type,
data.content!.id, data.content!.id,
data.content!.link ?? '', data.content!.link ?? '',
description: data.content!.title, description: data.content!.title,
file: fileString, // ارسال فایل به هندلر
); );
} }
}, },

View File

@ -31,6 +31,8 @@ class MainPageState extends CoreProvier {
List<DidvanVoiceModel> didvanVoiceList = []; List<DidvanVoiceModel> didvanVoiceList = [];
TopBannerModel? topBanner; TopBannerModel? topBanner;
// ... (سایر متدها بدون تغییر) ...
DidvanVoiceModel? _pickLatestVoice(List<DidvanVoiceModel> items) { DidvanVoiceModel? _pickLatestVoice(List<DidvanVoiceModel> items) {
if (items.isEmpty) return null; if (items.isEmpty) return null;
items.sort((a, b) { items.sort((a, b) {
@ -57,62 +59,35 @@ class MainPageState extends CoreProvier {
} }
} }
int getStoryStartIndex(List<MainPageContentType> stories) { // ... (سایر متدهای دریافت اطلاعات بدون تغییر) ...
final firstUnreadIndex = stories.indexWhere((story) => !story.isViewed);
return firstUnreadIndex != -1 ? firstUnreadIndex : 0;
}
Future<void> _getSwotItems() async { Future<void> _getSwotItems() async {
try { try {
swotItems = await SwotService.fetchSwotItems(); swotItems = await SwotService.fetchSwotItems();
} catch (e) { } catch (e) {
// ignore: avoid_print
print(e); print(e);
} }
} }
Future<void> _getDidvanPlus() async { Future<void> _getDidvanPlus() async {
debugPrint('🎬 Fetching Didvan Plus data...');
debugPrint('🎬 URL: ${RequestHelper.didvanPlus}');
debugPrint('🎬 Token exists: ${RequestService.token != null}');
try { try {
final service = RequestService(RequestHelper.didvanPlus); final service = RequestService(RequestHelper.didvanPlus);
await service.httpGet(); await service.httpGet();
debugPrint('🎬 Didvan Plus statusCode: ${service.statusCode}');
if (service.statusCode == 200) { if (service.statusCode == 200) {
final rawData = service.data('result'); final rawData = service.data('result');
debugPrint('🎬 Raw data type: ${rawData.runtimeType}');
if (rawData is List && rawData.isNotEmpty) { if (rawData is List && rawData.isNotEmpty) {
debugPrint('🎬 Data is List with ${rawData.length} items');
didvanPlusList = rawData didvanPlusList = rawData
.map((item) => .map((item) =>
DidvanPlusModel.fromJson(Map<String, dynamic>.from(item))) DidvanPlusModel.fromJson(Map<String, dynamic>.from(item)))
.toList(); .toList();
didvanPlus = didvanPlusList.first; 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(); notifyListeners();
} else if (rawData is Map) { } else if (rawData is Map) {
debugPrint('🎬 Data is Map, using directly');
didvanPlus = didvanPlus =
DidvanPlusModel.fromJson(Map<String, dynamic>.from(rawData)); DidvanPlusModel.fromJson(Map<String, dynamic>.from(rawData));
didvanPlusList = [didvanPlus!]; didvanPlusList = [didvanPlus!];
debugPrint('✅ Didvan Plus loaded: ${didvanPlus?.id}');
notifyListeners(); notifyListeners();
} else {
debugPrint('⚠️ Didvan Plus: Empty or unexpected data format');
} }
} else {
debugPrint(
'⚠️ Didvan Plus: Request failed with status ${service.statusCode}');
} }
} catch (e) { } catch (e) {
debugPrint('❌ Failed to load Didvan Plus: $e'); debugPrint('❌ Failed to load Didvan Plus: $e');
@ -120,50 +95,27 @@ class MainPageState extends CoreProvier {
} }
Future<void> _getDidvanVoice() async { Future<void> _getDidvanVoice() async {
debugPrint('🎙️ Fetching Didvan Voice data...');
debugPrint('🎙️ URL: ${RequestHelper.didvanVoice}');
debugPrint('🎙️ Token: ${RequestService.token}');
try { try {
final service = RequestService(RequestHelper.didvanVoice); final service = RequestService(RequestHelper.didvanVoice);
await service.httpGet(); await service.httpGet();
debugPrint('🎙️ Didvan Voice statusCode: ${service.statusCode}');
if (service.statusCode == 200) { if (service.statusCode == 200) {
final rawData = service.data('result'); 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) { if (rawData is List && rawData.isNotEmpty) {
debugPrint('🎙️ Data is List with ${rawData.length} items');
didvanVoiceList = rawData didvanVoiceList = rawData
.map((e) => DidvanVoiceModel.fromJson( .map((e) => DidvanVoiceModel.fromJson(
Map<String, dynamic>.from(e as Map))) Map<String, dynamic>.from(e as Map)))
.toList(); .toList();
didvanVoice = _pickLatestVoice(List.from(didvanVoiceList)); 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(); notifyListeners();
} else if (rawData is Map) { } else if (rawData is Map) {
debugPrint('🎙️ Data is Map, using directly');
didvanVoice = didvanVoice =
DidvanVoiceModel.fromJson(Map<String, dynamic>.from(rawData)); DidvanVoiceModel.fromJson(Map<String, dynamic>.from(rawData));
didvanVoiceList = [didvanVoice!]; didvanVoiceList = [didvanVoice!];
debugPrint('✅ Didvan Voice single item loaded: ${didvanVoice?.id}');
notifyListeners(); 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('❌ Failed to load Didvan Voice: $e');
debugPrint('Stack trace: $stackTrace');
} }
} }
@ -174,10 +126,7 @@ class MainPageState extends CoreProvier {
if (service.statusCode == 200) { if (service.statusCode == 200) {
final data = service.result['result'] ?? service.result; final data = service.result['result'] ?? service.result;
topBanner = TopBannerModel.fromJson(data); topBanner = TopBannerModel.fromJson(data);
debugPrint('✅ Top Banner loaded: ${topBanner?.id}');
notifyListeners(); notifyListeners();
} else {
debugPrint('⚠️ Top Banner: No result in response');
} }
} catch (e) { } catch (e) {
debugPrint('❌ Failed to load Top Banner: $e'); debugPrint('❌ Failed to load Top Banner: $e');
@ -187,7 +136,6 @@ class MainPageState extends CoreProvier {
Future<void> _fetchStories() async { Future<void> _fetchStories() async {
try { try {
stories = await StoryService.getStories(); stories = await StoryService.getStories();
// print("Fetched ${stories.length} stories.");
} catch (e) { } catch (e) {
stories = []; stories = [];
debugPrint("Could not fetch stories: $e"); debugPrint("Could not fetch stories: $e");
@ -207,7 +155,6 @@ class MainPageState extends CoreProvier {
_getDidvanVoice(), _getDidvanVoice(),
_getTopBanner(), _getTopBanner(),
]); ]);
debugPrint("✅ All main page data loaded");
appState = AppState.idle; appState = AppState.idle;
} catch (e) { } catch (e) {
debugPrint("❌ Main page init failed: $e"); debugPrint("❌ Main page init failed: $e");
@ -226,11 +173,66 @@ class MainPageState extends CoreProvier {
notifyListeners(); notifyListeners();
} }
/// متد جدید: دریافت جزئیات ماهنامه برای پیدا کردن فایل PDF
Future<void> _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( void navigationHandler(
String type, String type,
int id, int id,
String? link, { String? link, {
String? description, String? description,
String? file,
}) { }) {
link = link ?? ''; link = link ?? '';
dynamic args; dynamic args;
@ -281,19 +283,47 @@ class MainPageState extends CoreProvier {
} }
case 'monthly': 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( Navigator.of(navigatorKey.currentContext!).pushNamed(
Routes.web, Routes.web,
arguments: link, 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; return;
} }
} }
// هندل کردن سایر لینکهای عمومی
if (link == '') { if (link == '') {
return; return;
} }
if (link.startsWith('http')) { if (link!.startsWith('http')) {
AppInitializer.openWebLink( AppInitializer.openWebLink(
navigatorKey.currentContext!, navigatorKey.currentContext!,
'$link?accessToken=${RequestService.token}', '$link?accessToken=${RequestService.token}',
@ -301,6 +331,6 @@ class MainPageState extends CoreProvier {
); );
return; return;
} }
Navigator.of(navigatorKey.currentContext!).pushNamed(link, arguments: args); Navigator.of(navigatorKey.currentContext!).pushNamed(link!, arguments: args);
} }
} }

View File

@ -3,6 +3,7 @@ import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/didvan_voice_model.dart'; import 'package:didvan/models/didvan_voice_model.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/network/request.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/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -23,7 +24,6 @@ class DidvanVoiceDetailCard extends StatefulWidget {
class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> { class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
late AudioPlayer _audioPlayer; late AudioPlayer _audioPlayer;
// ignore: unused_field
bool _isInitialized = false; bool _isInitialized = false;
@override @override
@ -33,18 +33,21 @@ class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
} }
void _initializeAudio() async { void _initializeAudio() async {
_audioPlayer = AudioPlayer(); _audioPlayer = VoiceService.audioPlayer;
final audioUrl = final audioUrl =
'${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}'; '${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}';
debugPrint('🎙️ Didvan Voice Audio URL: $audioUrl'); debugPrint('🎙️ Didvan Voice Audio URL: $audioUrl');
try { try {
VoiceService.src = audioUrl;
await _audioPlayer.setUrl(audioUrl); await _audioPlayer.setUrl(audioUrl);
setState(() {
_isInitialized = true; if (mounted) {
}); setState(() {
debugPrint('✅ Audio initialized successfully'); _isInitialized = true;
});
}
} catch (e) { } catch (e) {
debugPrint('❌ Audio initialization error: $e'); debugPrint('❌ Audio initialization error: $e');
} }
@ -52,7 +55,6 @@ class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
@override @override
void dispose() { void dispose() {
_audioPlayer.dispose();
super.dispose(); super.dispose();
} }
@ -144,8 +146,10 @@ class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
overlayShape: RoundSliderOverlayShape( overlayShape: RoundSliderOverlayShape(
overlayRadius: 12), overlayRadius: 12),
trackHeight: 4, trackHeight: 4,
thumbColor: Color.fromARGB(255, 0, 126 , 167), thumbColor:
activeTrackColor: Color.fromARGB(255, 0, 126 , 167), Color.fromARGB(255, 0, 126, 167),
activeTrackColor:
Color.fromARGB(255, 0, 126, 167),
inactiveTrackColor: Colors.grey, inactiveTrackColor: Colors.grey,
), ),
child: Slider( child: Slider(
@ -266,4 +270,4 @@ class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
), ),
); );
} }
} }

View File

@ -1,6 +1,7 @@
import 'package:didvan/models/didvan_voice_model.dart'; import 'package:didvan/models/didvan_voice_model.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/network/request.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/didvan/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
@ -19,8 +20,6 @@ class DidvanVoiceSection extends StatefulWidget {
class _DidvanVoiceSectionState extends State<DidvanVoiceSection> { class _DidvanVoiceSectionState extends State<DidvanVoiceSection> {
late AudioPlayer _audioPlayer; late AudioPlayer _audioPlayer;
// ignore: unused_field
bool _isInitialized = false;
@override @override
void initState() { void initState() {
@ -28,28 +27,8 @@ class _DidvanVoiceSectionState extends State<DidvanVoiceSection> {
_initializeAudio(); _initializeAudio();
} }
void _initializeAudio() async { void _initializeAudio() {
_audioPlayer = AudioPlayer(); _audioPlayer = VoiceService.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) { String _formatDuration(Duration? duration) {
@ -62,6 +41,8 @@ class _DidvanVoiceSectionState extends State<DidvanVoiceSection> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final imageUrl = '${RequestHelper.baseUrl}${widget.didvanVoice.image}'; final imageUrl = '${RequestHelper.baseUrl}${widget.didvanVoice.image}';
final audioUrl =
'${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}';
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
@ -92,148 +73,176 @@ class _DidvanVoiceSectionState extends State<DidvanVoiceSection> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: StreamBuilder<PlayerState>(
crossAxisAlignment: CrossAxisAlignment.start, stream: _audioPlayer.playerStateStream,
children: [ builder: (context, snapshot) {
DidvanText( final isMyVoice = VoiceService.src == audioUrl;
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<PlayerState>(
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 || final playerState = snapshot.data;
processingState == ProcessingState.buffering) { final processingState =
return Container( 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), margin: const EdgeInsets.symmetric(horizontal: 8),
width: 48, width: 48,
height: 48, height: 48,
child: const CircularProgressIndicator( 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, color: Colors.white,
), ),
);
} else if (!playing) {
return IconButton(
onPressed: _audioPlayer.play,
icon: const Icon(Icons.play_circle_filled,
color: Colors.white),
iconSize: 48, 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<Duration?>(
stream: _audioPlayer.durationStream,
builder: (context, snapshot) {
final duration = snapshot.data;
return StreamBuilder<Duration>(
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<Duration?>(
stream: _audioPlayer.durationStream,
builder: (context, durationSnapshot) {
// اگر صدای من نیست، مدت زمان و پوزیشن را صفر نشان بده
final duration = isMyVoice
? (durationSnapshot.data ?? Duration.zero)
: Duration.zero;
return StreamBuilder<Duration>(
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<Duration?>(
stream: _audioPlayer.durationStream,
builder: (context, snapshot) {
final duration = snapshot.data ?? Duration.zero;
return StreamBuilder<Duration>(
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()));
},
),
);
},
);
},
),
),
],
), ),
), ),
], ],

View File

@ -54,6 +54,7 @@ class _MediaPageState extends State<MediaPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
// ignore: deprecated_member_use // ignore: deprecated_member_use
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
body: SafeArea( body: SafeArea(

View File

@ -147,6 +147,7 @@ class _VideoDetailsPageState extends State<VideoDetailsPage>
builder: (context, state) { builder: (context, state) {
if (!state.isStudioLoaded) { if (!state.isStudioLoaded) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
body: Center( body: Center(
child: Image.asset( child: Image.asset(
Assets.loadingAnimation, Assets.loadingAnimation,
@ -166,6 +167,7 @@ class _VideoDetailsPageState extends State<VideoDetailsPage>
return true; return true;
}, },
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(90.0), preferredSize: const Size.fromHeight(90.0),

View File

@ -44,6 +44,7 @@ class _NewStatisticState extends State<NewStatistic> {
const double headerHeight = 150.0; const double headerHeight = 150.0;
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverPersistentHeader( SliverPersistentHeader(

View File

@ -74,6 +74,7 @@ class _StatGeneralScreenState extends State<StatGeneralScreen> {
var source = context.read<StatGeneralScreenState>().source; var source = context.read<StatGeneralScreenState>().source;
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
elevation: 0.0, elevation: 0.0,
scrolledUnderElevation: 0.0, scrolledUnderElevation: 0.0,

View File

@ -75,6 +75,7 @@ class _NewStockState extends State<NewStock> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0.0, elevation: 0.0,

View File

@ -41,6 +41,7 @@ class _NewsDetailsState extends State<NewsDetails> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
body: Consumer<NewsDetailsState>( body: Consumer<NewsDetailsState>(
builder: (context, state, child) => StateHandler<NewsDetailsState>( builder: (context, state, child) => StateHandler<NewsDetailsState>(
onRetry: () => state.getNewsDetails(widget.pageData['id']), onRetry: () => state.getNewsDetails(widget.pageData['id']),

View File

@ -94,6 +94,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: colorScheme.background, backgroundColor: colorScheme.background,
body: SafeArea( body: SafeArea(
child: Stack( child: Stack(

View File

@ -40,6 +40,7 @@ class _PdfViewerPageState extends State<PdfViewerPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
title: DidvanText( title: DidvanText(
widget.title, widget.title,

View File

@ -288,6 +288,7 @@ class _StudioDetailsState extends State<StudioDetails>
builder: (context, state) { builder: (context, state) {
if (!state.isStudioLoaded) { if (!state.isStudioLoaded) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
body: Center( body: Center(
child: Image.asset( child: Image.asset(
Assets.loadingAnimation, Assets.loadingAnimation,
@ -307,6 +308,7 @@ class _StudioDetailsState extends State<StudioDetails>
return true; return true;
}, },
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(90.0), preferredSize: const Size.fromHeight(90.0),

View File

@ -236,6 +236,7 @@ class _StudioDetailsState extends State<StudioDetails>
return true; return true;
}, },
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(90.0), preferredSize: const Size.fromHeight(90.0),

View File

@ -29,6 +29,7 @@ class _ChangePasswordPageState extends State<ChangePasswordPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
body: Consumer<ChangePasswordState>( body: Consumer<ChangePasswordState>(
// ignore: deprecated_member_use // ignore: deprecated_member_use
builder: (context, state, child) => WillPopScope( builder: (context, state, child) => WillPopScope(

View File

@ -42,6 +42,7 @@ class _RadarDetailsState extends State<RadarDetails> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
body: Consumer<RadarDetailsState>( body: Consumer<RadarDetailsState>(
builder: (context, state, child) => StateHandler<RadarDetailsState>( builder: (context, state, child) => StateHandler<RadarDetailsState>(
onRetry: () => state.getRadarDetails(widget.pageData['id']), onRetry: () => state.getRadarDetails(widget.pageData['id']),

View File

@ -9,6 +9,7 @@ import 'package:didvan/providers/server_data.dart';
import 'package:didvan/providers/theme.dart'; import 'package:didvan/providers/theme.dart';
import 'package:didvan/providers/user.dart'; import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.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/app_initalizer.dart';
import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
@ -39,6 +40,13 @@ class _SplashState extends State<Splash> with SingleTickerProviderStateMixin {
void initState() { void initState() {
super.initState(); super.initState();
if (kIsWeb) {
final loader = html.document.getElementById('loading_indicator');
if (loader != null) {
loader.remove();
}
}
_setupAnimations(); _setupAnimations();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_startInitialization(); _startInitialization();
@ -87,6 +95,7 @@ class _SplashState extends State<Splash> with SingleTickerProviderStateMixin {
systemNavigationBarColor: colorScheme.background, systemNavigationBarColor: colorScheme.background,
), ),
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: colorScheme.background, backgroundColor: colorScheme.background,
body: SafeArea( body: SafeArea(
child: Center( child: Center(
@ -279,7 +288,7 @@ class _SplashState extends State<Splash> with SingleTickerProviderStateMixin {
} }
} }
void _navigateToNextScreen(String? token) { void _navigateToNextScreen(String? token) async {
if (!mounted) return; if (!mounted) return;
String extractedPath = initialURI?.path == '/' String extractedPath = initialURI?.path == '/'
@ -300,6 +309,22 @@ class _SplashState extends State<Splash> with SingleTickerProviderStateMixin {
routeArguments = false; 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( Navigator.of(context).pushReplacementNamed(
destinationRoute, destinationRoute,
arguments: routeArguments, arguments: routeArguments,

View File

@ -47,6 +47,7 @@ class _StoryViewerPageState extends State<StoryViewerPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
body: Directionality( body: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: PageView.builder( child: PageView.builder(

View File

@ -80,6 +80,7 @@ class _WebViewState extends State<WebView> {
} }
}, },
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
title: const DidvanText( title: const DidvanText(
'بازگشت', 'بازگشت',

View File

@ -5,6 +5,7 @@ import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:record/record.dart'; import 'package:record/record.dart';
import 'package:flutter_sound/flutter_sound.dart' hide Codec; import 'package:flutter_sound/flutter_sound.dart' hide Codec;
@ -34,20 +35,22 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
static const int inputSampleRate = 16000; static const int inputSampleRate = 16000;
static const int geminiSampleRate = 24000; static const int geminiSampleRate = 24000;
// --- VAD Settings --- // --- VAD Settings ---
static const double vadThreshold = 0.05; static const double vadThreshold = 0.05;
static const double speechThreshold = 0.1; static const double speechThreshold = 0.1;
// --- روش ساده و مطمئن: آستانه ثابت --- // --- روش ساده و مطمئن: آستانه ثابت ---
// صدای AI از اسپیکر معمولاً RMS حدود 0.05-0.12 دارد // صدای AI از اسپیکر معمولاً RMS حدود 0.05-0.12 دارد
// صدای کاربر مستقیم به میکروفون معمولاً RMS بالای 0.15 دارد // صدای کاربر مستقیم به میکروفون معمولاً RMS بالای 0.15 دارد
static const double userInterruptThreshold = 0.25; // آستانه پایینتر - حساسیت بیشتر static const double userInterruptThreshold =
0.25; // آستانه پایینتر - حساسیت بیشتر
static const int ignoreInitialMs = 800; // 800ms اول نادیده گرفته شود static const int ignoreInitialMs = 800; // 800ms اول نادیده گرفته شود
static const int sustainedChunksRequired = 4; // 4 chunk متوالی برای تایید اینتراپت static const int sustainedChunksRequired =
4; // 4 chunk متوالی برای تایید اینتراپت
int _interruptChunkCount = 0; // شمارنده chunkهای متوالی با صدای بالا int _interruptChunkCount = 0; // شمارنده chunkهای متوالی با صدای بالا
DateTime? _aiPlaybackStartTime; // زمان شروع پخش AI DateTime? _aiPlaybackStartTime; // زمان شروع پخش AI
static const int vadSustainMs = 150; static const int vadSustainMs = 150;
static const int silenceTimeoutMs = 1000; static const int silenceTimeoutMs = 1000;
@ -111,58 +114,62 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
vsync: this, vsync: this,
duration: const Duration(milliseconds: 50), duration: const Duration(milliseconds: 50),
)..addListener(() { )..addListener(() {
if (_isRecording || _isAiSpeaking) { if (_isRecording || _isAiSpeaking) {
setState(() { setState(() {
final random = math.Random(); 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++) { for (int i = 0; i < _audioWaveHeights.length; i++) {
double target = 0.1 + (random.nextDouble() * 0.4); _audioWaveHeights[i] = _audioWaveHeights[i] * 0.9;
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++) {
_audioWaveHeights[i] = _audioWaveHeights[i] * 0.9;
} }
} });
});
} }
Future<void> _initAudio() async { Future<void> _initAudio() async {
try { try {
// تنظیم AudioSession برای پخش از طریق MEDIA نه CALL // تنظیم AudioSession برای پخش از طریق MEDIA نه CALL
final session = await AudioSession.instance; final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration( await session.configure(AudioSessionConfiguration(
// 1. حالت PlayAndRecord برای استفاده همزمان // 1. حالت PlayAndRecord برای استفاده همزمان
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
// 2. تنظیمات کلیدی: // 2. تنظیمات کلیدی:
// defaultToSpeaker: صدا حتما از اسپیکر بیاید (نه گوشی) // defaultToSpeaker: صدا حتما از اسپیکر بیاید (نه گوشی)
// allowBluetooth: اجازه استفاده از هندزفری بلوتوث // allowBluetooth: اجازه استفاده از هندزفری بلوتوث
avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.defaultToSpeaker, AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.defaultToSpeaker,
// 3. حالت VoiceChat: این حالت پردازشگر سیگنال (DSP) موبایل را برای حذف اکو فعال میکند // 3. حالت VoiceChat: این حالت پردازشگر سیگنال (DSP) موبایل را برای حذف اکو فعال میکند
avAudioSessionMode: AVAudioSessionMode.voiceChat, avAudioSessionMode: AVAudioSessionMode.voiceChat,
androidAudioAttributes: const AndroidAudioAttributes( androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech, contentType: AndroidAudioContentType.speech,
flags: AndroidAudioFlags.none, flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.voiceCommunication, // در اندروید هم حالت مکالمه باشد usage: AndroidAudioUsage
.voiceCommunication, // در اندروید هم حالت مکالمه باشد
), ),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain, androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
)); ));
await _audioPlayer.openPlayer(); await _audioPlayer.openPlayer();
await _audioPlayer.setSubscriptionDuration(const Duration(milliseconds: 10)); await _audioPlayer
.setSubscriptionDuration(const Duration(milliseconds: 10));
_isPlayerInitialized = true; _isPlayerInitialized = true;
debugPrint('✅ Audio player initialized with VOICE CHAT + SPEAKER'); debugPrint('✅ Audio player initialized with VOICE CHAT + SPEAKER');
} catch (e) { } catch (e) {
debugPrint('❌ Error initializing audio player: $e'); debugPrint('❌ Error initializing audio player: $e');
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helper: Stop AI Playback (Internal Logic Only) // Helper: Stop AI Playback (Internal Logic Only)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -274,7 +281,7 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
sampleRate: 16000, sampleRate: 16000,
numChannels: 1, numChannels: 1,
// روشن بودن این گزینهها حیاتی است // روشن بودن این گزینهها حیاتی است
echoCancel: true, echoCancel: true,
noiseSuppress: true, noiseSuppress: true,
autoGain: false, autoGain: false,
), ),
@ -341,20 +348,22 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
// ۲. بررسی اینتراپت - روش ساده و مطمئن // ۲. بررسی اینتراپت - روش ساده و مطمئن
if (_ignoreAudioDuringAIPlayback) { if (_ignoreAudioDuringAIPlayback) {
if (_aiPlaybackStartTime != null) { if (_aiPlaybackStartTime != null) {
final elapsed = DateTime.now().difference(_aiPlaybackStartTime!).inMilliseconds; final elapsed =
DateTime.now().difference(_aiPlaybackStartTime!).inMilliseconds;
// در 500ms اول، همه چیز را نادیده بگیر (زمان برای استقرار صدا) // در 500ms اول، همه چیز را نادیده بگیر (زمان برای استقرار صدا)
if (elapsed < ignoreInitialMs) { if (elapsed < ignoreInitialMs) {
return; return;
} }
// اگر صدا از آستانه بالاتر بود // اگر صدا از آستانه بالاتر بود
if (rms > userInterruptThreshold) { if (rms > userInterruptThreshold) {
_interruptChunkCount++; _interruptChunkCount++;
// اگر چند chunk متوالی صدای بالا داشتیم = کاربر واقعاً صحبت میکند // اگر چند chunk متوالی صدای بالا داشتیم = کاربر واقعاً صحبت میکند
if (_interruptChunkCount >= sustainedChunksRequired) { 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; _wasStoppedByUser = true;
_interruptChunkCount = 0; _interruptChunkCount = 0;
_flushAiBuffers(); _flushAiBuffers();
@ -441,49 +450,51 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Receive and Play (AI Output) // Receive and Play (AI Output)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// کاهش حجم صدای PCM16 به صورت مستقیم /// کاهش حجم صدای PCM16 به صورت مستقیم
Uint8List _reduceAudioVolume(Uint8List audioData, double volumeFactor) { Uint8List _reduceAudioVolume(Uint8List audioData, double volumeFactor) {
final result = Uint8List(audioData.length); 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 maxOriginal = 0;
int maxReduced = 0; int maxReduced = 0;
for (int i = 0; i < audioData.length - 1; i += 2) { for (int i = 0; i < audioData.length - 1; i += 2) {
// خواندن sample به صورت Little Endian 16-bit signed integer // خواندن sample به صورت Little Endian 16-bit signed integer
int low = audioData[i]; int low = audioData[i];
int high = audioData[i + 1]; int high = audioData[i + 1];
int sample = (high << 8) | low; int sample = (high << 8) | low;
// تبدیل به signed // تبدیل به signed
if (sample >= 32768) { if (sample >= 32768) {
sample -= 65536; sample -= 65536;
} }
if (sample.abs() > maxOriginal) maxOriginal = sample.abs(); if (sample.abs() > maxOriginal) maxOriginal = sample.abs();
// کاهش شدید حجم - ضرب در ضریب خیلی کوچک // کاهش شدید حجم - ضرب در ضریب خیلی کوچک
double reduced = sample * volumeFactor; double reduced = sample * volumeFactor;
int newSample = reduced.round(); int newSample = reduced.round();
// محدود کردن // محدود کردن
newSample = newSample.clamp(-32768, 32767); newSample = newSample.clamp(-32768, 32767);
if (newSample.abs() > maxReduced) maxReduced = newSample.abs(); if (newSample.abs() > maxReduced) maxReduced = newSample.abs();
// تبدیل به unsigned // تبدیل به unsigned
if (newSample < 0) { if (newSample < 0) {
newSample += 65536; newSample += 65536;
} }
// نوشتن Little Endian // نوشتن Little Endian
result[i] = newSample & 0xFF; result[i] = newSample & 0xFF;
result[i + 1] = (newSample >> 8) & 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; return result;
} }
@ -491,7 +502,7 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
try { try {
_ignoreAudioDuringAIPlayback = true; _ignoreAudioDuringAIPlayback = true;
_aiPlaybackStartTime = DateTime.now(); // ثبت زمان شروع دریافت _aiPlaybackStartTime = DateTime.now(); // ثبت زمان شروع دریافت
String base64String; String base64String;
if (data is String) { if (data is String) {
base64String = data; base64String = data;
@ -529,6 +540,7 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
} }
} }
// متد اصلی پخش صدا با منطق جداگانه برای وب و موبایل
Future<void> _playAccumulatedAudio() async { Future<void> _playAccumulatedAudio() async {
if (_isSpeechActive) { if (_isSpeechActive) {
debugPrint('⚠️ User is speaking, cancelling AI playback'); debugPrint('⚠️ User is speaking, cancelling AI playback');
@ -562,48 +574,35 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
setState(() => _statusText = '🔊 AI در حال صحبت...'); setState(() => _statusText = '🔊 AI در حال صحبت...');
} }
final tempDir = await getTemporaryDirectory(); // --- تغییرات برای وب ---
final tempFile = File( if (kIsWeb) {
'${tempDir.path}/ai_response_${DateTime.now().millisecondsSinceEpoch}.pcm'); // در وب به جای ذخیره فایل، مستقیم از بافر پخش میکنیم
await tempFile.writeAsBytes(reducedAudioData); 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( await _audioPlayer.startPlayer(
fromURI: tempFile.path, fromURI: tempFile.path,
codec: fsp.Codec.pcm16, codec: fsp.Codec.pcm16,
sampleRate: geminiSampleRate, sampleRate: geminiSampleRate,
numChannels: 1, numChannels: 1,
whenFinished: () { whenFinished: () {
debugPrint('✅ Playback finished'); _onPlaybackFinished(tempFile: tempFile);
},
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 */
}
}
},
);
} catch (e) { } catch (e) {
debugPrint('❌ Playback Error: $e'); debugPrint('❌ Playback Error: $e');
_isPlayingFromQueue = false; _isPlayingFromQueue = false;
@ -616,6 +615,37 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
} }
} }
// متد کمکی برای پایان پخش و پاکسازی
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 @override
void dispose() { void dispose() {
_stopAiPlayback(); _stopAiPlayback();
@ -701,7 +731,6 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
child: Container(color: Colors.transparent), child: Container(color: Colors.transparent),
), ),
), ),
SafeArea( SafeArea(
child: Column( child: Column(
children: [ children: [
@ -787,7 +816,6 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
); );
}, },
), ),
AnimatedBuilder( AnimatedBuilder(
animation: _waveController, animation: _waveController,
builder: (context, child) { builder: (context, child) {
@ -800,7 +828,6 @@ class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
); );
}, },
), ),
AnimatedBuilder( AnimatedBuilder(
animation: _orbController, animation: _orbController,
builder: (context, child) { builder: (context, child) {
@ -969,4 +996,4 @@ class RipplePainter extends CustomPainter {
@override @override
bool shouldRepaint(covariant RipplePainter oldDelegate) => true; bool shouldRepaint(covariant RipplePainter oldDelegate) => true;
} }

View File

@ -199,12 +199,16 @@ class _Carousel3DState extends State<Carousel3D>
alignment: Alignment.center, alignment: Alignment.center,
child: Opacity( child: Opacity(
opacity: opacity, opacity: opacity,
child: IgnorePointer( child: GestureDetector(
ignoring: relativePos != 0, behavior: HitTestBehavior.opaque,
child: GestureDetector( onTap: () {
onTap: relativePos == 0 debugPrint('🎯 Item tapped - Index: $index, RelativePos: $relativePos, IsCentered: ${relativePos == 0}');
? () => widget.onItemTap?.call(index) if (relativePos == 0) {
: null, widget.onItemTap?.call(index);
}
},
child: IgnorePointer(
ignoring: false,
child: Container( child: Container(
width: screenWidth * 0.7, width: screenWidth * 0.7,
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -953,7 +953,105 @@ class _DidvanPageViewState extends State<DidvanPageView> {
Widget _contentBuilder(dynamic item, int index) { Widget _contentBuilder(dynamic item, int index) {
final content = item.contents[index]; final content = item.contents[index];
final text = content.text?.toLowerCase() ?? '';
final bool isSourcesSection = (index == item.contents.length - 1) &&
(text.contains('<a ') || text.contains('href='));
if (content.text != null) { if (content.text != null) {
if (isSourcesSection) {
final RegExp linkRegExp = RegExp(
r'<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>',
caseSensitive: false,
dotAll: true);
final matches = linkRegExp.allMatches(content.text!).toList();
String title = "";
int firstLinkIndex = content.text!.toLowerCase().indexOf('<a');
if (firstLinkIndex > 0) {
String preText = content.text!.substring(0, firstLinkIndex);
title = preText.replaceAll(RegExp(r'<[^>]*>'), '');
title =
title.replaceAll(RegExp(r'&nbsp;', 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( return Html(
data: content.text, data: content.text,
onAnchorTap: (href, _, element) { onAnchorTap: (href, _, element) {

View File

@ -58,7 +58,10 @@ class _DidvanScaffoldState extends State<DidvanScaffold> {
final double statusBarHeight = MediaQuery.of(context).padding.top; final double statusBarHeight = MediaQuery.of(context).padding.top;
final double systemNavigationBarHeight = final double systemNavigationBarHeight =
MediaQuery.of(context).padding.bottom; MediaQuery.of(context).padding.bottom;
final double keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
floatingActionButton: widget.floatingActionButton, floatingActionButton: widget.floatingActionButton,
body: Padding( body: Padding(
@ -68,7 +71,7 @@ class _DidvanScaffoldState extends State<DidvanScaffold> {
child: SizedBox( child: SizedBox(
height: MediaQuery.of(context).size.height - height: MediaQuery.of(context).size.height -
statusBarHeight - statusBarHeight -
systemNavigationBarHeight, (systemNavigationBarHeight - keyboardHeight),
child: Stack( child: Stack(
children: [ children: [
CustomScrollView( CustomScrollView(

View File

@ -4,7 +4,7 @@ FLUTTER_APPLICATION_PATH=C:\Flutter Projects\didvan-app\didvan-app
COCOAPODS_PARALLEL_CODE_SIGN=true COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_BUILD_DIR=build FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=5.0.0 FLUTTER_BUILD_NAME=5.0.0
FLUTTER_BUILD_NUMBER=6000 FLUTTER_BUILD_NUMBER=7002
DART_OBFUSCATION=false DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false TREE_SHAKE_ICONS=false

View File

@ -5,7 +5,7 @@ export "FLUTTER_APPLICATION_PATH=C:\Flutter Projects\didvan-app\didvan-app"
export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_BUILD_DIR=build" export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=5.0.0" export "FLUTTER_BUILD_NAME=5.0.0"
export "FLUTTER_BUILD_NUMBER=6000" export "FLUTTER_BUILD_NUMBER=7002"
export "DART_OBFUSCATION=false" export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true" export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false" export "TREE_SHAKE_ICONS=false"

View File

@ -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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 5.0.0+6000 version: 5.0.0+7005
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"

View File

@ -1,32 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF" /> <base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible" /> <meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project." /> <meta name="description" content="A new Flutter project." />
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" /> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="didvan" /> <meta name="apple-mobile-web-app-title" content="didvan" />
<link rel="apple-touch-icon" href="icons/icon.jpg" /> <link rel="apple-touch-icon" href="icons/icon.jpg" />
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<script src="flutter_bootstrap.js" async> <script src="flutter_bootstrap.js" async>
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
@ -65,9 +50,6 @@
<div id="loading_indicator" class="container"> <div id="loading_indicator" class="container">
<img class="indicator" src="./assets/lib/assets/animations/loading.gif" /> <img class="indicator" src="./assets/lib/assets/animations/loading.gif" />
</div> </div>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script> <script>
var serviceWorkerVersion = "{{flutter_service_worker_version}}"; var serviceWorkerVersion = "{{flutter_service_worker_version}}";
var scriptLoaded = false; var scriptLoaded = false;
@ -131,14 +113,6 @@
// Service workers not supported. Just drop the <script> tag. // Service workers not supported. Just drop the <script> tag.
loadMainDartJs(); loadMainDartJs();
} }
window.onload = function () {
setTimeout(function () {
var loadingIndicator = document.getElementById("loading_indicator");
if (loadingIndicator) {
loadingIndicator.remove();
}
}, 10000);
};
</script> </script>
</body> </body>
</html> </html>