Refresh cached training catalog safely

This commit is contained in:
Hamza-Ayed
2026-10-04 01:50:52 +03:00
parent b1e5aa2f98
commit 66efcdac6a
2 changed files with 80 additions and 52 deletions
+34 -17
View File
@@ -16,28 +16,47 @@ class TrainingLibraryScreen extends StatefulWidget {
} }
class _TrainingLibraryScreenState extends State<TrainingLibraryScreen> { class _TrainingLibraryScreenState extends State<TrainingLibraryScreen> {
late Future<TrainingCatalogResult> _plans; final TrainingPlanService _service = Get.find<TrainingPlanService>();
TrainingCatalogResult? _result;
bool _loading = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_plans = Get.find<TrainingPlanService>().fetchPlans(); _load();
} }
void _reload() => setState(() { Future<void> _load() async {
_plans = Get.find<TrainingPlanService>().fetchPlans(refresh: true); 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 @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('خطط التمرين')), appBar: AppBar(title: const Text('خطط التمرين')),
body: FutureBuilder<TrainingCatalogResult>( body: Builder(builder: (context) {
future: _plans, final result = _result;
builder: (context, snapshot) { if (_loading && result == null) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
final result = snapshot.data;
if (result == null) { if (result == null) {
return _CatalogMessage( return _CatalogMessage(
message: 'تعذر تحميل مكتبة التمارين.', message: 'تعذر تحميل مكتبة التمارين.',
@@ -55,8 +74,7 @@ class _TrainingLibraryScreenState extends State<TrainingLibraryScreen> {
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
children: [ children: [
if (result.fromCache) if (result.fromCache)
const _CacheNotice( const _CacheNotice(message: 'نعرض نسخة محفوظة على هذا الجهاز.'),
message: 'نعرض نسخة محفوظة على هذا الجهاز.'),
if (result.message != null) if (result.message != null)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
@@ -71,8 +89,7 @@ class _TrainingLibraryScreenState extends State<TrainingLibraryScreen> {
), ),
], ],
); );
}, }),
),
); );
} }
@@ -89,7 +106,7 @@ class _TrainingPlanSpotlightState extends State<TrainingPlanSpotlight> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_plans = Get.find<TrainingPlanService>().fetchPlans(); _plans = Get.find<TrainingPlanService>().fetchPlans(refresh: true);
} }
@override @override
@@ -503,17 +520,17 @@ String _levelLabel(String level) => switch (level) {
}; };
String _goalLabel(String goal) => switch (goal) { String _goalLabel(String goal) => switch (goal) {
'weight_loss' => 'خفض الوزن', 'weight_management' => 'إدارة الوزن',
'general_fitness' => 'لياقة عامة', 'general_fitness' => 'لياقة عامة',
'strength' => 'قوة',
'mobility' => 'مرونة وحركة', 'mobility' => 'مرونة وحركة',
'endurance' => 'التحمل',
_ => goal, _ => goal,
}; };
String _intensityLabel(String intensity) => switch (intensity) { String _intensityLabel(String intensity) => switch (intensity) {
'easy' => 'خفيفة', 'easy' => 'خفيفة',
'moderate' => 'متوسطة', 'moderate' => 'متوسطة',
'hard' => 'عالية', 'vigorous' => 'عالية',
_ => intensity, _ => intensity,
}; };
+14 -3
View File
@@ -15,12 +15,14 @@ class TrainingPlanService {
final AuthService _auth; final AuthService _auth;
final http.Client _client; final http.Client _client;
Future<List<TrainingPlan>> readCachedPlans() async {
final prefs = await SharedPreferences.getInstance();
return _decodeCache(prefs.getString(_cacheKey));
}
Future<TrainingCatalogResult> fetchPlans({bool refresh = false}) async { Future<TrainingCatalogResult> fetchPlans({bool refresh = false}) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final cached = _decodeCache(prefs.getString(_cacheKey)); final cached = _decodeCache(prefs.getString(_cacheKey));
if (!refresh && cached.isNotEmpty) {
return TrainingCatalogResult(plans: cached, fromCache: true);
}
if (!_auth.isApiConfigured) { if (!_auth.isApiConfigured) {
return TrainingCatalogResult( return TrainingCatalogResult(
plans: cached, plans: cached,
@@ -46,6 +48,15 @@ class TrainingPlanService {
.map(TrainingPlan.fromJson) .map(TrainingPlan.fromJson)
.where((plan) => plan.uuid.isNotEmpty) .where((plan) => plan.uuid.isNotEmpty)
.toList(growable: false); .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( await prefs.setString(
_cacheKey, _cacheKey,
jsonEncode(plans.map(_toCacheJson).toList(growable: false)), jsonEncode(plans.map(_toCacheJson).toList(growable: false)),