fixed web history responsive

This commit is contained in:
Mohamad Mahdi Jebeli 2026-02-28 10:38:22 +03:30
parent 1f0ec41c1a
commit 5871108f2e
21 changed files with 686 additions and 458 deletions

BIN
lib.zip

Binary file not shown.

View File

@ -8,11 +8,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:home_widget/home_widget.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
// import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/firebase_options.dart';
import 'package:didvan/models/requests/news.dart';
@ -60,17 +59,15 @@ void main() async {
try {
if (!kIsWeb) {
// ignore: deprecated_member_use
HomeWidget.registerBackgroundCallback(_backgroundCallbackHomeWidget);
HomeWidget.registerInteractivityCallback(
_backgroundCallbackHomeWidget);
HomeWidget.registerInteractivityCallback(_backgroundCallbackHomeWidget);
await NotificationService.initializeNotification();
if (Platform.isAndroid) {
try {
await FlutterDownloader.initialize(debug: true, ignoreSsl: true);
} catch (e) {
e.printError();
if (kDebugMode) print("Downloader error: $e");
}
}
}
@ -80,36 +77,29 @@ void main() async {
.timeout(
const Duration(seconds: 10),
onTimeout: () {
debugPrint(
"Firebase initialization timed out - continuing without Firebase");
throw TimeoutException(
'Firebase initialization timeout', const Duration(seconds: 10));
debugPrint("Firebase initialization timed out - continuing without Firebase");
throw TimeoutException('Firebase initialization timeout', const Duration(seconds: 10));
},
);
await FirebaseApi().initNotification().timeout(
const Duration(seconds: 5),
onTimeout: () {
debugPrint("Firebase notification init timed out - continuing");
},
);
if (!kIsWeb) {
await FirebaseApi().initNotification().timeout(
const Duration(seconds: 5),
onTimeout: () {
debugPrint("Firebase notification init timed out - continuing");
},
);
}
} catch (e) {
debugPrint("Initialization Error: $e");
debugPrint("App will continue without Firebase services");
}
await SentryFlutter.init(
(options) {
options.dsn =
'https://a4cfcaa7d67471240d295c25c968d91d@o4508585857384448.ingest.de.sentry.io/4508585886548048';
options.tracesSampleRate = 1.0;
options.profilesSampleRate = 1.0;
},
appRunner: () => runApp(const Didvan()),
);
runApp(const Didvan());
},
(error, stack) {
Sentry.captureException(error, stackTrace: stack);
debugPrint("Error captured: $error");
},
);
}
@ -274,5 +264,3 @@ class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
);
}
}
// Developed by Mohamad Mahdi Jebeli - 2025

View File

