137 lines
5.1 KiB
Dart
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;
|
|
}
|