Add training plan and GIF exercise catalog
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user