432 lines
13 KiB
Dart
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());
|
|
}
|
|
}
|
|
}
|