741 lines
23 KiB
Dart
Executable File
741 lines
23 KiB
Dart
Executable File
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_downloader/flutter_downloader.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:youtube_player_iframe/youtube_player_iframe.dart' as yt_iframe;
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
import 'package:chewie/chewie.dart';
|
|
import 'dart:async';
|
|
import 'dart:io' show Platform;
|
|
|
|
import '../models/app_models.dart';
|
|
import '../services/i18n_service.dart';
|
|
import '../services/toast_service.dart';
|
|
import '../screens/pdf_viewer_screen.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
|
|
class LessonDetailSheet extends StatefulWidget {
|
|
final QuickLesson lesson;
|
|
final bool isCompleted;
|
|
final Function(QuickLesson) onMarkComplete;
|
|
|
|
const LessonDetailSheet({
|
|
super.key,
|
|
required this.lesson,
|
|
required this.isCompleted,
|
|
required this.onMarkComplete,
|
|
});
|
|
|
|
@override
|
|
State<LessonDetailSheet> createState() => _LessonDetailSheetState();
|
|
}
|
|
|
|
class _LessonDetailSheetState extends State<LessonDetailSheet> with WidgetsBindingObserver {
|
|
// Controllers
|
|
yt_iframe.YoutubePlayerController? _ytIframeController;
|
|
VideoPlayerController? _videoPlayerController;
|
|
ChewieController? _chewieController;
|
|
|
|
StreamSubscription? _ytSubscription;
|
|
|
|
// State flags
|
|
bool _isLoading = true;
|
|
bool _useExternalFallback = false;
|
|
bool _playerInitialized = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
_initializePlayer();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (_videoPlayerController != null) {
|
|
try {
|
|
if (state == AppLifecycleState.paused && _videoPlayerController!.value.isPlaying) {
|
|
_videoPlayerController!.pause();
|
|
}
|
|
} catch (e) {
|
|
print('LessonDetailSheet: erro ao pausar VideoPlayer -> $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
|
|
_ytSubscription?.cancel();
|
|
|
|
_chewieController?.dispose();
|
|
_videoPlayerController?.dispose();
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
// MÉTODO FALLBACK: Adicionada verificação `mounted` antes de setState
|
|
void _activateExternalFallback() {
|
|
if (!mounted) return;
|
|
|
|
_ytSubscription?.cancel();
|
|
_ytIframeController = null;
|
|
|
|
_chewieController?.dispose();
|
|
_videoPlayerController?.dispose();
|
|
_videoPlayerController = null;
|
|
_chewieController = null;
|
|
|
|
// ✅ CORRIGIDO: Só chama setState se o widget ainda estiver montado
|
|
setState(() {
|
|
_useExternalFallback = true;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
|
|
Future<void> _initializePlayer() async {
|
|
if (_playerInitialized) return;
|
|
_playerInitialized = true;
|
|
if (!mounted) return;
|
|
|
|
final rawUrl = widget.lesson.videoUrl;
|
|
if (rawUrl == null || rawUrl.trim().isEmpty) {
|
|
print('LessonDetailSheet: videoUrl vazio');
|
|
_activateExternalFallback();
|
|
return;
|
|
}
|
|
|
|
final videoUrl = rawUrl.trim();
|
|
|
|
String? youtubeId;
|
|
try {
|
|
youtubeId = yt_iframe.YoutubePlayerController.convertUrlToId(videoUrl);
|
|
print('LessonDetailSheet: convertUrlToId -> $youtubeId for $videoUrl');
|
|
} catch (e) {
|
|
youtubeId = null;
|
|
print('LessonDetailSheet: erro convertUrlToId: $e');
|
|
}
|
|
|
|
if (youtubeId != null && youtubeId.isNotEmpty) {
|
|
try {
|
|
// ✅ CORRIGIDO: autoPlay é passado no construtor, não dentro de params.
|
|
_ytIframeController = yt_iframe.YoutubePlayerController.fromVideoId(
|
|
videoId: youtubeId,
|
|
autoPlay: kIsWeb, // Correção de erro: movemos para o local correto
|
|
params: const yt_iframe.YoutubePlayerParams(
|
|
showControls: true,
|
|
showFullscreenButton: true,
|
|
strictRelatedVideos: true,
|
|
playsInline: true,
|
|
mute: kIsWeb,
|
|
),
|
|
);
|
|
|
|
_ytSubscription?.cancel();
|
|
_ytSubscription = _ytIframeController!.stream.listen((event) {
|
|
|
|
if (event.error != null) {
|
|
print('LessonDetailSheet: Youtube Player Runtime Error: ${event.error}. State: ${event.playerState}. Forçando fallback.');
|
|
// ✅ CORRIGIDO: Garante que o fallback seja chamado apenas se o widget estiver montado.
|
|
Future.microtask(() {
|
|
if(mounted) {
|
|
_activateExternalFallback();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
await Future.delayed(const Duration(milliseconds: 200));
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_useExternalFallback = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print('LessonDetailSheet: falha ao inicializar yt iframe controller: $e');
|
|
_activateExternalFallback();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Tenta VideoPlayer + Chewie
|
|
try {
|
|
_videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(videoUrl));
|
|
await _videoPlayerController!.initialize();
|
|
_chewieController = ChewieController(
|
|
videoPlayerController: _videoPlayerController!,
|
|
autoPlay: true,
|
|
looping: false,
|
|
allowFullScreen: true,
|
|
aspectRatio: _videoPlayerController!.value.isInitialized
|
|
? _videoPlayerController!.value.aspectRatio
|
|
: 16 / 9,
|
|
placeholder: const Center(
|
|
child: CircularProgressIndicator(color: Color(0xFFE94560)),
|
|
),
|
|
errorBuilder: (context, errorMessage) {
|
|
print('LessonDetailSheet: Chewie errorBuilder -> $errorMessage');
|
|
return const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: Text(
|
|
'Erro ao carregar o vídeo. Tente o botão abaixo.',
|
|
style: TextStyle(color: Colors.white),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_useExternalFallback = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print('LessonDetailSheet: falha VideoPlayer/Chewie -> $e');
|
|
_videoPlayerController?.dispose();
|
|
_videoPlayerController = null;
|
|
_chewieController?.dispose();
|
|
_chewieController = null;
|
|
_activateExternalFallback();
|
|
}
|
|
}
|
|
|
|
Widget _buildExternalPlayerFallback() {
|
|
final url = widget.lesson.videoUrl;
|
|
final i18n = Provider.of<I18nService>(context, listen: false);
|
|
|
|
return Container(
|
|
height: 250,
|
|
color: const Color(0xFF1A1A2E),
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.videocam_off, color: Color(0xFFE94560), size: 50),
|
|
const SizedBox(height: 16),
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
|
child: Text(
|
|
'O player nativo falhou (pode ser devido a restrições de rede ou incompatibilidade na Web). Abra o vídeo diretamente no navegador.',
|
|
style: TextStyle(color: Colors.white, fontSize: 16),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () async {
|
|
if (url != null && url.isNotEmpty) {
|
|
final uri = Uri.parse(url);
|
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
} else {
|
|
ToastService.show(
|
|
message: i18n.t('URL de vídeo não disponível.'),
|
|
type: ToastType.error, );
|
|
}
|
|
},
|
|
icon: const Icon(Icons.open_in_new),
|
|
label: const Text('Abrir no Navegador'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFE94560),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildVideoPlayer() {
|
|
return Container(
|
|
height: 250,
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [Color(0xFF553C9A), Color(0xFF282A52)],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
|
),
|
|
child: _isLoading
|
|
? const Center(child: CircularProgressIndicator(color: Color(0xFFE94560)))
|
|
: ClipRRect(
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
|
child: Builder(
|
|
builder: (context) {
|
|
if (_ytIframeController != null) {
|
|
return yt_iframe.YoutubePlayer(
|
|
controller: _ytIframeController!,
|
|
aspectRatio: 16 / 9,
|
|
);
|
|
}
|
|
|
|
if (_chewieController != null) {
|
|
return Chewie(controller: _chewieController!);
|
|
}
|
|
|
|
if (_useExternalFallback) {
|
|
return _buildExternalPlayerFallback();
|
|
}
|
|
|
|
return const Center(
|
|
child: Text(
|
|
'URL de vídeo inválida ou não disponível.',
|
|
style: TextStyle(color: Colors.white),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _downloadFile(String url, String fileName) async {
|
|
final i18n = Provider.of<I18nService>(context, listen: false);
|
|
|
|
// 1. Bloqueio de YouTube
|
|
if (yt_iframe.YoutubePlayerController.convertUrlToId(url) != null) {
|
|
ToastService.show(
|
|
message: i18n.t('Não é possível fazer o download de vídeos do YouTube diretamente.'),
|
|
type: ToastType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (url.isEmpty) {
|
|
ToastService.show(
|
|
message: i18n.t('A URL do recurso não está disponível.'),
|
|
type: ToastType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 2. Lógica para Dispositivos (Android/iOS)
|
|
if (Platform.isAndroid || Platform.isIOS) {
|
|
|
|
// SOLUÇÃO: Pede permissão e obtém o caminho de salvamento
|
|
final status = await Permission.storage.request();
|
|
if (status.isGranted) {
|
|
|
|
String? externalStorageDir;
|
|
|
|
if (Platform.isAndroid) {
|
|
// Em Android modernos, usamos getExternalStorageDirectory()
|
|
// ou getDownloadsDirectory() que é específico para downloads no Android SDK 29+.
|
|
final dir = await getDownloadsDirectory();
|
|
externalStorageDir = dir?.path;
|
|
} else if (Platform.isIOS) {
|
|
// Para iOS, usa o diretório de documentos do aplicativo
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
externalStorageDir = dir.path;
|
|
}
|
|
|
|
|
|
if (externalStorageDir != null) {
|
|
try {
|
|
await FlutterDownloader.enqueue(
|
|
url: url,
|
|
savedDir: externalStorageDir, // <--- NOVO CAMINHO CORRETO
|
|
fileName: fileName,
|
|
showNotification: true,
|
|
openFileFromNotification: true,
|
|
// saveInPublicStorage: true, // Remova este se causar erro, nem sempre é necessário.
|
|
);
|
|
ToastService.show(message: i18n.t('Download iniciado'));
|
|
} catch (e) {
|
|
ToastService.show(
|
|
message: i18n.t('Falha ao iniciar o download: $e'),
|
|
type: ToastType.error,
|
|
);
|
|
}
|
|
} else {
|
|
ToastService.show(
|
|
message: i18n.t('Não foi possível encontrar o diretório de download.'),
|
|
type: ToastType.error,
|
|
);
|
|
}
|
|
} else {
|
|
ToastService.show(
|
|
message: i18n.t('Permissão de armazenamento negada'),
|
|
type: ToastType.error,
|
|
);
|
|
}
|
|
} else {
|
|
// (Lógica da Web / Desktop)
|
|
try {
|
|
if (await canLaunchUrl(Uri.parse(url))) {
|
|
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
|
ToastService.show(message: i18n.t('Download iniciado no navegador'));
|
|
} else {
|
|
ToastService.show(
|
|
message: i18n.t('Não foi possível iniciar o download. Verifique a URL.'),
|
|
type: ToastType.error,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
ToastService.show(
|
|
message: i18n.t('Ocorreu um erro ao iniciar o download: $e'),
|
|
type: ToastType.error,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// --- Widgets de UI ---
|
|
|
|
Widget _buildLessonHeader(I18nService i18n) {
|
|
return ShaderMask(
|
|
shaderCallback: (bounds) => const LinearGradient(
|
|
colors: [Color(0xFFE94560), Color(0xFFF07E3F)],
|
|
).createShader(bounds),
|
|
child: Text(
|
|
widget.lesson.titulo ?? '',
|
|
style: const TextStyle(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLessonContent() {
|
|
return Text(
|
|
widget.lesson.conteudo ?? '',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.white.withOpacity(0.8),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ✅ Mantido o uso de Expanded para garantir que a Row se ajusta
|
|
Widget _buildVideoActions(I18nService i18n) {
|
|
return Row(
|
|
children: [
|
|
// Duração: Ocupa 1/3 do espaço
|
|
Expanded(
|
|
flex: 1,
|
|
child: _buildDurationWidget(i18n),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Download: Ocupa 2/3 do espaço
|
|
Expanded(
|
|
flex: 2,
|
|
child: _buildDownloadVideoButton(i18n),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildBookSection(I18nService i18n) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'Recursos Adicionais',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildReadBookOnlineButton(i18n),
|
|
const SizedBox(height: 16),
|
|
_buildDownloadBookButton(i18n),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ✅ CORREÇÃO FINAL DE LAYOUT: Otimizado para evitar overflow vertical e horizontal
|
|
Widget _buildDurationWidget(I18nService i18n) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF553C9A), Color(0xFF282A52)],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.access_time_rounded, size: 20, color: Colors.white),
|
|
const SizedBox(width: 4),
|
|
// Força o texto a quebrar ou a usar '...' se o espaço no Expanded for muito pequeno
|
|
Flexible(
|
|
child: Text(
|
|
'${i18n.t('duration')}: ${widget.lesson.duracao ?? ''}',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMarkCompleteButton(I18nService i18n) {
|
|
return Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(30),
|
|
gradient: widget.isCompleted
|
|
? const LinearGradient(
|
|
colors: [Colors.green, Colors.greenAccent],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
)
|
|
: const LinearGradient(
|
|
colors: [Color(0xFFE94560), Color(0xFFF07E3F)],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.4),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: ElevatedButton.icon(
|
|
onPressed: widget.isCompleted
|
|
? null
|
|
: () {
|
|
widget.onMarkComplete(widget.lesson);
|
|
Navigator.pop(context);
|
|
ToastService.show(message: i18n.t('lesson_completed'));
|
|
},
|
|
icon: widget.isCompleted
|
|
? const Icon(Icons.check_circle_rounded, color: Colors.white)
|
|
: const Icon(Icons.check_circle_outline_rounded, color: Colors.white),
|
|
label: Text(
|
|
widget.isCompleted ? i18n.t('completed') : i18n.t('mark_as_complete'),
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: Colors.white,
|
|
backgroundColor: Colors.transparent,
|
|
shadowColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(25),
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
elevation: 0,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDownloadVideoButton(I18nService i18n) {
|
|
return InkWell(
|
|
onTap: () {
|
|
if (widget.lesson.videoUrl != null) {
|
|
_downloadFile(widget.lesson.videoUrl!, widget.lesson.titulo ?? 'video');
|
|
} else {
|
|
ToastService.show(
|
|
message: i18n.t('URL do vídeo não disponível.'), type: ToastType.error);
|
|
}
|
|
},
|
|
child: Container(
|
|
height: 50,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(30),
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFFE94560), Color(0xFFF07E3F)],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFFE94560).withOpacity(0.4),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.cloud_download_rounded, color: Colors.white, size: 24),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
i18n.t('download_video'),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildReadBookOnlineButton(I18nService i18n) {
|
|
return InkWell(
|
|
onTap: () {
|
|
if (widget.lesson.bookUrl != null && widget.lesson.bookUrl!.isNotEmpty) {
|
|
if (kIsWeb) {
|
|
launchUrl(Uri.parse(widget.lesson.bookUrl!), mode: LaunchMode.platformDefault);
|
|
} else {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => PdfViewerScreen(pdfUrl: widget.lesson.bookUrl!),
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
ToastService.show(
|
|
message: i18n.t('URL do livro não disponível.'),
|
|
type: ToastType.error,
|
|
);
|
|
}
|
|
},
|
|
child: Container(
|
|
height: 60,
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(30),
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF553C9A), Color(0xFF4D2C8E)],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF553C9A).withOpacity(0.4),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
i18n.t('read_book_online'),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDownloadBookButton(I18nService i18n) {
|
|
return InkWell(
|
|
onTap: () {
|
|
if (widget.lesson.bookUrl != null) {
|
|
_downloadFile(widget.lesson.bookUrl!, widget.lesson.titulo ?? 'livro');
|
|
} else {
|
|
ToastService.show(
|
|
message: i18n.t('URL do livro não disponível.'), type: ToastType.error);
|
|
}
|
|
},
|
|
child: Container(
|
|
height: 60,
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(30),
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF4D2C8E), Color(0xFF553C9A)],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF4D2C8E).withOpacity(0.4),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
i18n.t('download_book'),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Método build principal
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final i18n = Provider.of<I18nService>(context);
|
|
|
|
// O SingleChildScrollView evita que o conteúdo transborde verticalmente
|
|
return SingleChildScrollView(
|
|
child: Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF1A1A2E),
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
|
),
|
|
padding: const EdgeInsets.only(bottom: 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildVideoPlayer(),
|
|
Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildLessonHeader(i18n),
|
|
const SizedBox(height: 8),
|
|
_buildLessonContent(),
|
|
const SizedBox(height: 24),
|
|
_buildVideoActions(i18n),
|
|
const SizedBox(height: 24),
|
|
_buildBookSection(i18n),
|
|
const SizedBox(height: 32),
|
|
_buildMarkCompleteButton(i18n),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |