Atualização de gauge
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
||||
|
||||
import '../watched_videos_prefs.dart';
|
||||
import '../widgets/app_gradients.dart';
|
||||
import '../widgets/entrance.dart';
|
||||
import '../widgets/liquid_waves_background.dart';
|
||||
import '../widgets/pill_snackbar.dart';
|
||||
import '../widgets/tap_bounce.dart';
|
||||
|
||||
@@ -62,7 +61,8 @@ final List<VideoData> videoList = [
|
||||
VideoData(
|
||||
id: 5,
|
||||
title: 'Episódio 5',
|
||||
description: 'Qual a Influência das Bronquiolites recorrentes na má oclusão',
|
||||
description:
|
||||
'Qual a Influência das Bronquiolites recorrentes na má oclusão',
|
||||
youtubeId: 'DnhUa-T8_Ps',
|
||||
),
|
||||
VideoData(
|
||||
@@ -74,7 +74,8 @@ final List<VideoData> videoList = [
|
||||
VideoData(
|
||||
id: 7,
|
||||
title: 'Episódio 7',
|
||||
description: 'Qual a Influência das interrupções respiratórias na má oclusão',
|
||||
description:
|
||||
'Qual a Influência das interrupções respiratórias na má oclusão',
|
||||
youtubeId: 'NpmQ2brap5A',
|
||||
),
|
||||
VideoData(
|
||||
@@ -86,7 +87,8 @@ final List<VideoData> videoList = [
|
||||
VideoData(
|
||||
id: 9,
|
||||
title: 'Episódio 9',
|
||||
description: 'Qual a Influência de acordar com saliva seca na boca ou na almofada na saúde oral',
|
||||
description:
|
||||
'Qual a Influência de acordar com saliva seca na boca ou na almofada na saúde oral',
|
||||
youtubeId: 'bOm9t61cT_U',
|
||||
),
|
||||
VideoData(
|
||||
@@ -179,7 +181,8 @@ Future<void> showVideoPlayerDialog(
|
||||
}
|
||||
return Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _YoutubePlayerPage(video: video, scopeId: scopeId),
|
||||
builder: (context) =>
|
||||
_YoutubePlayerPage(video: video, scopeId: scopeId),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -234,7 +237,6 @@ class _VideoScreenState extends State<VideoScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
@@ -260,30 +262,8 @@ class _VideoScreenState extends State<VideoScreen> {
|
||||
body: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(color: const Color(0xFFFAFAF7)),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.40,
|
||||
bottom: -size.width * 0.45,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))),
|
||||
const LiquidWavesBackground(),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -341,9 +321,7 @@ class _VideoScreenState extends State<VideoScreen> {
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
return FadeSlideIn(
|
||||
delay: Duration(
|
||||
milliseconds: 40 * (index % 8),
|
||||
),
|
||||
delay: Duration(milliseconds: 40 * (index % 8)),
|
||||
child: _VideoButton(
|
||||
video: _filteredVideos[index],
|
||||
scopeId: widget.scopeId,
|
||||
@@ -561,70 +539,73 @@ class _VideoButton extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ColoredBox(
|
||||
color: const Color(0xFFFFE6F1),
|
||||
child: VideoThumbnail(video: video, borderRadius: 0),
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.32),
|
||||
],
|
||||
stops: const [0.55, 1.0],
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ColoredBox(
|
||||
color: const Color(0xFFFFE6F1),
|
||||
child: VideoThumbnail(
|
||||
video: video,
|
||||
borderRadius: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: VideoScreen._accentPink,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 6,
|
||||
bottom: 6,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'EP. ${video.id}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 9.5,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.32),
|
||||
],
|
||||
stops: const [0.55, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Center(
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: VideoScreen._accentPink,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 6,
|
||||
bottom: 6,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'EP. ${video.id}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 9.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -718,7 +699,9 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
|
||||
PlatformDispatcher.instance.views.first.physicalSize.width >
|
||||
PlatformDispatcher.instance.views.first.physicalSize.height;
|
||||
if (isLandscape == _controller.value.isFullScreen) return;
|
||||
_controller.updateValue(_controller.value.copyWith(isFullScreen: isLandscape));
|
||||
_controller.updateValue(
|
||||
_controller.value.copyWith(isFullScreen: isLandscape),
|
||||
);
|
||||
if (isLandscape) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
} else {
|
||||
@@ -782,12 +765,21 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: YoutubePlayer(controller: _controller),
|
||||
DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: kAppBarGradient,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: YoutubePlayer(controller: _controller),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.fromLTRB(20, 14, 20, 0),
|
||||
child: _VideoProgressRow(controller: _controller),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -809,6 +801,65 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
if (_nextVideos.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Próximos',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
color: VideoScreen._accentPink,
|
||||
),
|
||||
),
|
||||
if (_hasMoreVideos)
|
||||
TapBounce(
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(
|
||||
999,
|
||||
),
|
||||
onTap: () =>
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => VideoScreen(
|
||||
scopeId: widget.scopeId,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 2,
|
||||
),
|
||||
child: Text(
|
||||
'Ver mais',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13,
|
||||
color: VideoScreen._teal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
for (
|
||||
var i = 0;
|
||||
i < _visibleNextVideos.length;
|
||||
i++
|
||||
) ...[
|
||||
if (i > 0) const SizedBox(height: 10),
|
||||
_VideoButton(
|
||||
video: _visibleNextVideos[i],
|
||||
scopeId: widget.scopeId,
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -820,6 +871,85 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static const int _maxNextVideos = 4;
|
||||
|
||||
List<VideoData> get _nextVideos =>
|
||||
videoList.where((v) => v.id > widget.video.id).toList();
|
||||
|
||||
List<VideoData> get _visibleNextVideos =>
|
||||
_nextVideos.take(_maxNextVideos).toList();
|
||||
|
||||
bool get _hasMoreVideos => _nextVideos.length > _maxNextVideos;
|
||||
}
|
||||
|
||||
/// Barra de progresso do vídeo (posição atual / duração total), separada do
|
||||
/// player — a mesma que aparece por baixo do vídeo no design de referência.
|
||||
class _VideoProgressRow extends StatelessWidget {
|
||||
const _VideoProgressRow({required this.controller});
|
||||
|
||||
final YoutubePlayerController controller;
|
||||
|
||||
String _format(Duration d) {
|
||||
final minutes = d.inMinutes.remainder(60).toString();
|
||||
final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<YoutubePlayerValue>(
|
||||
valueListenable: controller,
|
||||
builder: (context, value, _) {
|
||||
final duration = value.metaData.duration;
|
||||
final position = value.position;
|
||||
final progress = duration.inMilliseconds > 0
|
||||
? (position.inMilliseconds / duration.inMilliseconds).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
: 0.0;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 5,
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.10),
|
||||
valueColor: const AlwaysStoppedAnimation(
|
||||
VideoScreen._accentPink,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_format(position),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_format(duration),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Preenche todo o espaço disponível recortando o vídeo (mantém a proporção
|
||||
@@ -1059,8 +1189,7 @@ class _VideoControlsState extends State<_VideoControls> {
|
||||
color: Colors.white,
|
||||
),
|
||||
tooltip: 'Retroceder 10s',
|
||||
onPressed: () =>
|
||||
_seekBy(const Duration(seconds: -10)),
|
||||
onPressed: () => _seekBy(const Duration(seconds: -10)),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
|
||||
Reference in New Issue
Block a user