Files
fitness/mobile/lib/services/training_plan_service.dart
T

137 lines
5.1 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../models/training_plan.dart';
import 'auth_service.dart';
class TrainingPlanService {
TrainingPlanService({required AuthService auth, http.Client? client})
: _auth = auth,
_client = client ?? http.Client();
static const _cacheKey = 'sportpath_training_plans_v1';
final AuthService _auth;
final http.Client _client;
Future<TrainingCatalogResult> 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,
fromCache: cached.isNotEmpty,
message: 'عنوان الخادم غير مضبوط على هذه النسخة.',
);
}
try {
final response = await _client
.get(Uri.parse('${_auth.apiBase}/plans.php'))
.timeout(const Duration(seconds: 15));
if (response.statusCode != 200) {
throw TrainingCatalogException(
'تعذر تحميل الخطط (${response.statusCode}).');
}
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic> || decoded['plans'] is! List) {
throw const TrainingCatalogException('صيغة بيانات الخطط غير متوقعة.');
}
final plans = (decoded['plans'] as List)
.whereType<Map<String, dynamic>>()
.map(TrainingPlan.fromJson)
.where((plan) => plan.uuid.isNotEmpty)
.toList(growable: false);
await prefs.setString(
_cacheKey,
jsonEncode(plans.map(_toCacheJson).toList(growable: false)),
);
return TrainingCatalogResult(plans: plans, fromCache: false);
} catch (error) {
if (cached.isNotEmpty) {
return TrainingCatalogResult(
plans: cached,
fromCache: true,
message: 'تعذر الاتصال؛ نعرض الخطط المحفوظة على الجهاز.',
);
}
return TrainingCatalogResult(
plans: const [],
fromCache: false,
message: error is TrainingCatalogException
? error.message
: 'لا يوجد اتصال بالخادم الآن.',
);
}
}
List<TrainingPlan> _decodeCache(String? value) {
if (value == null || value.isEmpty) return const [];
try {
final decoded = jsonDecode(value);
if (decoded is! List) return const [];
return decoded
.whereType<Map<String, dynamic>>()
.map(TrainingPlan.fromJson)
.toList(growable: false);
} catch (_) {
return const [];
}
}
Map<String, dynamic> _toCacheJson(TrainingPlan plan) => {
'plan_uuid': plan.uuid,
'slug': plan.slug,
'title_ar': plan.title,
'summary_ar': plan.summary,
'goal': plan.goal,
'level': plan.level,
'weeks_duration': plan.weeksDuration,
'safety_notes_ar': plan.safetyNotes,
'sessions': plan.sessions
.map((session) => {
'week': session.week,
'day': session.day,
'title_ar': session.title,
'type': session.type,
'duration_minutes': session.durationMinutes,
'intensity': session.intensity,
'notes_ar': session.notes,
'exercises': session.exercises
.map((exercise) => {
'exercise_uuid': exercise.uuid,
'slug': exercise.slug,
'title_ar': exercise.title,
'instructions_ar': exercise.instructions,
'target_muscles': exercise.targetMuscles,
'equipment': exercise.equipment,
'gif_url': exercise.gifUrl,
'gif_poster_url': exercise.posterUrl,
'duration_seconds': exercise.durationSeconds,
'repetitions': exercise.repetitions,
'safety_notes_ar': exercise.safetyNotes,
'alternative_exercise_uuid':
exercise.alternativeUuid,
'prescription': {
'sets': exercise.prescription.sets,
'reps': exercise.prescription.reps,
'duration_seconds':
exercise.prescription.durationSeconds,
'rest_seconds': exercise.prescription.restSeconds,
},
})
.toList(growable: false),
})
.toList(growable: false),
};
}
class TrainingCatalogException implements Exception {
const TrainingCatalogException(this.message);
final String message;
}