diff --git a/lib/app.dart b/lib/app.dart index f5bb4c1..d61e364 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'core/locator/locator.dart'; import 'core/theme/app_theme.dart'; +import 'core/widgets/offline_gate.dart'; import 'feature/auth/presentation/bloc/auth_bloc.dart'; import 'feature/auth/presentation/screen/mobile_screen.dart'; import 'feature/auth/presentation/screen/otp_screen.dart'; @@ -133,7 +134,7 @@ class HakemApp extends StatelessWidget { routerConfig: router, builder: (context, child) => Directionality( textDirection: TextDirection.rtl, - child: child!, + child: OfflineGate(child: child!), ), ), ); diff --git a/lib/core/network/connectivity_service.dart b/lib/core/network/connectivity_service.dart new file mode 100644 index 0000000..384772b --- /dev/null +++ b/lib/core/network/connectivity_service.dart @@ -0,0 +1,65 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +import '../config.dart'; + +/// وضعیتِ اینترنت را نگه می‌دارد: هم قطعِ رابطِ شبکه (وای‌فای/دیتا) و هم دسترسیِ +/// واقعی به سرور (با پینگِ سبکِ /health). [online] برای نمایشِ اوورلیِ «اتصال نیست». +class ConnectivityService { + ConnectivityService._(); + static final ConnectivityService I = ConnectivityService._(); + + final ValueNotifier online = ValueNotifier(true); + + final Connectivity _conn = Connectivity(); + final Dio _dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 4), + receiveTimeout: const Duration(seconds: 4), + )); + StreamSubscription? _sub; + bool _checking = false; + + Future init() async { + _sub = _conn.onConnectivityChanged.listen(_onChange); + await recheck(); + } + + void _onChange(List results) { + final hasInterface = + results.any((r) => r != ConnectivityResult.none); + if (!hasInterface) { + online.value = false; // بی‌درنگ: رابطِ شبکه قطع است + } else { + _verify(); // رابط هست؛ دسترسیِ واقعی را بررسی کن + } + } + + /// بررسیِ دستیِ اتصال (دکمه‌ی «تلاش مجدد»). + Future recheck() async { + final results = await _conn.checkConnectivity(); + final hasInterface = results.any((r) => r != ConnectivityResult.none); + if (!hasInterface) { + online.value = false; + return; + } + await _verify(); + } + + Future _verify() async { + if (_checking) return; + _checking = true; + try { + final res = await _dio.get('${AppConfig.baseUrl}/health'); + online.value = res.statusCode == 200; + } catch (_) { + online.value = false; + } finally { + _checking = false; + } + } + + void dispose() => _sub?.cancel(); +} diff --git a/lib/core/widgets/offline_gate.dart b/lib/core/widgets/offline_gate.dart new file mode 100644 index 0000000..8127f98 --- /dev/null +++ b/lib/core/widgets/offline_gate.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; + +import '../network/connectivity_service.dart'; +import '../theme/app_theme.dart'; + +/// [child] را می‌پوشاند و هنگامِ قطعِ اینترنت یک اوورلیِ «اتصال برقرار نیست» با +/// دکمه‌ی «تلاش مجدد» نشان می‌دهد. به‌محضِ برقراریِ اتصال، خودش کنار می‌رود. +class OfflineGate extends StatelessWidget { + final Widget child; + const OfflineGate({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + ValueListenableBuilder( + valueListenable: ConnectivityService.I.online, + builder: (context, online, _) => + online ? const SizedBox.shrink() : const _OfflineOverlay(), + ), + ], + ); + } +} + +class _OfflineOverlay extends StatefulWidget { + const _OfflineOverlay(); + @override + State<_OfflineOverlay> createState() => _OfflineOverlayState(); +} + +class _OfflineOverlayState extends State<_OfflineOverlay> { + bool _busy = false; + + Future _retry() async { + setState(() => _busy = true); + await ConnectivityService.I.recheck(); + if (mounted) setState(() => _busy = false); + } + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: Material( + color: Colors.black.withValues(alpha: 0.82), + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 32), + padding: const EdgeInsets.fromLTRB(24, 28, 24, 22), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.lapisMid, AppColors.lapisLo], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.gold, width: 2), + boxShadow: const [ + BoxShadow(color: Colors.black54, blurRadius: 20), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.bgDark, + border: Border.all(color: AppColors.goldDark, width: 2), + ), + child: const Icon(Icons.wifi_off_rounded, + color: AppColors.gold, size: 38), + ), + const SizedBox(height: 16), + const Text( + 'اتصال به اینترنت برقرار نیست', + textAlign: TextAlign.center, + style: TextStyle( + color: AppColors.gold, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'اینترنتِ دستگاهت را بررسی کن و دوباره تلاش کن.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70, fontSize: 13), + ), + const SizedBox(height: 22), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _busy ? null : _retry, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.gold, + foregroundColor: AppColors.bg, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + icon: _busy + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, color: AppColors.bg), + ) + : const Icon(Icons.refresh, size: 20), + label: Text(_busy ? 'در حال بررسی…' : 'تلاش مجدد'), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/feature/shop/presentation/screen/shop_screen.dart b/lib/feature/shop/presentation/screen/shop_screen.dart index 871b2ef..77e4efb 100644 --- a/lib/feature/shop/presentation/screen/shop_screen.dart +++ b/lib/feature/shop/presentation/screen/shop_screen.dart @@ -99,7 +99,7 @@ class ShopScreen extends StatelessWidget { const _AvatarsTab(), const _CarpetsTab(), const _FramesTab(), - + _BoostersTab(boosters: d.boosters), _VipTab(packages: d.vipPackages), ], @@ -130,9 +130,7 @@ class ShopScreen extends StatelessWidget { child: Row( children: [ const AppBackButton(), - const SizedBox(width: 6), - const Icon(Icons.storefront, color: AppColors.gold, size: 22), - const SizedBox(width: 6), + const Spacer(), const GlowText('فروشگاه', size: 20), const Spacer(), BlocBuilder( diff --git a/lib/main.dart b/lib/main.dart index 51e8986..66655a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'app.dart'; import 'core/locator/locator.dart'; +import 'core/network/connectivity_service.dart'; import 'core/service/app_sounds.dart'; import 'core/settings/app_settings.dart'; import 'feature/auth/domain/repository/auth_repository.dart'; @@ -10,6 +11,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await setupLocator(); await AppSettings.I.load(); // تنظیماتِ گرافیک + ConnectivityService.I.init(); // پایشِ اتصالِ اینترنت (بدون انتظار) AppSounds.preload(); // پیش‌بارگذاریِ صداها (بدون انتظار) final loggedIn = await locator().isLoggedIn(); runApp(HakemApp(loggedIn: loggedIn)); diff --git a/pubspec.lock b/pubspec.lock index 18b892d..6f0df23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,22 @@ packages: url: "https://pub.myket.ir" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.myket.ir" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.0" crypto: dependency: transitive description: @@ -129,6 +145,14 @@ packages: url: "https://pub.myket.ir" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.myket.ir" + source: hosted + version: "0.7.14" dio: dependency: "direct main" description: @@ -416,6 +440,14 @@ packages: url: "https://pub.myket.ir" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.myket.ir" + source: hosted + version: "0.5.0" ordered_set: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 68d1de6..0423632 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: flame_audio: ^2.11.14 random_avatar: ^0.0.8 flutter_svg: ^2.3.0 # نمایشِ شخصیتِ آواتارِ svg (و png با Image.network) + connectivity_plus: ^6.1.0 # تشخیصِ قطعِ اینترنت برای نمایشِ اوورلیِ «اتصال نیست» # خرید درون‌برنامه‌ای: هر flavor یکی را استفاده می‌کند (مایکت/کافه‌بازار). myket_iap: ^1.1.11 flutter_poolakey: ^2.2.0