From 66efcdac6aca1f59558196e1a7b75ae28e74efe4 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 4 Oct 2026 01:50:52 +0300 Subject: [PATCH] Refresh cached training catalog safely --- mobile/lib/screens/training_screens.dart | 115 ++++++++++-------- .../lib/services/training_plan_service.dart | 17 ++- 2 files changed, 80 insertions(+), 52 deletions(-) diff --git a/mobile/lib/screens/training_screens.dart b/mobile/lib/screens/training_screens.dart index 3c5eb0a..e2e3f96 100644 --- a/mobile/lib/screens/training_screens.dart +++ b/mobile/lib/screens/training_screens.dart @@ -16,63 +16,80 @@ class TrainingLibraryScreen extends StatefulWidget { } class _TrainingLibraryScreenState extends State { - late Future _plans; + final TrainingPlanService _service = Get.find(); + TrainingCatalogResult? _result; + bool _loading = true; @override void initState() { super.initState(); - _plans = Get.find().fetchPlans(); + _load(); } - void _reload() => setState(() { - _plans = Get.find().fetchPlans(refresh: true); + Future _load() async { + final cached = await _service.readCachedPlans(); + if (!mounted) return; + if (cached.isNotEmpty) { + setState(() { + _result = TrainingCatalogResult(plans: cached, fromCache: true); + _loading = false; }); + } + final result = await _service.fetchPlans(refresh: true); + if (mounted) { + setState(() { + _result = result; + _loading = false; + }); + } + } + + void _reload() { + setState(() => _loading = true); + _load(); + } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: const Text('خطط التمرين')), - body: FutureBuilder( - future: _plans, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - final result = snapshot.data; - if (result == null) { - return _CatalogMessage( - message: 'تعذر تحميل مكتبة التمارين.', - onRetry: _reload, - ); - } - if (result.plans.isEmpty) { - return _CatalogMessage( - message: result.message ?? - 'لا توجد خطط منشورة بعد. ستظهر الخطط هنا بعد إضافتها من لوحة الإدارة.', - onRetry: _reload, - ); - } - return ListView( - padding: const EdgeInsets.all(18), - children: [ - if (result.fromCache) - const _CacheNotice( - message: 'نعرض نسخة محفوظة على هذا الجهاز.'), - if (result.message != null) - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Text(result.message!, - style: const TextStyle(color: _muted, height: 1.5)), - ), - for (final plan in result.plans) - _PlanCard( - plan: plan, - onTap: () => - Get.to(() => TrainingPlanDetailScreen(plan: plan)), - ), - ], + body: Builder(builder: (context) { + final result = _result; + if (_loading && result == null) { + return const Center(child: CircularProgressIndicator()); + } + if (result == null) { + return _CatalogMessage( + message: 'تعذر تحميل مكتبة التمارين.', + onRetry: _reload, ); - }, - ), + } + if (result.plans.isEmpty) { + return _CatalogMessage( + message: result.message ?? + 'لا توجد خطط منشورة بعد. ستظهر الخطط هنا بعد إضافتها من لوحة الإدارة.', + onRetry: _reload, + ); + } + return ListView( + padding: const EdgeInsets.all(18), + children: [ + if (result.fromCache) + const _CacheNotice(message: 'نعرض نسخة محفوظة على هذا الجهاز.'), + if (result.message != null) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text(result.message!, + style: const TextStyle(color: _muted, height: 1.5)), + ), + for (final plan in result.plans) + _PlanCard( + plan: plan, + onTap: () => + Get.to(() => TrainingPlanDetailScreen(plan: plan)), + ), + ], + ); + }), ); } @@ -89,7 +106,7 @@ class _TrainingPlanSpotlightState extends State { @override void initState() { super.initState(); - _plans = Get.find().fetchPlans(); + _plans = Get.find().fetchPlans(refresh: true); } @override @@ -503,17 +520,17 @@ String _levelLabel(String level) => switch (level) { }; String _goalLabel(String goal) => switch (goal) { - 'weight_loss' => 'خفض الوزن', + 'weight_management' => 'إدارة الوزن', 'general_fitness' => 'لياقة عامة', - 'strength' => 'قوة', 'mobility' => 'مرونة وحركة', + 'endurance' => 'التحمل', _ => goal, }; String _intensityLabel(String intensity) => switch (intensity) { 'easy' => 'خفيفة', 'moderate' => 'متوسطة', - 'hard' => 'عالية', + 'vigorous' => 'عالية', _ => intensity, }; diff --git a/mobile/lib/services/training_plan_service.dart b/mobile/lib/services/training_plan_service.dart index 6c5e173..ae7a81b 100644 --- a/mobile/lib/services/training_plan_service.dart +++ b/mobile/lib/services/training_plan_service.dart @@ -15,12 +15,14 @@ class TrainingPlanService { final AuthService _auth; final http.Client _client; + Future> readCachedPlans() async { + final prefs = await SharedPreferences.getInstance(); + return _decodeCache(prefs.getString(_cacheKey)); + } + Future fetchPlans({bool refresh = false}) async { final prefs = await SharedPreferences.getInstance(); final cached = _decodeCache(prefs.getString(_cacheKey)); - if (!refresh && cached.isNotEmpty) { - return TrainingCatalogResult(plans: cached, fromCache: true); - } if (!_auth.isApiConfigured) { return TrainingCatalogResult( plans: cached, @@ -46,6 +48,15 @@ class TrainingPlanService { .map(TrainingPlan.fromJson) .where((plan) => plan.uuid.isNotEmpty) .toList(growable: false); + // An empty catalog can be an unpublished/temporarily incomplete response. + // Keep the last known good catalog rather than erasing offline content. + if (plans.isEmpty && cached.isNotEmpty) { + return TrainingCatalogResult( + plans: cached, + fromCache: true, + message: 'لا توجد خطط منشورة حاليًا؛ نعرض آخر نسخة محفوظة.', + ); + } await prefs.setString( _cacheKey, jsonEncode(plans.map(_toCacheJson).toList(growable: false)),