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/material.dart';
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:home_widget/home_widget.dart'; import 'package:home_widget/home_widget.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.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/config/theme_data.dart';
import 'package:didvan/firebase_options.dart'; import 'package:didvan/firebase_options.dart';
import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/news.dart';
@ -55,22 +54,20 @@ void main() async {
final loader = html.document.getElementById('loading_indicator'); final loader = html.document.getElementById('loading_indicator');
if (loader != null) { if (loader != null) {
loader.remove(); loader.remove();
} }
} }
try { try {
if (!kIsWeb) { if (!kIsWeb) {
// ignore: deprecated_member_use
HomeWidget.registerBackgroundCallback(_backgroundCallbackHomeWidget); HomeWidget.registerBackgroundCallback(_backgroundCallbackHomeWidget);
HomeWidget.registerInteractivityCallback( HomeWidget.registerInteractivityCallback(_backgroundCallbackHomeWidget);
_backgroundCallbackHomeWidget);
await NotificationService.initializeNotification(); await NotificationService.initializeNotification();
if (Platform.isAndroid) { if (Platform.isAndroid) {
try { try {
await FlutterDownloader.initialize(debug: true, ignoreSsl: true); await FlutterDownloader.initialize(debug: true, ignoreSsl: true);
} catch (e) { } catch (e) {
e.printError(); if (kDebugMode) print("Downloader error: $e");
} }
} }
} }
@ -80,36 +77,29 @@ void main() async {
.timeout( .timeout(
const Duration(seconds: 10), const Duration(seconds: 10),
onTimeout: () { onTimeout: () {
debugPrint( debugPrint("Firebase initialization timed out - continuing without Firebase");
"Firebase initialization timed out - continuing without Firebase"); throw TimeoutException('Firebase initialization timeout', const Duration(seconds: 10));
throw TimeoutException(
'Firebase initialization timeout', const Duration(seconds: 10));
}, },
); );
if (!kIsWeb) {
await FirebaseApi().initNotification().timeout(
const Duration(seconds: 5),
onTimeout: () {
debugPrint("Firebase notification init timed out - continuing");
},
);
}
await FirebaseApi().initNotification().timeout(
const Duration(seconds: 5),
onTimeout: () {
debugPrint("Firebase notification init timed out - continuing");
},
);
} catch (e) { } catch (e) {
debugPrint("Initialization Error: $e"); debugPrint("Initialization Error: $e");
debugPrint("App will continue without Firebase services"); debugPrint("App will continue without Firebase services");
} }
await SentryFlutter.init( runApp(const Didvan());
(options) {
options.dsn =
'https://a4cfcaa7d67471240d295c25c968d91d@o4508585857384448.ingest.de.sentry.io/4508585886548048';
options.tracesSampleRate = 1.0;
options.profilesSampleRate = 1.0;
},
appRunner: () => runApp(const Didvan()),
);
}, },
(error, stack) { (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: case Routes.didvanPlusList:
if (kDebugMode) {
print("📱 Route: didvanPlusList");
print("📱 Arguments type: ${settings.arguments.runtimeType}");
print("📱 Arguments: ${settings.arguments}");
}
if (settings.arguments is List) { 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( return _createRoute(
DidvanPlusListPage( 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( return _errorRoute(
'Invalid arguments for ${settings.name}: Expected List.'); 'Invalid arguments for ${settings.name}: Expected List, got ${settings.arguments.runtimeType}.');
case Routes.didvanPlusVideo: case Routes.didvanPlusVideo:
if (settings.arguments is DidvanPlusModel) { if (settings.arguments is DidvanPlusModel) {

View File

@ -1,4 +1,6 @@
import 'package:didvan/models/notification_message.dart'; 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:didvan/services/app_initalizer.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -16,6 +18,29 @@ import '../network/request.dart';
import '../network/request_helper.dart'; import '../network/request_helper.dart';
class HomeWidgetRepository { 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 { static Future<void> fetchWidget() async {
// RequestService.token = await StorageService.getValue(key: 'token'); // RequestService.token = await StorageService.getValue(key: 'token');
final service = RequestService( final service = RequestService(
@ -77,8 +102,17 @@ class HomeWidgetRepository {
if (row != 0) { if (row != 0) {
String? id = String? id =
await HomeWidget.getWidgetData<String>("id$row", defaultValue: ""); 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( WidgetResponse data = WidgetResponse(
id: int.parse(id!), id: parsedId,
title: await HomeWidget.getWidgetData("title$row", defaultValue: ""), title: await HomeWidget.getWidgetData("title$row", defaultValue: ""),
createdAt: createdAt:
await HomeWidget.getWidgetData("createdAt$row", defaultValue: ""), await HomeWidget.getWidgetData("createdAt$row", defaultValue: ""),
@ -200,16 +234,17 @@ class HomeWidgetRepository {
} else { } else {
switch (localData.type!) { switch (localData.type!) {
case "infography": case "infography":
if (localData.id == null || localData.id.toString().isEmpty) { final infographyId = _safeParseId(localData.id);
if (infographyId == null) {
if (kDebugMode) { if (kDebugMode) {
print( print(
"WARNING: Infography notification without ID - navigating to home"); "WARNING: Infography notification without valid ID - navigating to home");
} }
route = Routes.home; route = Routes.home;
} else { } else {
route = Routes.infography; route = Routes.infography;
args = { args = {
'id': int.parse(localData.id.toString()), 'id': infographyId,
'args': const InfographyRequestArgs(page: 0), 'args': const InfographyRequestArgs(page: 0),
'hasUnmarkConfirmation': false, 'hasUnmarkConfirmation': false,
'goToComment': openComments 'goToComment': openComments
@ -217,36 +252,38 @@ class HomeWidgetRepository {
} }
break; break;
case "news": case "news":
if (localData.id == null || localData.id.toString().isEmpty) { final newsId = _safeParseId(localData.id);
if (newsId == null) {
if (kDebugMode) { if (kDebugMode) {
print( print(
"WARNING: News notification without ID - navigating to home"); "WARNING: News notification without valid ID - navigating to home");
} }
route = Routes.home; route = Routes.home;
} else { } else {
route = Routes.newsDetails; route = Routes.newsDetails;
args = { args = {
'id': int.parse(localData.id.toString()), 'id': newsId,
'args': const NewsRequestArgs(page: 0), 'args': const NewsRequestArgs(page: 0),
'hasUnmarkConfirmation': false, 'hasUnmarkConfirmation': false,
'goToComment': openComments 'goToComment': openComments
}; };
if (kDebugMode) { if (kDebugMode) {
print("News navigation - ID: ${localData.id}"); print("News navigation - ID: $newsId");
} }
} }
break; break;
case "radar": case "radar":
if (localData.id == null || localData.id.toString().isEmpty) { final radarId = _safeParseId(localData.id);
if (radarId == null) {
if (kDebugMode) { if (kDebugMode) {
print( print(
"WARNING: Radar notification without ID - navigating to home"); "WARNING: Radar notification without valid ID - navigating to home");
} }
route = Routes.home; route = Routes.home;
} else { } else {
route = Routes.radarDetails; route = Routes.radarDetails;
args = { args = {
'id': int.parse(localData.id.toString()), 'id': radarId,
'args': const RadarRequestArgs(page: 0), 'args': const RadarRequestArgs(page: 0),
'hasUnmarkConfirmation': false, 'hasUnmarkConfirmation': false,
'goToComment': openComments 'goToComment': openComments
@ -254,49 +291,52 @@ class HomeWidgetRepository {
} }
break; break;
case "studio": case "studio":
if (localData.id == null || localData.id.toString().isEmpty) { final studioId = _safeParseId(localData.id);
if (studioId == null) {
if (kDebugMode) { if (kDebugMode) {
print( print(
"WARNING: Studio notification without ID - navigating to home"); "WARNING: Studio notification without valid ID - navigating to home");
} }
route = Routes.home; route = Routes.home;
} else { } else {
route = Routes.studioDetails; route = Routes.studioDetails;
args = { args = {
'type': 'podcast', 'type': 'podcast',
'id': int.parse(localData.id.toString()), 'id': studioId,
'goToComment': openComments 'goToComment': openComments
}; };
} }
break; break;
case "video": case "video":
if (localData.id == null || localData.id.toString().isEmpty) { final videoId = _safeParseId(localData.id);
if (videoId == null) {
if (kDebugMode) { if (kDebugMode) {
print( print(
"WARNING: Video notification without ID - navigating to home"); "WARNING: Video notification without valid ID - navigating to home");
} }
route = Routes.home; route = Routes.home;
} else { } else {
route = Routes.studioDetails; route = Routes.studioDetails;
args = { args = {
'type': 'podcast', 'type': 'podcast',
'id': int.parse(localData.id.toString()), 'id': videoId,
'goToComment': openComments 'goToComment': openComments
}; };
} }
break; break;
case "podcast": case "podcast":
if (localData.id == null || localData.id.toString().isEmpty) { final podcastId = _safeParseId(localData.id);
if (podcastId == null) {
if (kDebugMode) { if (kDebugMode) {
print( print(
"WARNING: Podcast notification without ID - navigating to home"); "WARNING: Podcast notification without valid ID - navigating to home");
} }
route = Routes.home; route = Routes.home;
} else { } else {
route = Routes.podcasts; route = Routes.podcasts;
args = { args = {
'type': 'podcast', 'type': 'podcast',
'id': int.parse(localData.id.toString()), 'id': podcastId,
'goToComment': openComments 'goToComment': openComments
}; };
} }
@ -308,14 +348,133 @@ class HomeWidgetRepository {
case "didvanplus": case "didvanplus":
case "didvan_plus": case "didvan_plus":
route = Routes.didvanPlusList; case "didvanPlus":
args = []; 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; break;
case "didvanvoice": case "didvanvoice":
case "didvan_voice": case "didvan_voice":
route = Routes.didvanVoiceList; case "didvan_Voice":
args = []; 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; break;
case "startup": case "startup":
case "technology": case "technology":
@ -418,6 +577,10 @@ class HomeWidgetRepository {
if (kDebugMode) { if (kDebugMode) {
print("Final navigation decision:"); print("Final navigation decision:");
print("Route: $route"); print("Route: $route");
print("Args type: ${args.runtimeType}");
if (args is List) {
print("Args list length: ${args.length}");
}
print("Args: $args"); print("Args: $args");
print("==========================="); print("===========================");
} }
@ -443,6 +606,7 @@ class HomeWidgetRepository {
if (navigatorKey.currentState != null) { if (navigatorKey.currentState != null) {
if (kDebugMode) { if (kDebugMode) {
print("Navigator is ready, performing navigation to: $route"); print("Navigator is ready, performing navigation to: $route");
print("With arguments type: ${args.runtimeType}");
} }
navigatorKey.currentState!.pushNamed(route, arguments: args); navigatorKey.currentState!.pushNamed(route, arguments: args);
} else { } else {

View File

@ -35,13 +35,13 @@ class MediaService {
static Future<void> _configureAudioSession() async { static Future<void> _configureAudioSession() async {
if (_isAudioSessionConfigured) return; if (_isAudioSessionConfigured) return;
try { try {
final session = await AudioSession.instance; final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration( await session.configure(const AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback, avAudioSessionCategory: AVAudioSessionCategory.playback,
avAudioSessionMode: AVAudioSessionMode.spokenAudio, avAudioSessionMode: AVAudioSessionMode.spokenAudio,
androidAudioAttributes: const AndroidAudioAttributes( androidAudioAttributes: AndroidAudioAttributes(
contentType: AndroidAudioContentType.music, contentType: AndroidAudioContentType.music,
flags: AndroidAudioFlags.none, flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.media, usage: AndroidAudioUsage.media,
@ -64,9 +64,8 @@ class MediaService {
void Function(bool isNext)? onTrackChanged, void Function(bool isNext)? onTrackChanged,
}) async { }) async {
try { try {
// تنظیم AudioSession برای پخش از MEDIA
await _configureAudioSession(); await _configureAudioSession();
String tag; String tag;
tag = tag =
'${currentPodcast?.description == 'radar' ? 'radar' : isVoiceMessage ? 'message' : 'podcast'}-$id'; '${currentPodcast?.description == 'radar' ? 'radar' : isVoiceMessage ? 'message' : 'podcast'}-$id';
@ -165,7 +164,7 @@ class MediaService {
) )
.then((result) { .then((result) {
if (result != null && result.files.length > 3) { if (result != null && result.files.length > 3) {
return null; return null;
} }
return result; return result;
}); });
@ -190,88 +189,93 @@ class MediaService {
static Future<String?> downloadFile(String url, {final String? name}) async { static Future<String?> downloadFile(String url, {final String? name}) async {
debugPrint("Attempting to download file from URL: $url"); debugPrint("Attempting to download file from URL: $url");
final basename = name ?? p.basename(url).split('?accessToken=').first; final basename = name ?? p.basename(url).split('?accessToken=').first;
PermissionStatus status; PermissionStatus status;
if (Platform.isAndroid) { if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo; final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) { if (androidInfo.version.sdkInt >= 33) {
debugPrint("Android 13+ detected. Requesting Media permissions."); debugPrint("Android 13+ detected. Requesting Media permissions.");
// برای Android 13+، بسته به نوع فایل permission مناسب را درخواست میکنیم final extension = basename.toLowerCase();
final extension = basename.toLowerCase(); if (extension.contains('.mp3') ||
if (extension.contains('.mp3') || extension.contains('.wav') || extension.contains('.wav') ||
extension.contains('.m4a') || extension.contains('.aac')) { extension.contains('.m4a') ||
status = await Permission.audio.request(); extension.contains('.aac')) {
} else if (extension.contains('.mp4') || extension.contains('.avi') || status = await Permission.audio.request();
extension.contains('.mov')) { } else if (extension.contains('.mp4') ||
status = await Permission.videos.request(); extension.contains('.avi') ||
} else if (extension.contains('.jpg') || extension.contains('.jpeg') || extension.contains('.mov')) {
extension.contains('.png') || extension.contains('.gif')) { status = await Permission.videos.request();
status = await Permission.photos.request(); } else if (extension.contains('.jpg') ||
} else { extension.contains('.jpeg') ||
// برای فایلهای دیگر، همه permissions رسانه را درخواست میکنیم extension.contains('.png') ||
final audioStatus = await Permission.audio.request(); extension.contains('.gif')) {
final videoStatus = await Permission.videos.request(); status = await Permission.photos.request();
final photoStatus = await Permission.photos.request();
status = (audioStatus.isGranted || videoStatus.isGranted || photoStatus.isGranted)
? PermissionStatus.granted
: audioStatus;
}
} else { } else {
debugPrint("Older Android version detected. Requesting Storage permission."); final audioStatus = await Permission.audio.request();
status = await Permission.storage.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) { } else if (Platform.isIOS) {
debugPrint("iOS detected. Requesting Photos permission."); debugPrint("iOS detected. Requesting Photos permission.");
status = await Permission.photos.request(); status = await Permission.photos.request();
} else { } else {
status = PermissionStatus.granted; status = PermissionStatus.granted;
} }
debugPrint("Permission status after request: $status"); debugPrint("Permission status after request: $status");
if (status.isGranted) { if (status.isGranted) {
Directory? dir; Directory? dir;
try { try {
if (Platform.isIOS) { if (Platform.isIOS) {
dir = await getApplicationDocumentsDirectory(); dir = await getApplicationDocumentsDirectory();
} else { } else {
dir = await getExternalStorageDirectory(); 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;
} }
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) { } else if (status.isPermanentlyDenied) {
debugPrint("Storage permission is permanently denied. Opening app settings."); debugPrint(
await openAppSettings(); "Storage permission is permanently denied. Opening app settings.");
return null; await openAppSettings();
return null;
} else { } else {
debugPrint("Storage permission was denied."); debugPrint("Storage permission was denied.");
return null; return null;
} }
} }
static String downloadFileFromWeb(String url) { static String downloadFileFromWeb(String url) {
final filename = url.split('/').last.split('?').first; final filename = url.split('/').last.split('?').first;
html.AnchorElement anchorElement = html.AnchorElement(href: url); html.AnchorElement anchorElement = html.AnchorElement(href: url);
@ -295,4 +299,4 @@ class MediaService {
hasDismissButton: false, hasDismissButton: false,
)); ));
} }
} }

View File

@ -9,104 +9,86 @@ import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class FirebaseApi { class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance; // تعریف متغیر به صورت سراسری در کلاس حذف شد تا از کرش زودهنگام جلوگیری شود
static String? fcmToken; static String? fcmToken;
Future<void> initNotification() async { Future<void> initNotification() async {
try { // ۱. بررسی پشتیبانی مرورگر (بسیار مهم برای آیفون و سافاری)
if (kIsWeb) { if (kIsWeb) {
fcmToken = await _firebaseMessaging.getToken( try {
vapidKey: // اول چک میکنیم پشتیبانی میشه یا نه
"BMXHGd93t_htpS7c62ceuuLVVmia2cEDmqxp46g9Vt0B3OxNMKIqN9nupsUMtv2Vq8Yy2sQGIqgCm9FxUSKvssU", bool isSupported = await FirebaseMessaging.instance.isSupported();
); if (!isSupported) {
} else { if (kDebugMode) {
fcmToken = await _firebaseMessaging.getToken(); 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, try {
announcement: true, final messaging = FirebaseMessaging.instance;
badge: true,
carPlay: false,
criticalAlert: true,
provisional: true,
sound: true,
);
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 (settings.authorizationStatus == AuthorizationStatus.authorized ||
if (kDebugMode) { settings.authorizationStatus == AuthorizationStatus.provisional) {
print("=== APP OPENED FROM NOTIFICATION (TERMINATED) ===");
print("Message ID: ${initMsg.messageId}"); if (kIsWeb) {
print("Raw Data: ${initMsg.data}"); fcmToken = await messaging.getToken(
if (initMsg.notification != null) { vapidKey: "BMXHGd93t_htpS7c62ceuuLVVmia2cEDmqxp46g9Vt0B3OxNMKIqN9nupsUMtv2Vq8Yy2sQGIqgCm9FxUSKvssU",
print("Title: ${initMsg.notification!.title}"); );
print("Body: ${initMsg.notification!.body}"); } 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); NotificationMessage data = NotificationMessage.fromJson(initMsg.data);
HomeWidgetRepository.data = 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 { 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 { try {
NotificationMessage data = NotificationMessage.fromJson(initMsg.data); NotificationMessage data = NotificationMessage.fromJson(initMsg.data);
HomeWidgetRepository.data = 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 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)}');
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) print("Error handling background message: $e");
print("Error handling background message: $e");
}
e.printError();
} }
}); });
@ -116,39 +98,10 @@ class FirebaseApi {
void handleMessage(RemoteMessage? message) async { void handleMessage(RemoteMessage? message) async {
if (message == null) return; 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 { try {
await NotificationService.showFirebaseNotification(message); await NotificationService.showFirebaseNotification(message);
} catch (e) { } catch (e) {
e.printError(); if (kDebugMode) print("Error showing local notification: $e");
} }
} }
} }

View File

@ -62,7 +62,7 @@ class DidvanPlusListPage extends StatelessWidget {
children: [ children: [
Center( Center(
child: SvgPicture.asset( child: SvgPicture.asset(
'lib/assets /images/logos/logo-horizontal-light.svg', 'lib/assets/images/logos/logo-horizontal-light.svg',
height: 55, height: 55,
), ),
), ),
@ -124,6 +124,13 @@ class DidvanPlusListPage extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
debugPrint('🎬 Opening video: ${item.title}'); 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( showDialog(
context: context, context: context,
useSafeArea: false, useSafeArea: false,
@ -156,11 +163,19 @@ class DidvanPlusListPage extends StatelessWidget {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
SkeletonImage( Builder(
imageUrl: '${RequestHelper.baseUrl}${item.image}', builder: (context) {
width: double.infinity, final imageUrl = item.image.startsWith('http') || item.image.contains(RequestHelper.baseUrl)
height: double.infinity, ? item.image
borderRadius: BorderRadius.circular(20), : '${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( Center(
child: Container( child: Container(

View File

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

View File

@ -133,7 +133,17 @@ class _DidvanVoiceListCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme; 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( return GestureDetector(
onTap: onTap, onTap: onTap,

View File

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

View File

@ -24,7 +24,6 @@ class DidvanVoiceDetailCard extends StatefulWidget {
class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> { class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
late AudioPlayer _audioPlayer; late AudioPlayer _audioPlayer;
bool _isInitialized = false;
@override @override
void initState() { void initState() {
@ -34,22 +33,40 @@ class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
void _initializeAudio() async { void _initializeAudio() async {
_audioPlayer = VoiceService.audioPlayer; _audioPlayer = VoiceService.audioPlayer;
debugPrint('🎙️ Detail - Original File: ${widget.didvanVoice.file}');
// برای یک لقمه استراتژی، لینک مستقیم از سرور استفاده میشود // برای یک لقمه استراتژی، لینک مستقیم از سرور استفاده میشود
final audioUrl = widget.didvanVoice.file.startsWith('http') final audioFileUrl = widget.didvanVoice.file.startsWith('http') || widget.didvanVoice.file.contains(RequestHelper.baseUrl)
? widget.didvanVoice.file ? 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('🎙️ Didvan Voice Audio URL: $audioUrl'); 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 { try {
VoiceService.src = audioUrl; VoiceService.src = audioUrl;
await _audioPlayer.setUrl(audioUrl); await _audioPlayer.setUrl(audioUrl);
if (mounted) {
setState(() {
_isInitialized = true;
});
}
} catch (e) { } catch (e) {
debugPrint('❌ Audio initialization error: $e'); debugPrint('❌ Audio initialization error: $e');
} }
@ -70,7 +87,17 @@ class _DidvanVoiceDetailCardState extends State<DidvanVoiceDetailCard> {
@override @override
Widget build(BuildContext context) { 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( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),

View File

@ -40,11 +40,38 @@ class _DidvanVoiceSectionState extends State<DidvanVoiceSection> {
@override @override
Widget build(BuildContext context) { 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 ? 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( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), 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/providers/user.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/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:persian_number_utility/persian_number_utility.dart';
@ -308,13 +309,34 @@ class _HistoryDrawerContentState extends State<HistoryDrawerContent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(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( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Container( child: Container(
width: MediaQuery.of(context).size.width * 0.75, width: drawerWidth,
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
decoration: BoxDecoration( decoration: BoxDecoration(
color: DesignConfig.isDark color: DesignConfig.isDark

View File

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

View File

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

View File

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

View File

@ -1,54 +1,11 @@
{{flutter_js}} {{flutter_js}}
{{flutter_build_config}} {{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({ _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) { onEntrypointLoaded: async function(engineInitializer) {
try { const appRunner = await engineInitializer.initializeEngine({
let appRunner = await engineInitializer.initializeEngine(); renderer: "html",
await appRunner.runApp(); });
} catch (error) { await appRunner.runApp();
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>';
}
}
} }
}); });

View File

@ -1,130 +1,69 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<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." />
<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-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" />
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="icon" type="image/png" href="favicon.png" /> <title>Didvan</title>
<script src="flutter_bootstrap.js" async> <link rel="manifest" href="manifest.json" />
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () { <script src="flutter_bootstrap.js" async></script>
navigator.serviceWorker.register('firebase-messaging-sw.js', {
scope: '/firebase-cloud-messaging-push-scope', <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> <style>
<link rel="manifest" href="manifest.json" /> body {
<style> margin: 0;
/* Override Roboto font to prevent Google Fonts loading */ padding: 0;
@font-face { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, Arial, sans-serif;
font-family: 'Roboto'; }
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: local('Arial'), local('Tahoma'), local('Helvetica');
}
.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 { .indicator {
width: 50vw; width: 25vw;
} }
}
</style>
</head>
@media only screen and (min-width: 600px) { <body style="overflow: hidden">
.indicator { <div id="loading_indicator" class="container">
width: 25vw; <img class="indicator" src="./assets/lib/assets/animations/loading.gif" alt="Loading..." />
} </div>
}
</style>
</head>
<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> </body>
</html> </html>