fixed some bugs for v5
This commit is contained in:
parent
e9032d1022
commit
d54d466e3d
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
@ -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"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()");
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: () {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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--;
|
||||||
|
|
|
||||||
|
|
@ -341,4 +341,4 @@ class _CommentPlaceholder extends StatelessWidget {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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, // ارسال فایل به هندلر
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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']),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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']),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
'بازگشت',
|
'بازگشت',
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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' ', 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) {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue