fixed web history responsive
This commit is contained in:
parent
1f0ec41c1a
commit
5871108f2e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 استفاده کند
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
167
web/index.html
167
web/index.html
|
|
@ -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>
|
||||
Loading…
Reference in New Issue