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
+66 -49
View File
@@ -16,63 +16,80 @@ 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()); }
} if (result == null) {
final result = snapshot.data; return _CatalogMessage(
if (result == null) { message: 'تعذر تحميل مكتبة التمارين.',
return _CatalogMessage( onRetry: _reload,
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)),
),
],
); );
}, }
), 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<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)),