Update Saqel Platform: 2026-09-12 16:23:39

This commit is contained in:
Hamza-Ayed
2026-09-12 16:23:39 +03:00
parent f4d4f02853
commit 81fa8c5dc8
9 changed files with 585 additions and 115 deletions
@@ -20,6 +20,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/utils/saqel_toast.dart';
import '../../../core/config/app_config.dart';
import '../../../core/services/storage_service.dart';
import '../../../data/models/subject_model.dart';
import '../../../logic/cubits/auth_cubit.dart';
@@ -81,15 +82,23 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
}
final token = await StorageService().getToken();
final headers = <String, String>{};
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
String finalVideoUrl = videoUrl;
if (token != null && token.isNotEmpty && finalVideoUrl.contains('/api/videos/') && !finalVideoUrl.contains('token=')) {
final sep = finalVideoUrl.contains('?') ? '&' : '?';
finalVideoUrl = '$finalVideoUrl${sep}token=${Uri.encodeComponent(token)}';
// IMPORTANT: Check whether the URL points to our own Saqel API or to an external CDN (BunnyCDN, Cloudflare R2).
// NEVER send the Saqel user JWT Bearer token to external CDNs! CDNs reject unauthorized Bearer headers
// with HTTP 403 Forbidden ("Resource is protected by access token").
final isOurBackend = finalVideoUrl.contains('/api/videos/') ||
finalVideoUrl.startsWith(AppConfig.baseUrl) ||
finalVideoUrl.contains('localhost') ||
finalVideoUrl.contains('127.0.0.1');
final headers = <String, String>{};
if (isOurBackend && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
if (!finalVideoUrl.contains('token=')) {
final sep = finalVideoUrl.contains('?') ? '&' : '?';
finalVideoUrl = '$finalVideoUrl${sep}token=${Uri.encodeComponent(token)}';
}
}
_videoController?.dispose();
@@ -119,6 +128,38 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
}
}
}).catchError((err) {
final errStr = err.toString();
// Resilient Fallback: If 403 Forbidden or Access Token error occurred on an external URL with token query params,
// retry once without the token parameters (e.g. public pull zones or direct CDNs).
if ((errStr.contains('403') || errStr.contains('token') || errStr.contains('permission')) &&
finalVideoUrl.contains('?')) {
final strippedUrl = finalVideoUrl.split('?').first;
if (strippedUrl != finalVideoUrl && strippedUrl.isNotEmpty) {
_videoController?.dispose();
_videoController = VideoPlayerController.networkUrl(
Uri.parse(strippedUrl),
httpHeaders: const {},
)..initialize().then((_) {
if (mounted) {
setState(() {
_isVideoInitialized = true;
_videoInitError = null;
});
_revealVideoControls();
if (shouldPlay) _videoController!.play();
}
}).catchError((retryErr) {
if (mounted) {
setState(() {
_isVideoInitialized = false;
_videoInitError = 'تعذر تشغيل بث الفيديو من المصدر السحابي، يمكنك استخدام السبورة التفاعلية للشرح.';
});
}
});
return;
}
}
if (mounted) {
setState(() {
_isVideoInitialized = false;
@@ -407,26 +448,48 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
),
const SizedBox(height: 14),
ElevatedButton.icon(
icon: const Icon(CupertinoIcons.arrow_clockwise, size: 16),
label: const Text('بدء تشغيل بث الفيديو المباشر 🎥'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
setState(() {
_videoInitError = null;
_isVideoInitialized = false;
});
_initPlayer(
state.playbackData.videoUrl,
state.currentPositionSeconds,
true,
);
},
Wrap(
alignment: WrapAlignment.center,
spacing: 10,
runSpacing: 10,
children: [
ElevatedButton.icon(
icon: const Icon(CupertinoIcons.arrow_clockwise, size: 16),
label: const Text('إعادة محاولة البث 🎥'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
setState(() {
_videoInitError = null;
_isVideoInitialized = false;
});
_initPlayer(
state.playbackData.videoUrl,
state.currentPositionSeconds,
true,
);
},
),
OutlinedButton.icon(
icon: const Icon(CupertinoIcons.mic_fill, size: 16, color: AppColors.appleBlue),
label: const Text('السبورة والشرح الصوتي 🎙️', style: TextStyle(color: Colors.white)),
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppColors.appleBlue.withAlpha(150)),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
setState(() {
_showChalkboardMode = true;
_videoInitError = null;
});
},
),
],
),
],
),