MVP I
This commit is contained in:
@@ -15,10 +15,36 @@ class BrushingPrefs {
|
|||||||
|
|
||||||
static String _key(String base, String scopeId) => '${base}_$scopeId';
|
static String _key(String base, String scopeId) => '${base}_$scopeId';
|
||||||
|
|
||||||
|
static DateTime _currentWeekMonday() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return DateTime(
|
||||||
|
now.year,
|
||||||
|
now.month,
|
||||||
|
now.day,
|
||||||
|
).subtract(Duration(days: now.weekday - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê as escovagens guardadas e, de caminho, descarta (apaga do
|
||||||
|
/// armazenamento) qualquer registo de semanas anteriores — o contador
|
||||||
|
/// semanal deve zerar a cada nova semana, não só na exibição mas também
|
||||||
|
/// nos dados guardados, para não crescer para sempre.
|
||||||
static Future<List<DateTime>> _getEntries(String scopeId) async {
|
static Future<List<DateTime>> _getEntries(String scopeId) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final raw = prefs.getStringList(_key(_kDatesKey, scopeId)) ?? const [];
|
final key = _key(_kDatesKey, scopeId);
|
||||||
return raw.map(DateTime.tryParse).whereType<DateTime>().toList();
|
final raw = prefs.getStringList(key) ?? const [];
|
||||||
|
final entries = raw.map(DateTime.tryParse).whereType<DateTime>().toList();
|
||||||
|
|
||||||
|
final monday = _currentWeekMonday();
|
||||||
|
final kept = entries
|
||||||
|
.where((d) => !DateTime(d.year, d.month, d.day).isBefore(monday))
|
||||||
|
.toList();
|
||||||
|
if (kept.length != entries.length) {
|
||||||
|
await prefs.setStringList(
|
||||||
|
key,
|
||||||
|
kept.map((d) => d.toIso8601String()).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return kept;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<int> getWeeklyGoal(String scopeId) async {
|
static Future<int> getWeeklyGoal(String scopeId) async {
|
||||||
@@ -51,11 +77,15 @@ class BrushingPrefs {
|
|||||||
/// o utilizador).
|
/// o utilizador).
|
||||||
static Future<void> logToday(String scopeId) async {
|
static Future<void> logToday(String scopeId) async {
|
||||||
if (!(await canLogMore(scopeId))) return;
|
if (!(await canLogMore(scopeId))) return;
|
||||||
|
// Usa as entradas já podadas (só da semana atual) para não ressuscitar
|
||||||
|
// registos de semanas anteriores ao gravar de volta.
|
||||||
|
final entries = await _getEntries(scopeId);
|
||||||
|
entries.add(DateTime.now());
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final key = _key(_kDatesKey, scopeId);
|
await prefs.setStringList(
|
||||||
final list = prefs.getStringList(key) ?? <String>[];
|
_key(_kDatesKey, scopeId),
|
||||||
list.add(DateTime.now().toIso8601String());
|
entries.map((d) => d.toIso8601String()).toList(),
|
||||||
await prefs.setStringList(key, list);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Já atingiu o limite diário de [maxPerDay] escovagens hoje?
|
/// Já atingiu o limite diário de [maxPerDay] escovagens hoje?
|
||||||
@@ -64,21 +94,9 @@ class BrushingPrefs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Conta quantas escovagens foram registadas na semana atual
|
/// Conta quantas escovagens foram registadas na semana atual
|
||||||
/// (segunda-feira a domingo).
|
/// (segunda-feira a domingo). [_getEntries] já descarta semanas
|
||||||
|
/// anteriores, por isso todas as entradas devolvidas já são desta semana.
|
||||||
static Future<int> getWeekCount(String scopeId) async {
|
static Future<int> getWeekCount(String scopeId) async {
|
||||||
final entries = await _getEntries(scopeId);
|
return (await _getEntries(scopeId)).length;
|
||||||
|
|
||||||
final now = DateTime.now();
|
|
||||||
final monday = DateTime(
|
|
||||||
now.year,
|
|
||||||
now.month,
|
|
||||||
now.day,
|
|
||||||
).subtract(Duration(days: now.weekday - 1));
|
|
||||||
|
|
||||||
return entries
|
|
||||||
.where(
|
|
||||||
(d) => !DateTime(d.year, d.month, d.day).isBefore(monday),
|
|
||||||
)
|
|
||||||
.length;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,15 +166,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(color: const Color(0xFFFAFAF7)),
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: -size.width * 0.38,
|
left: -size.width * 0.38,
|
||||||
|
|||||||
1002
lib/logged_home.dart
1002
lib/logged_home.dart
File diff suppressed because it is too large
Load Diff
@@ -40,8 +40,16 @@ class MyApp extends StatelessWidget {
|
|||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2F9E94)),
|
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2F9E94)),
|
||||||
scaffoldBackgroundColor: const Color(0xFFFFE2EF),
|
scaffoldBackgroundColor: const Color(0xFFFAFAF7),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
// Sem isto, o Material 3 aplica por padrão uma sobreposição de cor
|
||||||
|
// (surfaceTintColor) e uma elevação extra ao rolar (scrolledUnderElevation)
|
||||||
|
// por cima de qualquer app bar — o que "lavava" o gradiente customizado
|
||||||
|
// das app bars, fazendo-o parecer uma cor sólida em vez de gradiente.
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
GlobalMaterialLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
|||||||
@@ -148,15 +148,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
|
|||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(color: const Color(0xFFFAFAF7)),
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: -size.width * 0.40,
|
left: -size.width * 0.40,
|
||||||
|
|||||||
@@ -88,13 +88,7 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
color: const Color(0xFFFAFAF7),
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
|
|||||||
@@ -10,38 +10,33 @@ import '../widgets/tap_bounce.dart';
|
|||||||
class CuriosidadeScreen extends StatelessWidget {
|
class CuriosidadeScreen extends StatelessWidget {
|
||||||
const CuriosidadeScreen({super.key});
|
const CuriosidadeScreen({super.key});
|
||||||
|
|
||||||
static const Color _teal = Color(0xFF2F9E94);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: PreferredSize(
|
||||||
backgroundColor: _teal,
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
foregroundColor: Colors.white,
|
child: Container(
|
||||||
elevation: 0,
|
|
||||||
flexibleSpace: Container(
|
|
||||||
decoration: const BoxDecoration(gradient: kAppBarGradient),
|
decoration: const BoxDecoration(gradient: kAppBarGradient),
|
||||||
),
|
child: AppBar(
|
||||||
title: const Text(
|
backgroundColor: Colors.transparent,
|
||||||
'Curiosidades',
|
foregroundColor: Colors.white,
|
||||||
style: TextStyle(fontWeight: FontWeight.w900),
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Curiosidades',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w900),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(color: const Color(0xFFFAFAF7)),
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: -size.width * 0.40,
|
left: -size.width * 0.40,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class _HelloSplashScreenState extends State<HelloSplashScreen> with TickerProvid
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
color: const Color(0xFFFFC9DF),
|
color: const Color(0xFFFAFAF7),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -90,7 +90,7 @@ class _HelloSplashScreenState extends State<HelloSplashScreen> with TickerProvid
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 64,
|
fontSize: 64,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: Colors.white,
|
color: const Color(0xFFFF9AD0),
|
||||||
height: 1.0,
|
height: 1.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,32 +5,30 @@ import '../widgets/app_gradients.dart';
|
|||||||
class TermsScreen extends StatelessWidget {
|
class TermsScreen extends StatelessWidget {
|
||||||
const TermsScreen({super.key});
|
const TermsScreen({super.key});
|
||||||
|
|
||||||
static const Color _teal = Color(0xFF2F9E94);
|
|
||||||
static const Color _accentPink = Color(0xFFFF55A7);
|
static const Color _accentPink = Color(0xFFFF55A7);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: PreferredSize(
|
||||||
backgroundColor: _teal,
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
foregroundColor: Colors.white,
|
child: Container(
|
||||||
elevation: 0,
|
|
||||||
flexibleSpace: Container(
|
|
||||||
decoration: const BoxDecoration(gradient: kAppBarGradient),
|
decoration: const BoxDecoration(gradient: kAppBarGradient),
|
||||||
),
|
child: AppBar(
|
||||||
title: const Text(
|
backgroundColor: Colors.transparent,
|
||||||
'Termos de Serviço',
|
foregroundColor: Colors.white,
|
||||||
style: TextStyle(fontWeight: FontWeight.w900),
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Termos de Serviço',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w900),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
color: const Color(0xFFFAFAF7),
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
|||||||
@@ -38,79 +38,79 @@ final List<VideoData> videoList = [
|
|||||||
VideoData(
|
VideoData(
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'Episódio 1',
|
title: 'Episódio 1',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência do nariz entupido na má oclusão',
|
||||||
youtubeId: 'PJ58CZv4ECw',
|
youtubeId: 'PJ58CZv4ECw',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Episódio 2',
|
title: 'Episódio 2',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência das alergias sazionais na má oclusão',
|
||||||
youtubeId: 'y4_kWmZtAtg',
|
youtubeId: 'y4_kWmZtAtg',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 3,
|
id: 3,
|
||||||
title: 'Episódio 3',
|
title: 'Episódio 3',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência das Otites frequentes na má oclusão',
|
||||||
youtubeId: 'nD75Y5PuKTo',
|
youtubeId: 'nD75Y5PuKTo',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 4,
|
id: 4,
|
||||||
title: 'Episódio 4',
|
title: 'Episódio 4',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência das Amigdalites recorrentes na má oclusão',
|
||||||
youtubeId: 'yvFllWYeuLw',
|
youtubeId: 'yvFllWYeuLw',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 5,
|
id: 5,
|
||||||
title: 'Episódio 5',
|
title: 'Episódio 5',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência das Bronquiolites recorrentes na má oclusão',
|
||||||
youtubeId: 'DnhUa-T8_Ps',
|
youtubeId: 'DnhUa-T8_Ps',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 6,
|
id: 6,
|
||||||
title: 'Episódio 6',
|
title: 'Episódio 6',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência dos problemas respitatórios na má oclusão',
|
||||||
youtubeId: 'zKt_iwkrjvo',
|
youtubeId: 'zKt_iwkrjvo',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 7,
|
id: 7,
|
||||||
title: 'Episódio 7',
|
title: 'Episódio 7',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência das interrupções respiratórias na má oclusão',
|
||||||
youtubeId: 'NpmQ2brap5A',
|
youtubeId: 'NpmQ2brap5A',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 8,
|
id: 8,
|
||||||
title: 'Episódio 8',
|
title: 'Episódio 8',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência do ressonar na má oclusão',
|
||||||
youtubeId: 'Wj3KYw9pBi0',
|
youtubeId: 'Wj3KYw9pBi0',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 9,
|
id: 9,
|
||||||
title: 'Episódio 9',
|
title: 'Episódio 9',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência de acordar com saliva seca na boca ou na almofada na saúde oral',
|
||||||
youtubeId: 'bOm9t61cT_U',
|
youtubeId: 'bOm9t61cT_U',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 10,
|
id: 10,
|
||||||
title: 'Episódio 10',
|
title: 'Episódio 10',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a Influência da respiração oral na má oclusão',
|
||||||
youtubeId: 'fAitMizbcms',
|
youtubeId: 'fAitMizbcms',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 11,
|
id: 11,
|
||||||
title: 'Episódio 11',
|
title: 'Episódio 11',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a influência do uso exagerado da chupeta na má oclusão',
|
||||||
youtubeId: '6sYoBUjks_I',
|
youtubeId: '6sYoBUjks_I',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 12,
|
id: 12,
|
||||||
title: 'Episódio 12',
|
title: 'Episódio 12',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a influência do uso exagerado da chupeta na má oclusão',
|
||||||
youtubeId: 'eznKrErQbHo',
|
youtubeId: 'eznKrErQbHo',
|
||||||
),
|
),
|
||||||
VideoData(
|
VideoData(
|
||||||
id: 13,
|
id: 13,
|
||||||
title: 'Episódio 13',
|
title: 'Episódio 13',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
description: 'Qual a influência do hábito de chuchar o dedo na má oclusão',
|
||||||
youtubeId: 'VO9CNqHRdeM',
|
youtubeId: 'VO9CNqHRdeM',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -236,32 +236,32 @@ class _VideoScreenState extends State<VideoScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: PreferredSize(
|
||||||
backgroundColor: VideoScreen._teal,
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
foregroundColor: Colors.white,
|
// O gradiente é pintado por este Container de tamanho fixo, em vez
|
||||||
surfaceTintColor: VideoScreen._teal,
|
// de confiar no `flexibleSpace` do AppBar — em alguns aparelhos o
|
||||||
elevation: 0,
|
// `flexibleSpace` de um AppBar comum não recebia o tamanho esperado
|
||||||
flexibleSpace: Container(
|
// e a gradiente aparecia como cor sólida.
|
||||||
|
child: Container(
|
||||||
decoration: const BoxDecoration(gradient: kAppBarGradient),
|
decoration: const BoxDecoration(gradient: kAppBarGradient),
|
||||||
),
|
child: AppBar(
|
||||||
title: const Text(
|
backgroundColor: Colors.transparent,
|
||||||
'Videos Educativos',
|
foregroundColor: Colors.white,
|
||||||
style: TextStyle(fontWeight: FontWeight.w900),
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Videos Educativos',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w900),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(color: const Color(0xFFFAFAF7)),
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: -size.width * 0.40,
|
left: -size.width * 0.40,
|
||||||
@@ -548,27 +548,84 @@ class _VideoButton extends StatelessWidget {
|
|||||||
return TapBounce(
|
return TapBounce(
|
||||||
scale: 0.95,
|
scale: 0.95,
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 8,
|
elevation: 10,
|
||||||
shadowColor: Colors.black.withValues(alpha: 0.12),
|
shadowColor: Colors.black.withValues(alpha: 0.18),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(18),
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(18),
|
||||||
onTap: () => _showVideoPlayer(context, video),
|
onTap: () => _showVideoPlayer(context, video),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(10),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
AspectRatio(
|
||||||
height: 80,
|
aspectRatio: 16 / 9,
|
||||||
decoration: BoxDecoration(
|
child: ClipRRect(
|
||||||
color: const Color(0xFFFFE6F1),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderRadius: BorderRadius.circular(12),
|
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],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: VideoThumbnail(video: video),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
video.title,
|
video.title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|||||||
@@ -27,10 +27,4 @@ class WatchedVideosPrefs {
|
|||||||
final ids = prefs.getStringList(_key(scopeId)) ?? const [];
|
final ids = prefs.getStringList(_key(scopeId)) ?? const [];
|
||||||
return ids.length;
|
return ids.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Set<int>> getWatchedIds(String scopeId) async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
final ids = prefs.getStringList(_key(scopeId)) ?? const [];
|
|
||||||
return ids.map((e) => int.tryParse(e) ?? -1).where((e) => e >= 0).toSet();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Gradiente usado em todas as app bars da aplicação.
|
/// Gradiente usado em todas as app bars da aplicação. Direção puramente
|
||||||
|
/// horizontal (não diagonal) de propósito: a app bar retrátil da Home é
|
||||||
|
/// alta (~190) enquanto as das outras abas são baixas (~56) — um gradiente
|
||||||
|
/// diagonal mostra proporções de cor bem diferentes consoante a altura,
|
||||||
|
/// fazendo a Home parecer mais "cheia de cor" que as outras. Na horizontal,
|
||||||
|
/// a transição depende só da largura (igual em todas as app bars), por
|
||||||
|
/// isso todas ficam visualmente idênticas.
|
||||||
const LinearGradient kAppBarGradient = LinearGradient(
|
const LinearGradient kAppBarGradient = LinearGradient(
|
||||||
begin: Alignment.topRight,
|
|
||||||
end: Alignment.bottomLeft,
|
|
||||||
colors: [Color(0xFF6BB79F), Color(0xFF6BB79F)],
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Gradiente usado nos botões verdes da aplicação — um toque da cor da app
|
|
||||||
/// bar (#6BB79F) misturado com o teal original.
|
|
||||||
const LinearGradient kGreenButtonGradient = LinearGradient(
|
|
||||||
begin: Alignment.centerLeft,
|
begin: Alignment.centerLeft,
|
||||||
end: Alignment.centerRight,
|
end: Alignment.centerRight,
|
||||||
colors: [Color(0xFF2F9E94), Color(0xFF6BB79F)],
|
colors: [Color(0xFF1C7A6E), Color(0xFF8FD4BB)],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Cor sólida (sem gradiente, de propósito) usada nos botões e cards verdes
|
||||||
|
/// da aplicação — botões de "Entrar"/"Criar conta", "Adicionar criança" e o
|
||||||
|
/// card "Vídeos educativos". Só as app bars usam gradiente ([kAppBarGradient]);
|
||||||
|
/// mantém-se como [LinearGradient] com as duas cores iguais para não obrigar
|
||||||
|
/// a mudar todos os `decoration: gradient: kGreenButtonGradient` existentes.
|
||||||
|
const LinearGradient kGreenButtonGradient = LinearGradient(
|
||||||
|
colors: [Color(0xFF2F9E94), Color(0xFF2F9E94)],
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Gradiente rosa vivo do card do quiz na Home.
|
/// Gradiente rosa vivo do card do quiz na Home.
|
||||||
|
|||||||
Reference in New Issue
Block a user