didvan-app/lib/views/splash/splash.dart

481 lines
14 KiB
Dart

// ignore_for_file: unused_field, deprecated_member_use
import 'dart:io';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/main.dart';
import 'package:didvan/providers/media.dart';
import 'package:didvan/providers/server_data.dart';
import 'package:didvan/providers/theme.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/app_home_widget/home_widget_repository.dart';
import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/storage/storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:in_app_update/in_app_update.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:universal_html/html.dart' as html;
import 'package:url_launcher/url_launcher.dart';
class Splash extends StatefulWidget {
const Splash({Key? key}) : super(key: key);
@override
State<Splash> createState() => _SplashState();
}
class _SplashState extends State<Splash> with SingleTickerProviderStateMixin {
bool _errorOccured = false;
late AnimationController _pulseController;
late Animation<double> _pulseAnimation;
@override
void initState() {
super.initState();
if (kIsWeb) {
final loader = html.document.getElementById('loading_indicator');
if (loader != null) {
loader.remove();
}
}
_setupAnimations();
WidgetsBinding.instance.addPostFrameCallback((_) {
_startInitialization();
});
}
void _setupAnimations() {
_pulseController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_pulseAnimation = Tween<double>(
begin: 1.0,
end: 1.1,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
_pulseController.repeat(reverse: true);
}
@override
void dispose() {
_pulseController.dispose();
super.dispose();
}
void _startInitialization() {
final themeProvider = context.read<ThemeProvider>();
final userProvider = context.read<UserProvider>();
final mediaProvider = context.read<MediaProvider>();
_initialize(themeProvider, userProvider, mediaProvider);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final colorScheme = Theme.of(context).colorScheme;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
systemNavigationBarColor: colorScheme.background,
),
child: Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: colorScheme.background,
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(flex: 2),
_buildLogo(),
const SizedBox(height: 60),
if (_errorOccured)
_buildErrorView(colorScheme)
else
_buildLoadingView(colorScheme),
const Spacer(flex: 3),
],
),
),
),
),
);
}
Widget _buildLogo() {
return SvgPicture.asset(
Assets.horizontalLogoWithText,
height: 80,
);
}
Widget _buildLoadingView(ColorScheme colorScheme) {
return Column(
children: [
SizedBox(
width: 60,
height: 60,
child: Image.asset(
Assets.loadingAnimation,
fit: BoxFit.contain,
),
),
const SizedBox(height: 24),
AnimatedBuilder(
animation: _pulseController,
builder: (context, child) {
return Opacity(
opacity: 0.4 + (0.4 * _pulseController.value),
child: Text(
'در حال بارگذاری...',
style: TextStyle(
fontSize: 14,
color: colorScheme.onBackground.withOpacity(0.6),
fontWeight: FontWeight.w400,
),
),
);
},
),
],
);
}
Widget _buildErrorView(ColorScheme colorScheme) {
return Column(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: colorScheme.error.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.wifi_off_rounded,
size: 24,
color: colorScheme.error,
),
),
const SizedBox(height: 16),
Text(
'مشکل در اتصال',
style: TextStyle(
fontSize: 16,
color: colorScheme.onBackground,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'لطفاً اتصال اینترنت خود را بررسی کنید',
style: TextStyle(
fontSize: 13,
color: colorScheme.onBackground.withOpacity(0.6),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
setState(() {
_errorOccured = false;
});
_startInitialization();
},
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('تلاش مجدد'),
),
],
);
}
Future<void> _initialize(ThemeProvider themeProvider,
UserProvider userProvider, MediaProvider mediaProvider) async {
try {
if (!mounted) return;
final settingsData = await AppInitializer.initilizeSettings();
themeProvider.themeMode = settingsData.themeMode;
themeProvider.fontFamily = settingsData.fontFamily;
themeProvider.fontScale = settingsData.fontScale;
if (mounted) {
await AppInitializer.setupServices(context);
}
final String? token = await userProvider.setAndGetToken();
if (token != null) {
RequestService.token = token;
}
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
bool stopForUpdate = await _checkForUpdate();
if (stopForUpdate) return;
}
if (kIsWeb) {
html.window.onBeforeUnload.listen((event) {
StorageService.webStorage
.removeWhere((key, value) => key == 'image-cache');
});
}
bool isMobileApp = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
if (isMobileApp) {
final String? hasSeenOnboarding =
await StorageService.getValue(key: 'hasSeenOnboarding');
if (hasSeenOnboarding != 'true') {
if (mounted) {
Navigator.of(context).pushReplacementNamed(Routes.onboarding);
}
return;
}
}
if (token != null) {
if (!kIsWeb) {
await mediaProvider.getDownloadsList();
}
// Add timeout for getUserInfo to prevent blocking on slow networks
final result = await userProvider.getUserInfo().timeout(
const Duration(seconds: 15),
onTimeout: () {
debugPrint("getUserInfo timed out - treating as failure");
return false;
},
);
if (!result) {
await StorageService.delete(key: 'token');
if (mounted) {
Navigator.of(context).pushNamedAndRemoveUntil(
Routes.splash,
(_) => false,
);
}
return;
}
// Add timeout for ServerDataProvider.getData()
try {
await ServerDataProvider.getData().timeout(
const Duration(seconds: 10),
onTimeout: () {
debugPrint("ServerDataProvider.getData timed out - continuing without server data");
},
);
} catch (e) {
debugPrint("ServerDataProvider.getData failed: $e - continuing");
// Continue even if server data fetch fails
}
}
_navigateToNextScreen(token);
} catch (e) {
debugPrint("Error in initialize: $e");
if (mounted) {
setState(() {
_errorOccured = true;
});
}
}
}
void _navigateToNextScreen(String? token) async {
if (!mounted) return;
String extractedPath = initialURI?.path == '/'
? Routes.home
: (initialURI?.path ?? Routes.home);
final String destinationRoute =
token == null ? Routes.authenticaion : extractedPath;
dynamic routeArguments =
token == null ? {'isResetPassword': false} : {'showDialogs': false};
if (destinationRoute == Routes.home) {
initialURI = null;
}
if (destinationRoute == Routes.authenticaion) {
routeArguments = false;
}
if (token != null && HomeWidgetRepository.data != null) {
Navigator.of(context).pushReplacementNamed(
Routes.home,
arguments: {'showDialogs': false},
);
await HomeWidgetRepository.decideWhereToGoNotif();
if (HomeWidgetRepository.data != null) {
await StorageService.delete(
key:
'notification${AppInitializer.createNotificationId(HomeWidgetRepository.data!)}');
}
return;
}
Navigator.of(context).pushReplacementNamed(
destinationRoute,
arguments: routeArguments,
);
}
Future<bool> _checkForUpdate() async {
try {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
bool isGooglePlay = packageInfo.installerStore == 'com.android.vending';
if (isGooglePlay) {
try {
AppUpdateInfo updateInfo = await InAppUpdate.checkForUpdate();
if (updateInfo.updateAvailability ==
UpdateAvailability.updateAvailable) {
if (updateInfo.immediateUpdateAllowed) {
await InAppUpdate.performImmediateUpdate();
return true;
} else if (updateInfo.flexibleUpdateAllowed) {
await InAppUpdate.startFlexibleUpdate();
await InAppUpdate.completeFlexibleUpdate();
return false;
}
}
} catch (e) {
return await _checkCustomApiUpdate(packageInfo);
}
} else {
return await _checkCustomApiUpdate(packageInfo);
}
} catch (e) {
debugPrint("Update check failed: $e");
}
return false;
}
Future<bool> _checkCustomApiUpdate(PackageInfo packageInfo) async {
final req = RequestService(
RequestHelper.checkVersion,
useAutherization: true,
);
await req.httpGet();
if (req.isSuccess && mounted) {
final result = req.result;
final apiVersion = result['version']?.toString();
final bool isMandatory = result['isMandatory'] ?? false;
final String downloadUrl =
result['appName'] ?? 'https://api.didvan.app/uploads/apk';
if (apiVersion != null &&
_isUpdateAvailable(packageInfo.version, apiVersion)) {
_showUpdateDialog(downloadUrl, isMandatory);
return isMandatory;
}
}
return false;
}
bool _isUpdateAvailable(String currentVersion, String newVersion) {
try {
List<int> currentParts =
currentVersion.split('.').map(int.parse).toList();
List<int> newParts = newVersion.split('.').map(int.parse).toList();
for (int i = 0; i < newParts.length; i++) {
if (i >= currentParts.length) return true;
if (newParts[i] > currentParts[i]) return true;
if (newParts[i] < currentParts[i]) return false;
}
return false;
} catch (e) {
debugPrint("Version parsing error: $e");
return false;
}
}
void _showUpdateDialog(String url, bool isMandatory) {
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: !isMandatory,
builder: (context) {
return PopScope(
canPop: !isMandatory,
child: AlertDialog(
title: const Text(
'به‌روزرسانی دیدوان',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
content: Text(
isMandatory
? 'نسخه جدیدی از دیدوان در دسترس است. برای ادامه استفاده از برنامه باید آن را به‌روزرسانی کنید.'
: 'نسخه جدیدی از برنامه در دسترس است. آیا مایل به به‌روزرسانی هستید؟',
textAlign: TextAlign.center,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
actionsAlignment: MainAxisAlignment.center,
actions: [
if (!isMandatory)
TextButton(
onPressed: () => Navigator.pop(context),
child:
const Text('بعداً', style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
onPressed: () async {
final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: const Text('دانلود و نصب',
style: TextStyle(color: Colors.white)),
),
],
),
);
},
);
}
}