@ -430,15 +430,31 @@ class RouteGenerator {
);
case Routes.didvanPlusList:
if (kDebugMode) {
print("📱 Route: didvanPlusList");
print("📱 Arguments type: ${settings.arguments.runtimeType}");
print("📱 Arguments: ${settings.arguments}");
}
if (settings.arguments is List) {
final argsList = settings.arguments as List;
if (kDebugMode) {
print("📱 Arguments list length: ${argsList.length}");
print("📱 First item type: ${argsList.isNotEmpty ? argsList.first.runtimeType : 'empty'}");
}
return _createRoute(
DidvanPlusListPage(
items: (settings.arguments as List).cast<DidvanPlusModel>(),
items: argsList.cast<DidvanPlusModel>(),
),
);
}
if (kDebugMode) {
print("📱 ❌ Invalid arguments type - Expected List, got ${settings.arguments.runtimeType}");
}
return _errorRoute(
'Invalid arguments for ${settings.name}: Expected List.');
'Invalid arguments for ${settings.name}: Expected List, got ${settings.arguments.runtimeType}.');
case Routes.didvanPlusVideo:
if (settings.arguments is DidvanPlusModel) {

View File

@ -1,4 +1,6 @@
import 'package:didvan/models/notification_message.dart';
import 'package:didvan/models/didvan_voice_model.dart';
import 'package:didvan/models/didvan_plus_model.dart';
import 'package:didvan/services/app_initalizer.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
@ -16,6 +18,29 @@ import '../network/request.dart';
import '../network/request_helper.dart';
class HomeWidgetRepository {
/// Helper method to safely parse ID from notification data
static int? _safeParseId(dynamic id) {
if (id == null) return null;
String idStr = id.toString().trim();
// Check if string contains "null" or is empty
if (idStr.isEmpty ||
idStr == 'null' ||
idStr.toLowerCase().contains('null')) {
return null;
}
try {
return int.parse(idStr);
} catch (e) {
if (kDebugMode) {
print("Failed to parse ID: $idStr - Error: $e");
}
return null;
}
}
static Future<void> fetchWidget() async {
// RequestService.token = await StorageService.getValue(key: 'token');
final service = RequestService(
@ -77,8 +102,17 @@ class HomeWidgetRepository {
if (row != 0) {
String? id =
await HomeWidget.getWidgetData<String>("id$row", defaultValue: "");
final parsedId = _safeParseId(id);
if (parsedId == null) {
if (kDebugMode) {
print("Invalid widget ID: $id - cannot create WidgetResponse");
}
return;
}
WidgetResponse data = WidgetResponse(
id: int.parse(id!),
id: parsedId,
title: await HomeWidget.getWidgetData("title$row", defaultValue: ""),
createdAt:
await HomeWidget.getWidgetData("createdAt$row", defaultValue: ""),
@ -200,16 +234,17 @@ class HomeWidgetRepository {
} else {
switch (localData.type!) {
case "infography":
if (localData.id == null || localData.id.toString().isEmpty) {
final infographyId = _safeParseId(localData.id);
if (infographyId == null) {
if (kDebugMode) {
print(
"WARNING: Infography notification without ID - navigating to home");
"WARNING: Infography notification without valid ID - navigating to home");
}
route = Routes.home;
} else {
route = Routes.infography;
args = {
'id': int.parse(localData.id.toString()),
'id': infographyId,
'args': const InfographyRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
'goToComment': openComments
@ -217,36 +252,38 @@ class HomeWidgetRepository {
}
break;
case "news":
if (localData.id == null || localData.id.toString().isEmpty) {
final newsId = _safeParseId(localData.id);
if (newsId == null) {
if (kDebugMode) {
print(
"WARNING: News notification without ID - navigating to home");
"WARNING: News notification without valid ID - navigating to home");
}
route = Routes.home;
} else {
route = Routes.newsDetails;
args = {
'id': int.parse(localData.id.toString()),
'id': newsId,
'args': const NewsRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
'goToComment': openComments
};
if (kDebugMode) {
print("News navigation - ID: ${localData.id}");
print("News navigation - ID: $newsId");
}
}
break;
case "radar":
if (localData.id == null || localData.id.toString().isEmpty) {
final radarId = _safeParseId(localData.id);
if (radarId == null) {
if (kDebugMode) {
print(
"WARNING: Radar notification without ID - navigating to home");
"WARNING: Radar notification without valid ID - navigating to home");
}
route = Routes.home;
} else {
route = Routes.radarDetails;
args = {
'id': int.parse(localData.id.toString()),
'id': radarId,
'args': const RadarRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
'goToComment': openComments
@ -254,49 +291,52 @@ class HomeWidgetRepository {
}
break;
case "studio":
if (localData.id == null || localData.id.toString().isEmpty) {
final studioId = _safeParseId(localData.id);
if (studioId == null) {
if (kDebugMode) {
print(
"WARNING: Studio notification without ID - navigating to home");
"WARNING: Studio notification without valid ID - navigating to home");
}
route = Routes.home;
} else {
route = Routes.studioDetails;
args = {
'type': 'podcast',
'id': int.parse(localData.id.toString()),
'id': studioId,
'goToComment': openComments
};
}
break;
case "video":
if (localData.id == null || localData.id.toString().isEmpty) {
final videoId = _safeParseId(localData.id);
if (videoId == null) {
if (kDebugMode) {
print(
"WARNING: Video notification without ID - navigating to home");
"WARNING: Video notification without valid ID - navigating to home");
}
route = Routes.home;
} else {
route = Routes.studioDetails;
args = {
'type': 'podcast',
'id': int.parse(localData.id.toString()),
'id': videoId,
'goToComment': openComments
};
}
break;
case "podcast":
if (localData.id == null || localData.id.toString().isEmpty) {
final podcastId = _safeParseId(localData.id);
if (podcastId == null) {
if (kDebugMode) {
print(
"WARNING: Podcast notification without ID - navigating to home");
"WARNING: Podcast notification without valid ID - navigating to home");
}
route = Routes.home;
} else {
route = Routes.podcasts;
args = {
'type': 'podcast',
'id': int.parse(localData.id.toString()),
'id': podcastId,
'goToComment': openComments
};
}
@ -308,14 +348,133 @@ class HomeWidgetRepository {
case "didvanplus":
case "didvan_plus":
route = Routes.didvanPlusList;
args = [];
case "didvanPlus":
if (kDebugMode) {
print(
"🎬 Fetching didvan plus list for notification navigation...");
}
try {
final service = RequestService(RequestHelper.didvanPlus);
await service.httpGet();
if (kDebugMode) {
print("🎬 DidvanPlus API Response - Status: ${service.statusCode}");
print("🎬 DidvanPlus API Response - Success: ${service.isSuccess}");
}
if (service.statusCode == 200) {
final rawData = service.data('result');
if (kDebugMode) {
print("🎬 Raw Data Type: ${rawData.runtimeType}");
print("🎬 Raw Data: $rawData");
}
List<DidvanPlusModel> videos = [];
if (rawData is List && rawData.isNotEmpty) {
videos = rawData
.map((e) => DidvanPlusModel.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList();
} else if (rawData is Map) {
videos = [
DidvanPlusModel.fromJson(
Map<String, dynamic>.from(rawData))
];
}
if (kDebugMode) {
print("🎬 Parsed ${videos.length} didvan plus videos");
for (var video in videos) {
print(" - ${video.title} (ID: ${video.id})");
}
}
if (videos.isNotEmpty) {
route = Routes.didvanPlusList;
args = videos;
if (kDebugMode) {
print(
"🎬 ✅ Successfully prepared navigation with ${videos.length} videos");
print("🎬 Args type: ${args.runtimeType}");
}
} else {
route = Routes.home;
if (kDebugMode) {
print("🎬 ⚠️ No didvan plus videos found, navigating to home");
}
}
} else {
route = Routes.home;
if (kDebugMode) {
print(
"🎬 ❌ Failed to fetch didvan plus (status: ${service.statusCode}), navigating to home");
}
}
} catch (e, stackTrace) {
route = Routes.home;
if (kDebugMode) {
print("🎬 ❌ Error fetching didvan plus: $e");
print("🎬 Stack trace: $stackTrace");
}
}
break;
case "didvanvoice":
case "didvan_voice":
route = Routes.didvanVoiceList;
args = [];
case "didvan_Voice":
case "didvanVoice":
case "media":
if (kDebugMode) {
print(
"Fetching didvan voice list for notification navigation...");
}
try {
final service = RequestService(RequestHelper.didvanVoice);
await service.httpGet();
if (service.statusCode == 200) {
final rawData = service.data('result');
List<DidvanVoiceModel> voices = [];
if (rawData is List && rawData.isNotEmpty) {
voices = rawData
.map((e) => DidvanVoiceModel.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList();
} else if (rawData is Map) {
voices = [
DidvanVoiceModel.fromJson(
Map<String, dynamic>.from(rawData))
];
}
if (voices.isNotEmpty) {
route = Routes.didvanVoiceList;
args = voices;
if (kDebugMode) {
print(
"Successfully fetched ${voices.length} didvan voices");
}
} else {
route = Routes.home;
if (kDebugMode) {
print("No didvan voices found, navigating to home");
}
}
} else {
route = Routes.home;
if (kDebugMode) {
print(
"Failed to fetch didvan voices (status: ${service.statusCode}), navigating to home");
}
}
} catch (e) {
route = Routes.home;
if (kDebugMode) {
print("Error fetching didvan voices: $e, navigating to home");
}
}
break;
case "startup":
case "technology":
@ -418,6 +577,10 @@ class HomeWidgetRepository {
if (kDebugMode) {
print("Final navigation decision:");
print("Route: $route");
print("Args type: ${args.runtimeType}");
if (args is List) {
print("Args list length: ${args.length}");
}
print("Args: $args");
print("===========================");
}
@ -443,6 +606,7 @@ class HomeWidgetRepository {
if (navigatorKey.currentState != null) {
if (kDebugMode) {
print("Navigator is ready, performing navigation to: $route");
print("With arguments type: ${args.runtimeType}");
}
navigatorKey.currentState!.pushNamed(route, arguments: args);
} else {

View File

@ -38,10 +38,10 @@ class MediaService {
try {
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
await session.configure(const AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
androidAudioAttributes: const AndroidAudioAttributes(
androidAudioAttributes: AndroidAudioAttributes(
contentType: AndroidAudioContentType.music,
flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.media,
@ -64,7 +64,6 @@ class MediaService {
void Function(bool isNext)? onTrackChanged,
}) async {
try {
// تنظیم AudioSession برای پخش از MEDIA
await _configureAudioSession();
String tag;
@ -193,85 +192,90 @@ class MediaService {
PermissionStatus status;
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
debugPrint("Android 13+ detected. Requesting Media permissions.");
// برای Android 13+، بسته به نوع فایل permission مناسب را درخواست میکنیم
final extension = basename.toLowerCase();
if (extension.contains('.mp3') || extension.contains('.wav') ||
extension.contains('.m4a') || extension.contains('.aac')) {
status = await Permission.audio.request();
} else if (extension.contains('.mp4') || extension.contains('.avi') ||
extension.contains('.mov')) {
status = await Permission.videos.request();
} else if (extension.contains('.jpg') || extension.contains('.jpeg') ||
extension.contains('.png') || extension.contains('.gif')) {
status = await Permission.photos.request();
} else {
// برای فایلهای دیگر، همه permissions رسانه را درخواست میکنیم
final audioStatus = await Permission.audio.request();
final videoStatus = await Permission.videos.request();
final photoStatus = await Permission.photos.request();
status = (audioStatus.isGranted || videoStatus.isGranted || photoStatus.isGranted)
? PermissionStatus.granted
: audioStatus;
}
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
debugPrint("Android 13+ detected. Requesting Media permissions.");
final extension = basename.toLowerCase();
if (extension.contains('.mp3') ||
extension.contains('.wav') ||
extension.contains('.m4a') ||
extension.contains('.aac')) {
status = await Permission.audio.request();
} else if (extension.contains('.mp4') ||
extension.contains('.avi') ||
extension.contains('.mov')) {
status = await Permission.videos.request();
} else if (extension.contains('.jpg') ||
extension.contains('.jpeg') ||
extension.contains('.png') ||
extension.contains('.gif')) {
status = await Permission.photos.request();
} else {
debugPrint("Older Android version detected. Requesting Storage permission.");
status = await Permission.storage.request();
final audioStatus = await Permission.audio.request();
final videoStatus = await Permission.videos.request();
final photoStatus = await Permission.photos.request();
status = (audioStatus.isGranted ||
videoStatus.isGranted ||
photoStatus.isGranted)
? PermissionStatus.granted
: audioStatus;
}
} else {
debugPrint(
"Older Android version detected. Requesting Storage permission.");
status = await Permission.storage.request();
}
} else if (Platform.isIOS) {
debugPrint("iOS detected. Requesting Photos permission.");
status = await Permission.photos.request();
debugPrint("iOS detected. Requesting Photos permission.");
status = await Permission.photos.request();
} else {
status = PermissionStatus.granted;
status = PermissionStatus.granted;
}
debugPrint("Permission status after request: $status");
if (status.isGranted) {
Directory? dir;
try {
if (Platform.isIOS) {
dir = await getApplicationDocumentsDirectory();
} else {
dir = await getExternalStorageDirectory();
}
if (dir == null) {
debugPrint("Error: Could not determine download directory.");
return null;
}
final path = dir.path;
debugPrint("Download directory determined: $path");
final taskId = await FlutterDownloader.enqueue(
url: url,
savedDir: path,
fileName: basename,
showNotification: true,
openFileFromNotification: true,
saveInPublicStorage: true,
);
debugPrint("Download successfully enqueued with taskId: $taskId");
return path;
} catch (e) {
debugPrint("Error during download process: $e");
return null;
Directory? dir;
try {
if (Platform.isIOS) {
dir = await getApplicationDocumentsDirectory();
} else {
dir = await getExternalStorageDirectory();
}
if (dir == null) {
debugPrint("Error: Could not determine download directory.");
return null;
}
final path = dir.path;
debugPrint("Download directory determined: $path");
final taskId = await FlutterDownloader.enqueue(
url: url,
savedDir: path,
fileName: basename,
showNotification: true,
openFileFromNotification: true,
saveInPublicStorage: true,
);
debugPrint("Download successfully enqueued with taskId: $taskId");
return path;
} catch (e) {
debugPrint("Error during download process: $e");
return null;
}
} else if (status.isPermanentlyDenied) {
debugPrint("Storage permission is permanently denied. Opening app settings.");
await openAppSettings();
return null;
debugPrint(
"Storage permission is permanently denied. Opening app settings.");
await openAppSettings();
return null;
} else {
debugPrint("Storage permission was denied.");
return null;
debugPrint("Storage permission was denied.");
return null;
}
}
static String downloadFileFromWeb(String url) {
final filename = url.split('/').last.split('?').first;
html.AnchorElement anchorElement = html.AnchorElement(href: url);

View File

@ -9,104 +9,86 @@ import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
// تعریف متغیر به صورت سراسری در کلاس حذف شد تا از کرش زودهنگام جلوگیری شود
static String? fcmToken;
Future<void> initNotification() async {
try {
if (kIsWeb) {
fcmToken = await _firebaseMessaging.getToken(
vapidKey:
"BMXHGd93t_htpS7c62ceuuLVVmia2cEDmqxp46g9Vt0B3OxNMKIqN9nupsUMtv2Vq8Yy2sQGIqgCm9FxUSKvssU",
);
} else {
fcmToken = await _firebaseMessaging.getToken();
// ۱. بررسی پشتیبانی مرورگر (بسیار مهم برای آیفون و سافاری)
if (kIsWeb) {
try {
// اول چک میکنیم پشتیبانی میشه یا نه
bool isSupported = await FirebaseMessaging.instance.isSupported();
if (!isSupported) {
if (kDebugMode) {
print("⚠️ مرورگر از نوتیفیکیشن پشتیبانی نمی‌کند. پروسه نوتیفیکیشن با موفقیت لغو شد تا از کرش جلوگیری شود.");
}
return; // خروج امن از تابع
}
} catch (e) {
if (kDebugMode) print("Error checking FCM support: $e");
return; // خروج در صورت بروز خطای ناشناخته
}
if (kDebugMode) {
print("fCMToken: $fcmToken");
}
} catch (e) {
e.printError();
}
_firebaseMessaging.requestPermission(
alert: true,
announcement: true,
badge: true,
carPlay: false,
criticalAlert: true,
provisional: true,
sound: true,
);
// ۲. حالا که مطمئن شدیم مرورگر پشتیبانی میکنه، فایربیس رو لود میکنیم
try {
final messaging = FirebaseMessaging.instance;
final initMsg = await FirebaseMessaging.instance.getInitialMessage();
NotificationSettings settings = await messaging.requestPermission(
alert: true,
announcement: true,
badge: true,
carPlay: false,
criticalAlert: true,
provisional: true,
sound: true,
);
if (initMsg != null) {
if (kDebugMode) {
print("=== APP OPENED FROM NOTIFICATION (TERMINATED) ===");
print("Message ID: ${initMsg.messageId}");
print("Raw Data: ${initMsg.data}");
if (initMsg.notification != null) {
print("Title: ${initMsg.notification!.title}");
print("Body: ${initMsg.notification!.body}");
if (settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional) {
if (kIsWeb) {
fcmToken = await messaging.getToken(
vapidKey: "BMXHGd93t_htpS7c62ceuuLVVmia2cEDmqxp46g9Vt0B3OxNMKIqN9nupsUMtv2Vq8Yy2sQGIqgCm9FxUSKvssU",
);
} else {
fcmToken = await messaging.getToken();
}
print("================================================");
}
try {
if (kDebugMode) {
print("fCMToken: $fcmToken");
}
} else {
if (kDebugMode) {
print("User declined or has not accepted notification permissions");
}
}
} catch (e) {
if (kDebugMode) print("Error initializing notifications: $e");
}
// ۳. کدهای مربوط به هندل کردن پیامها
try {
final initMsg = await FirebaseMessaging.instance.getInitialMessage();
if (initMsg != null) {
if (kDebugMode) print("=== APP OPENED FROM NOTIFICATION (TERMINATED) ===");
NotificationMessage data = NotificationMessage.fromJson(initMsg.data);
HomeWidgetRepository.data = data;
if (kDebugMode) {
print("Parsed NotificationMessage: ${data.toJson()}");
print("Scheduling navigation from terminated state...");
}
// Schedule navigation to happen after app is fully initialized
// This ensures navigatorKey is ready
// Future.delayed(const Duration(milliseconds: 1500), () async {
// if (kDebugMode) {
// print("Executing delayed navigation from terminated state");
// }
// await HomeWidgetRepository.decideWhereToGoNotif();
// await StorageService.delete(
// key: 'notification${AppInitializer.createNotificationId(data)}');
// });
} catch (e) {
if (kDebugMode) {
print("Error handling initial message: $e");
}
e.printError();
}
} catch (e) {
if (kDebugMode) print("Error handling initial message: $e");
}
FirebaseMessaging.onMessageOpenedApp.listen((initMsg) async {
if (kDebugMode) {
print("=== APP OPENED FROM NOTIFICATION (BACKGROUND) ===");
print("Message ID: ${initMsg.messageId}");
print("Raw Data: ${initMsg.data}");
if (initMsg.notification != null) {
print("Title: ${initMsg.notification!.title}");
print("Body: ${initMsg.notification!.body}");
}
print("================================================");
}
try {
NotificationMessage data = NotificationMessage.fromJson(initMsg.data);
HomeWidgetRepository.data = data;
if (kDebugMode) {
print("Parsed NotificationMessage: ${data.toJson()}");
print("Scheduling navigation from background state...");
}
await Future.delayed(const Duration(milliseconds: 300));
await HomeWidgetRepository.decideWhereToGoNotif();
await StorageService.delete(
key: 'notification${AppInitializer.createNotificationId(data)}');
} catch (e) {
if (kDebugMode) {
print("Error handling background message: $e");
}
e.printError();
if (kDebugMode) print("Error handling background message: $e");
}
});
@ -116,39 +98,10 @@ class FirebaseApi {
void handleMessage(RemoteMessage? message) async {
if (message == null) return;
if (kDebugMode) {
print("=== NOTIFICATION RECEIVED (FOREGROUND) ===");
print("Message ID: ${message.messageId}");
print("From: ${message.from}");
print("Sent Time: ${message.sentTime}");
print("TTL: ${message.ttl}");
print("Message Type: ${message.messageType}");
print("Category: ${message.category}");
if (message.notification != null) {
print("--- NOTIFICATION PAYLOAD ---");
print("Title: ${message.notification!.title}");
print("Body: ${message.notification!.body}");
print("Android Image: ${message.notification!.android?.imageUrl}");
print("Apple Image: ${message.notification!.apple?.imageUrl}");
}
print("--- DATA PAYLOAD ---");
print("Raw Data: ${message.data}");
try {
NotificationData notifData = NotificationData.fromJson(message.data);
print("Parsed Data: ${notifData.toJson()}");
} catch (e) {
print("Error parsing NotificationData: $e");
}
print("==========================================");
}
try {
await NotificationService.showFirebaseNotification(message);
} catch (e) {
e.printError();
if (kDebugMode) print("Error showing local notification: $e");
}
}
}

View File

@ -62,7 +62,7 @@ class DidvanPlusListPage extends StatelessWidget {
children: [
Center(
child: SvgPicture.asset(
'lib/assets /images/logos/logo-horizontal-light.svg',
'lib/assets/images/logos/logo-horizontal-light.svg',
height: 55,
),
),
@ -124,6 +124,13 @@ class DidvanPlusListPage extends StatelessWidget {
return GestureDetector(
onTap: () {
debugPrint('🎬 Opening video: ${item.title}');
debugPrint('🎬 Plus List - Original Image: ${item.image}');
debugPrint('🎬 Plus List - Original File: ${item.file}');
final bool imageHasHttp = item.image.startsWith('http');
final bool imageHasBaseUrl = item.image.contains(RequestHelper.baseUrl);
debugPrint('🎬 Plus List - Image has HTTP: $imageHasHttp, has BaseURL: $imageHasBaseUrl');
showDialog(
context: context,
useSafeArea: false,
@ -156,11 +163,19 @@ class DidvanPlusListPage extends StatelessWidget {
child: Stack(
fit: StackFit.expand,
children: [
SkeletonImage(
imageUrl: '${RequestHelper.baseUrl}${item.image}',
width: double.infinity,
height: double.infinity,
borderRadius: BorderRadius.circular(20),
Builder(
builder: (context) {
final imageUrl = item.image.startsWith('http') || item.image.contains(RequestHelper.baseUrl)
? item.image
: '${RequestHelper.baseUrl}${item.image}';
debugPrint('🖼️ Plus List - Final Image URL: $imageUrl');
return SkeletonImage(
imageUrl: imageUrl,
width: double.infinity,
height: double.infinity,
borderRadius: BorderRadius.circular(20),
);
},
),
Center(
child: Container(

View File

@ -29,17 +29,53 @@ class _DidvanPlusVideoPlayerState extends State<DidvanPlusVideoPlayer> {
}
void _initializeVideo() {
final videoUrl = '${RequestHelper.baseUrl}${widget.video.file}';
final fullUrl = '$videoUrl?accessToken=${RequestService.token}';
debugPrint('🎥 Didvan Plus Video - Original File: ${widget.video.file}');
debugPrint('🎥 Didvan Plus Video - Original Image: ${widget.video.image}');
debugPrint('🎥 Video URL: $fullUrl');
// بررسی اینکه آیا لینک از قبل کامل است یا نه
final bool fileHasHttp = widget.video.file.startsWith('http');
final bool fileHasBaseUrl = widget.video.file.contains(RequestHelper.baseUrl);
_videoController = VideoPlayerController.networkUrl(
Uri.parse(fullUrl),
httpHeaders: {
'Authorization': 'Bearer ${RequestService.token}',
},
)..initialize().then((_) {
debugPrint('🎥 File has HTTP: $fileHasHttp');
debugPrint('🎥 File has BaseURL: $fileHasBaseUrl');
// برای دیدوان پلاس، لینک مستقیم از سرور استفاده میشود
final videoUrl = widget.video.file.startsWith('http') || widget.video.file.contains(RequestHelper.baseUrl)
? widget.video.file
: '${RequestHelper.baseUrl}${widget.video.file}';
debugPrint('🎥 Video URL before token: $videoUrl');
// فقط به لینکهای baseUrl سرور accessToken اضافه میشود، نه لینکهای external مثل S3
final bool needsToken = videoUrl.contains(RequestHelper.baseUrl);
debugPrint('🎥 Needs access token: $needsToken');
final String fullUrl;
if (needsToken) {
final separator = videoUrl.contains('?') ? '&' : '?';
fullUrl = '$videoUrl${separator}accessToken=${RequestService.token}';
} else {
fullUrl = videoUrl; // لینکهای S3 نیازی به token ندارند
}
debugPrint('🎥 Didvan Plus Video - Final URL: $fullUrl');
// برای S3 signed URLs نباید Authorization header اضافه کنیم
if (needsToken) {
_videoController = VideoPlayerController.networkUrl(
Uri.parse(fullUrl),
httpHeaders: {
'Authorization': 'Bearer ${RequestService.token}',
},
);
} else {
// برای S3 بدون Authorization header
_videoController = VideoPlayerController.networkUrl(
Uri.parse(fullUrl),
);
}
_videoController.initialize().then((_) {
debugPrint('✅ Video initialized successfully');
setState(() {
_chewieController = ChewieController(
@ -64,7 +100,9 @@ class _DidvanPlusVideoPlayerState extends State<DidvanPlusVideoPlayer> {
color: Colors.black,
child: Center(
child: Image.network(
'${RequestHelper.baseUrl}${widget.video.image}',
widget.video.image.startsWith('http') || widget.video.image.contains(RequestHelper.baseUrl)
? widget.video.image
: '${RequestHelper.baseUrl}${widget.video.image}',
fit: BoxFit.cover,
),
),
@ -126,7 +164,9 @@ class _DidvanPlusVideoPlayerState extends State<DidvanPlusVideoPlayer> {
fit: StackFit.expand,
children: [
Image.network(
'${RequestHelper.baseUrl}${widget.video.image}',
widget.video.image.startsWith('http') || widget.video.image.contains(RequestHelper.baseUrl)
? widget.video.image
: '${RequestHelper.baseUrl}${widget.video.image}',
fit: BoxFit.fitWidth,
),
const Center(

View File

@ -133,7 +133,17 @@ class _DidvanVoiceListCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final imageUrl = '${RequestHelper.baseUrl}${voice.image}';
debugPrint('🎙️ Voice List - Original Image: ${voice.image}');
final bool imageHasHttp = voice.image.startsWith('http');
final bool imageHasBaseUrl = voice.image.contains(RequestHelper.baseUrl);
debugPrint('🎙️ Voice List - Image has HTTP: $imageHasHttp, has BaseURL: $imageHasBaseUrl');
final imageUrl = voice.image.startsWith('http') || voice.image.contains(RequestHelper.baseUrl)
? voice.image
: '${RequestHelper.baseUrl}${voice.image}';
debugPrint('🖼️ یک لقمه استراتژی List - Image URL: $imageUrl');
return GestureDetector(
onTap: onTap,

View File

@ -36,21 +36,56 @@ class _DidvanPlusSectionState extends State<DidvanPlusSection> {
}
void _initializeVideo() {
final videoUrl = '${RequestHelper.baseUrl}${widget.didvanPlus.file}';
final fullUrl = '$videoUrl?accessToken=${RequestService.token}';
debugPrint('🎥 Didvan Plus Section - Original File: ${widget.didvanPlus.file}');
debugPrint('🎥 Didvan Plus Section - Original Image: ${widget.didvanPlus.image}');
debugPrint('🎥 Didvan Plus Video URL: $fullUrl');
debugPrint('🎥 Video file path: ${widget.didvanPlus.file}');
// بررسی اینکه آیا لینک از قبل کامل است یا نه
final bool fileHasHttp = widget.didvanPlus.file.startsWith('http');
final bool fileHasBaseUrl = widget.didvanPlus.file.contains(RequestHelper.baseUrl);
debugPrint('🎥 File has HTTP: $fileHasHttp');
debugPrint('🎥 File has BaseURL: $fileHasBaseUrl');
// برای دیدوان پلاس، لینک مستقیم از سرور استفاده میشود
final videoUrl = widget.didvanPlus.file.startsWith('http') || widget.didvanPlus.file.contains(RequestHelper.baseUrl)
? widget.didvanPlus.file
: '${RequestHelper.baseUrl}${widget.didvanPlus.file}';
debugPrint('🎥 Video URL before token: $videoUrl');
// فقط به لینکهای baseUrl سرور accessToken اضافه میشود، نه لینکهای external مثل S3
final bool needsToken = videoUrl.contains(RequestHelper.baseUrl);
debugPrint('🎥 Needs access token: $needsToken');
final String fullUrl;
if (needsToken) {
final separator = videoUrl.contains('?') ? '&' : '?';
fullUrl = '$videoUrl${separator}accessToken=${RequestService.token}';
} else {
fullUrl = videoUrl; // لینکهای S3 نیازی به token ندارند
}
debugPrint('🎥 Didvan Plus Section - Final URL: $fullUrl');
debugPrint('🎥 Base URL: ${RequestHelper.baseUrl}');
debugPrint(
'🎥 Token exists: ${RequestService.token != null && RequestService.token!.isNotEmpty}');
_videoController = VideoPlayerController.networkUrl(
Uri.parse(fullUrl),
httpHeaders: {
'Authorization': 'Bearer ${RequestService.token}',
},
)..initialize().then((_) {
// برای S3 signed URLs نباید Authorization header اضافه کنیم
if (needsToken) {
_videoController = VideoPlayerController.networkUrl(
Uri.parse(fullUrl),
httpHeaders: {
'Authorization': 'Bearer ${RequestService.token}',
},
);
} else {
// برای S3 بدون Authorization header
_videoController = VideoPlayerController.networkUrl(
Uri.parse(fullUrl),
);
}
_videoController.initialize().then((_) {
debugPrint('✅ Video initialized successfully');
setState(() {
_chewieController = ChewieController(
@ -75,7 +110,9 @@ class _DidvanPlusSectionState extends State<DidvanPlusSection> {
color: Colors.black,
child: Center(
child: Image.network(
'${RequestHelper.baseUrl}${widget.didvanPlus.image}',
widget.didvanPlus.image.startsWith('http') || widget.didvanPlus.image.contains(RequestHelper.baseUrl)
? widget.didvanPlus.image
: '${RequestHelper.baseUrl}${widget.didvanPlus.image}',
fit: BoxFit.cover,
),
),
@ -95,8 +132,12 @@ class _DidvanPlusSectionState extends State<DidvanPlusSection> {
super.dispose();
}
@override
@override
Widget build(BuildContext context) {
debugPrint('🎬 Plus Section Build - Original File: ${widget.didvanPlus.file}');
debugPrint('🎬 Plus Section Build - Original Image: ${widget.didvanPlus.image}');
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
@ -161,7 +202,9 @@ class _DidvanPlusSectionState extends State<DidvanPlusSection> {
color: Colors.black,
child: Center(
child: Image.network(
'${RequestHelper.baseUrl}${widget.didvanPlus.image}',
widget.didvanPlus.image.startsWith('http') || widget.didvanPlus.image.contains(RequestHelper.baseUrl)
? widget.didvanPlus.image
: '${RequestHelper.baseUrl}${widget.didvanPlus.image}',
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;

View File

@ -24,7 +24,6 @@ class DidvanVoiceDetailCard extends StatefulWidget {
class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
late AudioPlayer _audioPlayer;
bool _isInitialized = false;
@override
void initState() {
@ -34,22 +33,40 @@ class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
void _initializeAudio() async {
_audioPlayer = VoiceService.audioPlayer;
// برای یک لقمه استراتژی، لینک مستقیم از سرور استفاده میشود
final audioUrl = widget.didvanVoice.file.startsWith('http')
? widget.didvanVoice.file
: '${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}';
debugPrint('🎙️ Didvan Voice Audio URL: $audioUrl');
debugPrint('🎙️ Detail - Original File: ${widget.didvanVoice.file}');
// برای یک لقمه استراتژی، لینک مستقیم از سرور استفاده میشود
final audioFileUrl = widget.didvanVoice.file.startsWith('http') || widget.didvanVoice.file.contains(RequestHelper.baseUrl)
? widget.didvanVoice.file
: '${RequestHelper.baseUrl}${widget.didvanVoice.file}';
debugPrint('🎙️ Audio URL before token: $audioFileUrl');
// فقط به لینکهای baseUrl سرور accessToken اضافه میشود، نه لینکهای external مثل S3
final bool needsToken = audioFileUrl.contains(RequestHelper.baseUrl);
debugPrint('🎙️ Needs access token: $needsToken');
final String audioUrl;
if (needsToken) {
final separator = audioFileUrl.contains('?') ? '&' : '?';
audioUrl = '$audioFileUrl${separator}accessToken=${RequestService.token}';
} else {
audioUrl = audioFileUrl; // لینکهای S3 نیازی به token ندارند
}
debugPrint('🎙️ یک لقمه استراتژی Detail - Final Audio URL: $audioUrl');
debugPrint('🎙️ Detail - Original Image: ${widget.didvanVoice.image}');
final bool fileHasHttp = widget.didvanVoice.file.startsWith('http');
final bool fileHasBaseUrl = widget.didvanVoice.file.contains(RequestHelper.baseUrl);
debugPrint('🎙️ Detail - File has HTTP: $fileHasHttp, has BaseURL: $fileHasBaseUrl');
debugPrint('🎙️ یک لقمه استراتژی Detail - Final Audio URL: $audioUrl');
try {
VoiceService.src = audioUrl;
await _audioPlayer.setUrl(audioUrl);
if (mounted) {
setState(() {
_isInitialized = true;
});
}
} catch (e) {
debugPrint('❌ Audio initialization error: $e');
}
@ -70,7 +87,17 @@ class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
@override
Widget build(BuildContext context) {
final imageUrl = '${RequestHelper.baseUrl}${widget.didvanVoice.image}';
debugPrint('🎙️ Detail Build - Original Image: ${widget.didvanVoice.image}');
final bool imageHasHttp = widget.didvanVoice.image.startsWith('http');
final bool imageHasBaseUrl = widget.didvanVoice.image.contains(RequestHelper.baseUrl);
debugPrint('🎙️ Detail Build - Image has HTTP: $imageHasHttp, has BaseURL: $imageHasBaseUrl');
final imageUrl = widget.didvanVoice.image.startsWith('http') || widget.didvanVoice.image.contains(RequestHelper.baseUrl)
? widget.didvanVoice.image
: '${RequestHelper.baseUrl}${widget.didvanVoice.image}';
debugPrint('🖼️ یک لقمه استراتژی Detail - Image URL: $imageUrl');
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),

View File

@ -40,11 +40,38 @@ class _DidvanVoiceSectionState extends State<DidvanVoiceSection> {
@override
Widget build(BuildContext context) {
final imageUrl = '${RequestHelper.baseUrl}${widget.didvanVoice.image}';
debugPrint('🎙️ Original File: ${widget.didvanVoice.file}');
debugPrint('🎙️ Original Image: ${widget.didvanVoice.image}');
final bool fileHasHttp = widget.didvanVoice.file.startsWith('http');
final bool fileHasBaseUrl = widget.didvanVoice.file.contains(RequestHelper.baseUrl);
debugPrint('🎙️ File has HTTP: $fileHasHttp, has BaseURL: $fileHasBaseUrl');
final imageUrl = widget.didvanVoice.image.startsWith('http') || widget.didvanVoice.image.contains(RequestHelper.baseUrl)
? widget.didvanVoice.image
: '${RequestHelper.baseUrl}${widget.didvanVoice.image}';
// برای یک لقمه استراتژی، لینک مستقیم از سرور استفاده میشود
final audioUrl = widget.didvanVoice.file.startsWith('http')
final audioFileUrl = widget.didvanVoice.file.startsWith('http') || widget.didvanVoice.file.contains(RequestHelper.baseUrl)
? widget.didvanVoice.file
: '${RequestHelper.baseUrl}${widget.didvanVoice.file}?accessToken=${RequestService.token}';
: '${RequestHelper.baseUrl}${widget.didvanVoice.file}';
debugPrint('🎙️ Audio URL before token: $audioFileUrl');
// فقط به لینکهای baseUrl سرور accessToken اضافه میشود، نه لینکهای external مثل S3
final bool needsToken = audioFileUrl.contains(RequestHelper.baseUrl);
debugPrint('🎙️ Needs access token: $needsToken');
final String audioUrl;
if (needsToken) {
final separator = audioFileUrl.contains('?') ? '&' : '?';
audioUrl = '$audioFileUrl${separator}accessToken=${RequestService.token}';
} else {
audioUrl = audioFileUrl; // لینکهای S3 نیازی به token ندارند
}
debugPrint('🎙️ یک لقمه استراتژی - Image URL: $imageUrl');
debugPrint('🎙️ یک لقمه استراتژی - Audio URL: $audioUrl');
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),

View File

@ -7,6 +7,7 @@ import 'package:didvan/models/ai/ai_chat_args.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
@ -308,13 +309,34 @@ class _HistoryDrawerContentState extends State<HistoryDrawerContent> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
// Calculate responsive width:
// For web: use min of 400px or 30% of screen width
// For mobile: use 75% of screen width
double drawerWidth;
if (kIsWeb) {
if (screenWidth > 1200) {
// Desktop: fixed width of 400px
drawerWidth = 400;
} else if (screenWidth > 768) {
// Tablet: 40% of screen width
drawerWidth = screenWidth * 0.4;
} else {
// Mobile web: 75% of screen width
drawerWidth = screenWidth * 0.75;
}
} else {
// Native mobile: 75% of screen width
drawerWidth = screenWidth * 0.75;
}
return Align(
alignment: Alignment.centerLeft,
child: Material(
color: Colors.transparent,
child: Container(
width: MediaQuery.of(context).size.width * 0.75,
width: drawerWidth,
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
color: DesignConfig.isDark

View File

@ -49,7 +49,7 @@ class SkeletonImage extends StatelessWidget {
httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'},
width: width,
height: height,
imageUrl: imageUrl.startsWith('http')
imageUrl: imageUrl.startsWith('http') || imageUrl.contains(RequestHelper.baseUrl)
? imageUrl.replaceAll('\n', '')
: RequestHelper.baseUrl + imageUrl.replaceAll('\n', ''),
placeholder: (context, _) => ShimmerPlaceholder(

View File

@ -22,6 +22,7 @@ class _PrimaryControlsState extends State<PrimaryControls> {
bool isAnimating = false;
bool isAnimatingForward = false;
bool isAnimatingBackward = false;
bool _isLocallyPlaying = false;
// bool isSpeedMenuOpen = false;
double opacity = 1;
@ -35,8 +36,18 @@ class _PrimaryControlsState extends State<PrimaryControls> {
chewieController.videoPlayerController.addListener(
() {
position.value = chewieController.videoPlayerController.value.position;
if (mounted) {
final isPlaying =
chewieController.videoPlayerController.value.isPlaying;
if (_isLocallyPlaying != isPlaying) {
setState(() {
_isLocallyPlaying = isPlaying;
});
}
}
},
);
_isLocallyPlaying = chewieController.videoPlayerController.value.isPlaying;
}
void _startHideControlsTimer() {
@ -54,23 +65,25 @@ class _PrimaryControlsState extends State<PrimaryControls> {
@override
void dispose() {
_hideControlsTimer?.cancel(); // Clean up the timer
_hideControlsTimer?.cancel();
super.dispose();
}
void _handlePlay() {
{
setState(() {
if (chewieController.isPlaying) {
if (_isLocallyPlaying) {
chewieController.pause();
_isLocallyPlaying = false;
opacity = 1;
} else {
chewieController.play();
_isLocallyPlaying = true;
opacity = 0;
}
isAnimating = true;
});
_startHideControlsTimer(); // Restart the timer on tap
_startHideControlsTimer();
}
}
@ -79,7 +92,7 @@ class _PrimaryControlsState extends State<PrimaryControls> {
setState(() {
opacity = 1;
});
_startHideControlsTimer(); // Restart the timer on tap
_startHideControlsTimer();
} else {
setState(() {
opacity = 0;
@ -171,7 +184,7 @@ class _PrimaryControlsState extends State<PrimaryControls> {
shape: BoxShape.circle,
color: Colors.black.withValues(alpha: 0.4)),
child: Icon(
chewieController.isPlaying
_isLocallyPlaying
? CupertinoIcons.pause_fill
: CupertinoIcons.play_fill,
color: Colors.white,
@ -352,7 +365,7 @@ class _PrimaryControlsState extends State<PrimaryControls> {
chewieController.videoPlayerController.value.duration;
if (duration.inSeconds == 0) {
return const SizedBox(); // Ya namayesh-e yek loading bar
return const SizedBox();
}
double maxValue = duration.inMilliseconds.toDouble();
@ -364,12 +377,12 @@ class _PrimaryControlsState extends State<PrimaryControls> {
data: SliderThemeData(
trackHeight: 2,
overlayShape: SliderComponentShape.noOverlay,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 8)),
thumbShape:
const RoundSliderThumbShape(enabledThumbRadius: 8)),
child: Slider(
min: 0,
max: maxValue,
value: currentValue.clamp(0.0, maxValue), // Estefade az clamp
value: currentValue.clamp(0.0, maxValue),
onChanged: (value) async {
await chewieController.pause();
position.value = Duration(milliseconds: value.round());
@ -412,7 +425,7 @@ class _PrimaryControlsState extends State<PrimaryControls> {
child: InkWell(
onTap: () => setState(() {
chewieController.toggleFullScreen();
_startHideControlsTimer(); // Restart the timer on tap
_startHideControlsTimer();
}),
child: Icon(
chewieController.isFullScreen

View File

@ -0,0 +1,10 @@
// تنظیمات Web Renderer برای جلوگیری از استفاده از CanvasKit و Google CDN
import 'package:flutter/foundation.dart';
void configureWebRenderer() {
if (kIsWeb) {
// این تنظیمات در زمان build به flutter_bootstrap.js منتقل میشوند
// فایل flutter_bootstrap.js را ویرایش کردهایم تا از HTML renderer استفاده کند
}
}

View File

@ -3,8 +3,8 @@ FLUTTER_ROOT=C:\flutter
FLUTTER_APPLICATION_PATH=C:\Flutter Projects\didvan-app\didvan-app
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=5.0.1
FLUTTER_BUILD_NUMBER=7007
FLUTTER_BUILD_NAME=5.1.0
FLUTTER_BUILD_NUMBER=7008
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false

View File

@ -4,8 +4,8 @@ export "FLUTTER_ROOT=C:\flutter"
export "FLUTTER_APPLICATION_PATH=C:\Flutter Projects\didvan-app\didvan-app"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=5.0.1"
export "FLUTTER_BUILD_NUMBER=7007"
export "FLUTTER_BUILD_NAME=5.1.0"
export "FLUTTER_BUILD_NUMBER=7008"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"

View File

@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 5.0.1+7007
version: 5.1.0+8008
environment:
sdk: ">=3.0.0 <4.0.0"

View File

@ -1,54 +1,11 @@
{{flutter_js}}
{{flutter_build_config}}
// Override buildConfig to force local CanvasKit usage
if (window._flutter && window._flutter.buildConfig) {
// Remove engineRevision to prevent CDN usage
delete window._flutter.buildConfig.engineRevision;
// Add useLocalCanvasKit flag
window._flutter.buildConfig.useLocalCanvasKit = true;
}
// Override the internal canvaskit path resolution function
if (window._flutter && window._flutter.loader) {
const originalLoad = window._flutter.loader.load;
window._flutter.loader.load = function(options) {
options = options || {};
options.config = options.config || {};
// Force local CanvasKit
options.config.canvasKitBaseUrl = "/canvaskit/";
// Disable Google Fonts
options.config.canvasKitForceCpuOnly = false;
return originalLoad.call(this, options);
};
}
// Configure Flutter to use local CanvasKit files instead of CDN
_flutter.loader.load({
config: {
// Use local CanvasKit files to avoid issues with filtered domains
canvasKitBaseUrl: "/canvaskit/",
// Don't fetch from Google CDN
canvasKitForceCpuOnly: false,
},
onEntrypointLoaded: async function(engineInitializer) {
try {
let appRunner = await engineInitializer.initializeEngine();
await appRunner.runApp();
} catch (error) {
console.error('Failed to initialize Flutter app:', error);
// Fallback: Try with CPU-only rendering if WebGL fails
try {
let appRunner = await engineInitializer.initializeEngine({
renderer: "html",
});
await appRunner.runApp();
} catch (fallbackError) {
console.error('Fallback initialization also failed:', fallbackError);
// Show a user-friendly error message
document.body.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:Arial"><div style="text-align:center;"><h2>مشکل در بارگذاری برنامه</h2><p>لطفا اتصال اینترنت خود را بررسی کنید و صفحه را مجددا بارگذاری کنید.</p><button onclick="location.reload()" style="padding:10px 20px;font-size:16px;cursor:pointer;">تلاش مجدد</button></div></div>';
}
}
const appRunner = await engineInitializer.initializeEngine({
renderer: "html",
});
await appRunner.runApp();
}
});

View File

@ -1,130 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<base href="$FLUTTER_BASE_HREF" />
<head>
<base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project." />
<meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project." />
<meta http-equiv="Content-Security-Policy" content="font-src 'self' data:; style-src 'self' 'unsafe-inline';">
<!-- Block Google Fonts and gstatic.com to prevent requests -->
<meta http-equiv="Content-Security-Policy" content="font-src 'self' data:; style-src 'self' 'unsafe-inline';">
<!-- تنظیم renderer به HTML برای جلوگیری از دانلود CanvasKit از Google CDN -->
<meta name="flutter-renderer" content="html" />
<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-title" content="didvan" />
<link rel="apple-touch-icon" href="icons/icon.jpg" />
<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-title" content="didvan" />
<link rel="apple-touch-icon" href="icons/icon.jpg" />
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="icon" type="image/png" href="favicon.png" />
<script src="flutter_bootstrap.js" async>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('firebase-messaging-sw.js', {
scope: '/firebase-cloud-messaging-push-scope',
});
<title>Didvan</title>
<link rel="manifest" href="manifest.json" />
<script src="flutter_bootstrap.js" async></script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('firebase-messaging-sw.js', {
scope: '/firebase-cloud-messaging-push-scope',
});
}
</script>
});
}
</script>
<title>Didvan</title>
<link rel="manifest" href="manifest.json" />
<style>
/* Override Roboto font to prevent Google Fonts loading */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: local('Arial'), local('Tahoma'), local('Helvetica');
}
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, Arial, sans-serif;
}
.container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
}
.indicator {
width: 50vw;
}
@media only screen and (min-width: 600px) {
.indicator {
width: 50vw;
width: 25vw;
}
}
</style>
</head>
@media only screen and (min-width: 600px) {
.indicator {
width: 25vw;
}
}
</style>
</head>
<body style="overflow: hidden">
<div id="loading_indicator" class="container">
<img class="indicator" src="./assets/lib/assets/animations/loading.gif" alt="Loading..." />
</div>
<body style="overflow: hidden">
<div id="loading_indicator" class="container">
<img class="indicator" src="./assets/lib/assets/animations/loading.gif" />
</div>
<script>
var serviceWorkerVersion = "{{flutter_service_worker_version}}";
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement("script");
scriptTag.src = `main.dart.js?version=${Math.random()}`;
scriptTag.type = "application/javascript";
document.body.append(scriptTag);
}
if ("serviceWorker" in navigator) {
// Service workers are supported. Use them.
window.addEventListener("load", function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl =
"flutter_service_worker.js?v=" + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener("statechange", () => {
if (serviceWorker.state == "activated") {
console.log("Installed new service worker.");
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing || reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log("New service worker available.");
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log("Loading app from service worker.");
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
"Failed to load app from service worker. Falling back to plain <script> tag."
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
</script>
</body>
</html>