frontendPlayer/lib/data/services/api_service.dart
2026-04-10 09:55:19 +03:30

432 lines
13 KiB
Dart

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../../core/config/app_config.dart';
import '../mock/mock_data.dart';
import 'dio_web_credentials_stub.dart'
if (dart.library.html) 'dio_web_credentials_web.dart';
import 'video_encryptor_stub.dart'
if (dart.library.html) 'video_encryptor_web.dart';
class ApiException implements Exception {
final String message;
ApiException(this.message);
@override
String toString() => message;
}
class ApiService {
late final Dio _dio;
String? _accessToken;
String _tokenType = 'Bearer';
String? _userName;
String? _userPhone;
String? get currentUserName => _userName;
String? get currentUserPhone => _userPhone;
ApiService() {
_dio = Dio(
BaseOptions(
baseUrl: AppConfig.baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: {'Accept': 'application/json'},
),
);
configureWebCredentials(_dio);
_dio.interceptors.add(
LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseHeader: true,
responseBody: true,
error: true,
logPrint: (obj) => debugPrint(obj.toString()),
),
);
}
Future<Map<String, dynamic>> login(
String phoneNumber,
String password,
) async {
if (AppConfig.isDebug) {
await Future.delayed(
const Duration(seconds: 1),
); // Simulate network delay
// Simple mock validation
if (phoneNumber.isNotEmpty && password.isNotEmpty) {
final data = Map<String, dynamic>.from(MockData.loginSuccess);
_bindAccessToken(data);
return data;
} else {
throw ApiException('اطلاعات ورود نامعتبر است.');
}
} else {
try {
final response = await _dio.post(
'/auth/login',
data: {'phone_number': phoneNumber, 'password': password},
);
if (response.data is Map<String, dynamic>) {
final data = response.data as Map<String, dynamic>;
_bindAccessToken(data);
return data;
}
return <String, dynamic>{};
} on DioException catch (e) {
throw ApiException(_formatDioError(e));
} catch (e) {
throw ApiException(e.toString());
}
}
}
void _bindAccessToken(Map<String, dynamic> payload) {
final token = payload['access_token'];
if (token is String && token.trim().isNotEmpty) {
_accessToken = token.trim();
}
final tokenType = payload['token_type'];
if (tokenType is String && tokenType.trim().isNotEmpty) {
_tokenType = tokenType.trim();
}
if (_accessToken != null) {
_dio.options.headers['Authorization'] = '$_tokenType $_accessToken';
}
final user = payload['user'];
if (user is Map<String, dynamic>) {
final userName = user['user_name'];
final userPhone = user['user_phone'];
if (userName is String && userName.trim().isNotEmpty) {
_userName = userName.trim();
}
if (userPhone is String && userPhone.trim().isNotEmpty) {
_userPhone = userPhone.trim();
}
}
}
void clearSession() {
_accessToken = null;
_userName = null;
_userPhone = null;
_dio.options.headers.remove('Authorization');
}
String _formatDioError(DioException e) {
final request = e.requestOptions;
final response = e.response;
final uri = request.uri.toString();
final method = request.method;
final statusCode = response?.statusCode;
final responseData = response?.data;
return 'DioException(type: ${e.type}, method: $method, uri: $uri, statusCode: $statusCode, message: ${e.message}, response: $responseData)';
}
Future<Map<String, dynamic>> getDashboardData() async {
if (AppConfig.isDebug) {
await Future.delayed(const Duration(seconds: 1));
return MockData.dashboardStats;
} else {
try {
final response = await _dio.get('/dashboard');
return response.data;
} catch (e) {
throw ApiException(e.toString());
}
}
}
Future<Map<String, dynamic>> getCourses({int page = 1}) async {
if (AppConfig.isDebug) {
await Future.delayed(const Duration(milliseconds: 600));
return <String, dynamic>{
'data': <dynamic>[],
'links': <String, dynamic>{
'first': null,
'last': null,
'prev': null,
'next': null,
},
'meta': <String, dynamic>{
'current_page': 1,
'from': null,
'last_page': 1,
'path': '${AppConfig.baseUrl}/web/courses',
'per_page': 0,
'to': null,
'total': 0,
'links': <dynamic>[],
},
};
}
try {
final response = await _dio.get(
'/web/courses',
queryParameters: {'page': page},
);
if (response.data is Map<String, dynamic>) {
return response.data as Map<String, dynamic>;
}
return <String, dynamic>{};
} on DioException catch (e) {
throw ApiException(_formatDioError(e));
} catch (e) {
throw ApiException(e.toString());
}
}
Future<Map<String, dynamic>> getCourseById(int courseId) async {
if (AppConfig.isDebug) {
await Future.delayed(const Duration(milliseconds: 500));
return <String, dynamic>{
'data': <String, dynamic>{
'id': courseId,
'teacher_name': <String, dynamic>{
'user_name': _userName ?? 'کاربر نمونه',
},
'title': 'دوره نمونه شماره $courseId',
'cover_url': null,
'description': 'توضیحات نمونه برای دوره شماره $courseId',
'price': 0,
'preview_video_url': null,
},
};
}
try {
final response = await _dio.get('/web/courses/$courseId');
if (response.data is Map<String, dynamic>) {
return response.data as Map<String, dynamic>;
}
return <String, dynamic>{};
} on DioException catch (e) {
throw ApiException(_formatDioError(e));
} catch (e) {
throw ApiException(e.toString());
}
}
Future<Map<String, dynamic>> getCourseLicenses({
required int courseId,
int page = 1,
}) async {
if (AppConfig.isDebug) {
await Future.delayed(const Duration(milliseconds: 500));
return <String, dynamic>{
'data': <Map<String, dynamic>>[
<String, dynamic>{
'activation_code': 'SPOT-$courseId-001',
'status': 'active',
'device_limits': <Map<String, dynamic>>[
<String, dynamic>{'count': 2},
],
'course': <String, dynamic>{
'id': courseId,
'title': 'دوره نمونه شماره $courseId',
},
'user': <String, dynamic>{'user_name': _userName ?? 'کاربر نمونه'},
},
],
'links': <String, dynamic>{
'first': null,
'last': null,
'prev': null,
'next': null,
},
'meta': <String, dynamic>{
'current_page': 1,
'from': 1,
'last_page': 1,
'path': '${AppConfig.baseUrl}/web/courses/$courseId/licenses',
'per_page': 15,
'to': 1,
'total': 1,
'links': <dynamic>[],
},
};
}
try {
final response = await _dio.get(
'/web/courses/$courseId/licenses',
queryParameters: {'page': page},
);
if (response.data is Map<String, dynamic>) {
return response.data as Map<String, dynamic>;
}
return <String, dynamic>{};
} on DioException catch (e) {
throw ApiException(_formatDioError(e));
} catch (e) {
throw ApiException(e.toString());
}
}
Future<Map<String, dynamic>> createCourseLicense({
required int courseId,
required String studentPhone,
String? studentName,
String? studentEmail,
required List<Map<String, dynamic>> deviceLimits,
}) async {
if (AppConfig.isDebug) {
await Future.delayed(const Duration(milliseconds: 500));
return <String, dynamic>{
'status': 'success',
'message': 'لایسنس با موفقیت ساخته شد.',
'data': <String, dynamic>{
'activation_code': 'NEW-$courseId-001',
'status': 'active',
'device_limits': deviceLimits,
'course': <String, dynamic>{'id': courseId},
'user': <String, dynamic>{
'user_name': studentName ?? 'دانشجو',
'user_phone': studentPhone,
},
},
};
}
try {
final response = await _dio.post(
'/web/courses/$courseId/licenses',
data: <String, dynamic>{
'student_phone': studentPhone,
if (studentName != null && studentName.trim().isNotEmpty)
'student_name': studentName.trim(),
if (studentEmail != null && studentEmail.trim().isNotEmpty)
'student_email': studentEmail.trim(),
'device_limits': deviceLimits,
},
);
if (response.data is Map<String, dynamic>) {
return response.data as Map<String, dynamic>;
}
return <String, dynamic>{};
} on DioException catch (e) {
throw ApiException(_formatDioError(e));
} catch (e) {
throw ApiException(e.toString());
}
}
Future<Map<String, dynamic>> createCourse({
required String title,
required String description,
required num price,
required Uint8List coverBytes,
required String coverFileName,
required Uint8List previewVideoBytes,
required String previewVideoFileName,
required String teacherPhone,
required String alt,
}) async {
if (AppConfig.isDebug) {
await Future.delayed(const Duration(milliseconds: 700));
return <String, dynamic>{
'status': 'success',
'message': 'دوره با موفقیت ساخته شد.',
'data': <String, dynamic>{
'id': 999,
'teacher_name': <String, dynamic>{
'user_name': _userName ?? 'کاربر نمونه',
},
'title': title,
'cover_url': null,
'description': description,
'price': price.toInt(),
'preview_video_url': null,
},
};
}
try {
final formData = FormData.fromMap({
'title': title,
'description': description,
'price': price,
'alt': alt,
'cover': MultipartFile.fromBytes(coverBytes, filename: coverFileName),
'preview_video': MultipartFile.fromBytes(
previewVideoBytes,
filename: previewVideoFileName,
),
'teacher_phone': teacherPhone,
});
final response = await _dio.post('/web/courses', data: formData);
if (response.data is Map<String, dynamic>) {
return response.data as Map<String, dynamic>;
}
return <String, dynamic>{};
} on DioException catch (e) {
throw ApiException(_formatDioError(e));
} catch (e) {
throw ApiException(e.toString());
}
}
Future<Map<String, dynamic>> uploadEncryptedCourseVideo({
required int courseId,
required String title,
required Uint8List sourceVideoBytes,
void Function(int sent, int total)? onSendProgress,
}) async {
final encrypted = await encryptVideoForUpload(sourceVideoBytes);
if (AppConfig.isDebug) {
await Future.delayed(const Duration(milliseconds: 900));
return <String, dynamic>{
'status': 'success',
'message': 'ویدیوی رمزنگاری شده با موفقیت دریافت و ثبت شد.',
'data': <String, dynamic>{
'title': title,
'course_id': courseId,
'created_at': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
'content_id': encrypted.contentId,
},
};
}
try {
final formData = FormData.fromMap({
'title': title,
'content_id': encrypted.contentId,
'content_key': encrypted.contentKeyHex,
'video_file': MultipartFile.fromBytes(
encrypted.encryptedFileBytes,
filename: encrypted.encryptedFileName,
),
});
final response = await _dio.post(
'/web/courses/$courseId/videos',
data: formData,
onSendProgress: onSendProgress,
);
if (response.data is Map<String, dynamic>) {
return response.data as Map<String, dynamic>;
}
return <String, dynamic>{};
} on DioException catch (e) {
throw ApiException(_formatDioError(e));
} catch (e) {
throw ApiException(e.toString());
}
}
}