CTK 1.2.0

This commit is contained in:
Carlos Correia
2026-07-30 00:53:51 +01:00
parent 89b27e5e86
commit fc875d559c
7 changed files with 298 additions and 311 deletions

View File

@@ -12,8 +12,7 @@ class Clinic {
required this.lat, required this.lat,
required this.lon, required this.lon,
required this.address, required this.address,
required this.phone, required this.phoneNumbers,
required this.hasPhone,
required this.openingHours, required this.openingHours,
required this.hasOpeningHours, required this.hasOpeningHours,
this.distanceKm, this.distanceKm,
@@ -24,11 +23,17 @@ class Clinic {
final double lat; final double lat;
final double lon; final double lon;
final String address; final String address;
final String phone;
final bool hasPhone; /// Um consultório na OSM pode ter mais do que um contacto na mesma tag
/// `phone` (separados por `;`/`,`) — já vêm separados aqui, para quem usa
/// isto poder oferecer escolha em vez de tentar ligar para o texto todo
/// de uma vez. Lista vazia quando não há telefone.
final List<String> phoneNumbers;
final String openingHours; final String openingHours;
final bool hasOpeningHours; final bool hasOpeningHours;
bool get hasPhone => phoneNumbers.isNotEmpty;
/// Distância (km) até à morada guardada — só preenchida depois de /// Distância (km) até à morada guardada — só preenchida depois de
/// [OverpassService.fetchNearbyClinics] a calcular; `null` até lá. /// [OverpassService.fetchNearbyClinics] a calcular; `null` até lá.
final double? distanceKm; final double? distanceKm;
@@ -40,8 +45,7 @@ class Clinic {
lat: lat, lat: lat,
lon: lon, lon: lon,
address: address, address: address,
phone: phone, phoneNumbers: phoneNumbers,
hasPhone: hasPhone,
openingHours: openingHours, openingHours: openingHours,
hasOpeningHours: hasOpeningHours, hasOpeningHours: hasOpeningHours,
distanceKm: distanceKm, distanceKm: distanceKm,
@@ -76,10 +80,16 @@ class Clinic {
(tags['addr:city'] ?? tags['addr:suburb']), (tags['addr:city'] ?? tags['addr:suburb']),
].whereType<String>().where((s) => s.trim().isNotEmpty).toList(); ].whereType<String>().where((s) => s.trim().isNotEmpty).toList();
final phone = (tags['phone'] as String?)?.trim() ?? final phoneRaw = (tags['phone'] as String?)?.trim() ??
(tags['contact:phone'] as String?)?.trim(); (tags['contact:phone'] as String?)?.trim();
final phoneNumbers = (phoneRaw == null || phoneRaw.isEmpty)
? const <String>[]
: phoneRaw
.split(RegExp(r'[;,]'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
final openingHours = (tags['opening_hours'] as String?)?.trim(); final openingHours = (tags['opening_hours'] as String?)?.trim();
final hasPhone = phone != null && phone.isNotEmpty;
final hasOpeningHours = openingHours != null && openingHours.isNotEmpty; final hasOpeningHours = openingHours != null && openingHours.isNotEmpty;
return Clinic( return Clinic(
@@ -92,8 +102,7 @@ class Clinic {
address: addressParts.isNotEmpty address: addressParts.isNotEmpty
? addressParts.join('') ? addressParts.join('')
: ConsultoriosStrings.noAddress, : ConsultoriosStrings.noAddress,
phone: hasPhone ? phone : ConsultoriosStrings.noPhone, phoneNumbers: phoneNumbers,
hasPhone: hasPhone,
openingHours: hasOpeningHours openingHours: hasOpeningHours
? openingHours ? openingHours
: ConsultoriosStrings.noOpeningHours, : ConsultoriosStrings.noOpeningHours,

View File

@@ -23,8 +23,8 @@ class ClinicCard extends StatelessWidget {
final bool isFavorite; final bool isFavorite;
final ValueChanged<String> onToggleFavorite; final ValueChanged<String> onToggleFavorite;
Future<void> _call(BuildContext context) async { Future<void> _call(BuildContext context, String number) async {
final uri = Uri(scheme: 'tel', path: clinic.phone); final uri = Uri(scheme: 'tel', path: number);
try { try {
final launched = await launchUrl(uri); final launched = await launchUrl(uri);
if (!launched && context.mounted) { if (!launched && context.mounted) {
@@ -35,6 +35,82 @@ class ClinicCard extends StatelessWidget {
} }
} }
/// Um consultório na OSM pode listar vários contactos na mesma tag — em
/// vez de tentar ligar para o texto todo (o que nem é um número válido),
/// mostra-se este seletor para escolher qual dos números ligar.
Future<void> _showNumberPicker(BuildContext context) async {
final chosen = await showModalBottomSheet<String>(
context: context,
showDragHandle: true,
backgroundColor: AppColors.pinkBackground,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(18, 4, 18, 18),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
ConsultoriosStrings.chooseNumberTitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w900,
color: AppColors.pink,
),
),
const SizedBox(height: 14),
for (final number in clinic.phoneNumbers) ...[
TapBounce(
scale: 0.98,
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () => Navigator.of(ctx).pop(number),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
child: Row(
children: [
const Icon(
Icons.call_rounded,
color: AppColors.teal,
size: 18,
),
const SizedBox(width: 10),
Expanded(
child: Text(
number,
style: const TextStyle(
fontWeight: FontWeight.w800,
),
),
),
],
),
),
),
),
),
const SizedBox(height: 10),
],
],
),
),
);
},
);
if (chosen != null && context.mounted) await _call(context, chosen);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@@ -56,15 +132,6 @@ class ClinicCard extends StatelessWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: AppColors.pinkBackground,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -132,36 +199,57 @@ class ClinicCard extends StatelessWidget {
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TapBounce( _buildPhoneButton(context),
],
),
);
}
Widget _buildPhoneButton(BuildContext context) {
final numbers = clinic.phoneNumbers;
final String label;
final VoidCallback? onTap;
if (numbers.isEmpty) {
label = ConsultoriosStrings.noPhone;
onTap = null;
} else if (numbers.length == 1) {
label = numbers.first;
onTap = () => _call(context, numbers.first);
} else {
label = ConsultoriosStrings.chooseNumber(numbers.length);
onTap = () => _showNumberPicker(context);
}
final enabled = onTap != null;
final color = enabled ? Colors.white : Colors.black.withValues(alpha: 0.35);
return TapBounce(
child: Material( child: Material(
color: clinic.hasPhone color: enabled ? AppColors.teal : Colors.black.withValues(alpha: 0.06),
? AppColors.teal
: Colors.black.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
onTap: clinic.hasPhone ? () => _call(context) : null, onTap: onTap,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 12),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Icons.call_rounded, enabled ? Icons.call_rounded : Icons.call_outlined,
size: 16, size: 16,
color: clinic.hasPhone color: color,
? Colors.white
: Colors.black.withValues(alpha: 0.35),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Flexible(
clinic.phone, child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 13, fontSize: 13,
color: clinic.hasPhone color: color,
? Colors.white ),
: Colors.black.withValues(alpha: 0.35),
), ),
), ),
], ],
@@ -169,9 +257,6 @@ class ClinicCard extends StatelessWidget {
), ),
), ),
), ),
),
],
),
); );
} }
} }

View File

@@ -83,9 +83,6 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
QuizResultData? _lastResult; QuizResultData? _lastResult;
int? _brushingWeekCount;
int _weeklyGoal = BrushingPrefs.defaultWeeklyGoal;
bool _brushingDailyLimitReached = false;
int? _watchedVideoCount; int? _watchedVideoCount;
String _cachedUserName = HomeStrings.noName; String _cachedUserName = HomeStrings.noName;
@@ -124,32 +121,21 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
setState(() => _favoriteClinicIds = ids); setState(() => _favoriteClinicIds = ids);
} }
/// Recarrega os dados locais de escovagem e vídeos assistidos da criança /// Recarrega a contagem de vídeos assistidos da criança atualmente
/// atualmente selecionada. Chamado ao trocar de criança, ao editar a meta /// selecionada. Chamado ao trocar de criança e depois de assistir um vídeo
/// semanal e depois de registar uma escovagem ou um vídeo completo. /// completo.
Future<void> refreshStats() async { Future<void> refreshStats() async {
final scope = (_selectedChildScopeId ?? '').trim(); final scope = (_selectedChildScopeId ?? '').trim();
if (scope.isEmpty) { if (scope.isEmpty) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() => _watchedVideoCount = null);
_brushingWeekCount = null;
_weeklyGoal = BrushingPrefs.defaultWeeklyGoal;
_brushingDailyLimitReached = false;
_watchedVideoCount = null;
});
return; return;
} }
final weekCount = await BrushingPrefs.getWeekCount(scope);
final goal = await BrushingPrefs.getWeeklyGoal(scope);
final dailyLimitReached = await BrushingPrefs.hasReachedDailyLimit(scope);
final watchedCount = await WatchedVideosPrefs.getWatchedCount(scope); final watchedCount = await WatchedVideosPrefs.getWatchedCount(scope);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_brushingWeekCount = weekCount;
_weeklyGoal = goal;
_brushingDailyLimitReached = dailyLimitReached;
_watchedVideoCount = watchedCount; _watchedVideoCount = watchedCount;
}); });
} }
@@ -458,9 +444,14 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
bottom: Radius.circular(40), bottom: Radius.circular(40),
), ),
), ),
flexibleSpace: FlexibleSpaceBar( // Nota: o gradiente é pintado diretamente no slot `flexibleSpace`
titlePadding: EdgeInsets.zero, // em vez de usar `FlexibleSpaceBar.background` — o FlexibleSpaceBar
background: ClipRRect( // aplica um fade de opacidade e um deslocamento em parallax ao seu
// `background` à medida que a barra colapsa, o que fazia o
// gradiente parecer diferente (e perder o border radius) a meio do
// scroll. Colocando o gradiente diretamente aqui, ele ocupa sempre
// exatamente a altura atual da barra, sem fade nem parallax.
flexibleSpace: ClipRRect(
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(40), bottom: Radius.circular(40),
), ),
@@ -518,7 +509,6 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
), ),
), ),
), ),
),
title: Padding( title: Padding(
padding: const EdgeInsets.only(left: 16, right: 10), padding: const EdgeInsets.only(left: 16, right: 10),
child: TapBounce( child: TapBounce(
@@ -977,18 +967,6 @@ class _InicioTab extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(height: 16),
FadeSlideIn(
delay: const Duration(milliseconds: 70),
child: _StatsRow(
brushingCount: state?._brushingWeekCount,
weeklyGoal:
state?._weeklyGoal ?? BrushingPrefs.defaultWeeklyGoal,
brushedToday: state?._brushingDailyLimitReached ?? false,
watchedCount: state?._watchedVideoCount,
onTapBrushing: () => _logBrushing(context, state, scopeId),
),
),
const SizedBox(height: 20), const SizedBox(height: 20),
FadeSlideIn( FadeSlideIn(
delay: const Duration(milliseconds: 110), delay: const Duration(milliseconds: 110),
@@ -1068,34 +1046,6 @@ class _InicioTab extends StatelessWidget {
), ),
); );
} }
Future<void> _logBrushing(
BuildContext context,
_LoggedHomeScreenState? state,
String? scopeId,
) async {
final scope = (scopeId ?? '').trim();
if (scope.isEmpty) {
final uid = (supabase.auth.currentUser?.id ?? '').trim();
if (uid.isEmpty) return;
await _requireFirstChild(context, uid);
return;
}
if (!(await BrushingPrefs.canLogMore(scope))) {
if (context.mounted) {
showPillSnackBar(
context,
HomeStrings.brushingLimitReached(BrushingPrefs.maxPerDay),
);
}
return;
}
await BrushingPrefs.logToday(scope);
await state?.refreshStats();
if (context.mounted) {
showPillSnackBar(context, HomeStrings.brushingLogged);
}
}
} }
/// Faz [child] "respirar" (escala sobe e desce suavemente, em loop) — usado /// Faz [child] "respirar" (escala sobe e desce suavemente, em loop) — usado
@@ -1164,146 +1114,6 @@ class _HomeSectionLabel extends StatelessWidget {
} }
} }
class _StatsRow extends StatelessWidget {
const _StatsRow({
required this.brushingCount,
required this.weeklyGoal,
required this.brushedToday,
required this.watchedCount,
required this.onTapBrushing,
});
final int? brushingCount;
final int weeklyGoal;
final bool brushedToday;
final int? watchedCount;
final VoidCallback onTapBrushing;
@override
Widget build(BuildContext context) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: TapBounce(
scale: 0.97,
child: _StatCard(
icon: Icons.brush_rounded,
iconColor: AppColors.pink,
value: brushingCount == null
? '--'
: '$brushingCount/$weeklyGoal',
label: HomeStrings.brushingThisWeek,
done: brushedToday,
onTap: onTapBrushing,
),
),
),
const SizedBox(width: 12),
Expanded(
child: _StatCard(
icon: Icons.movie_filter_rounded,
iconColor: AppColors.purple,
value: watchedCount == null ? '--' : '$watchedCount',
label: HomeStrings.completedEpisodes,
),
),
],
),
);
}
}
class _StatCard extends StatelessWidget {
const _StatCard({
required this.icon,
required this.iconColor,
required this.value,
required this.label,
this.onTap,
this.done = false,
});
final IconData icon;
final Color iconColor;
final String value;
final String label;
final VoidCallback? onTap;
final bool done;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
elevation: 8,
shadowColor: Colors.black.withValues(alpha: 0.10),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: iconColor.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: iconColor, size: 20),
),
if (done)
Positioned(
top: -4,
right: -4,
child: Container(
width: 16,
height: 16,
decoration: const BoxDecoration(
color: AppColors.teal,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_rounded,
size: 11,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 10),
Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.6),
),
),
],
),
),
),
);
}
}
Future<Map<String, dynamic>?> _createChildViaSheet( Future<Map<String, dynamic>?> _createChildViaSheet(
BuildContext context, BuildContext context,
String uid, String uid,

View File

@@ -36,7 +36,7 @@ class _HelloSplashScreenState extends State<HelloSplashScreen> with TickerProvid
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 250),
); );
_opacity = CurvedAnimation( _opacity = CurvedAnimation(

View File

@@ -8,6 +8,7 @@ import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../watched_videos_prefs.dart'; import '../watched_videos_prefs.dart';
import '../colors/app_gradients.dart'; import '../colors/app_gradients.dart';
import '../strings/home_strings.dart';
import '../strings/video_strings.dart'; import '../strings/video_strings.dart';
import '../widgets/entrance.dart'; import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
@@ -213,12 +214,25 @@ class VideoScreen extends StatefulWidget {
class _VideoScreenState extends State<VideoScreen> { class _VideoScreenState extends State<VideoScreen> {
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
List<VideoData> _filteredVideos = videoList; List<VideoData> _filteredVideos = videoList;
int? _watchedCount;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_filteredVideos = videoList; _filteredVideos = videoList;
_searchController.addListener(_onSearchChanged); _searchController.addListener(_onSearchChanged);
_loadWatchedCount();
}
Future<void> _loadWatchedCount() async {
final scope = (widget.scopeId ?? '').trim();
if (scope.isEmpty) {
if (mounted) setState(() => _watchedCount = null);
return;
}
final count = await WatchedVideosPrefs.getWatchedCount(scope);
if (!mounted) return;
setState(() => _watchedCount = count);
} }
@override @override
@@ -302,6 +316,10 @@ class _VideoScreenState extends State<VideoScreen> {
), ),
), ),
), ),
if ((widget.scopeId ?? '').trim().isNotEmpty) ...[
const SizedBox(height: 12),
_CompletedEpisodesCard(count: _watchedCount),
],
const SizedBox(height: 16), const SizedBox(height: 16),
// Lista vertical (um vídeo abaixo do outro, rolando para // Lista vertical (um vídeo abaixo do outro, rolando para
// baixo), mas cada card em si é horizontal — miniatura à // baixo), mas cada card em si é horizontal — miniatura à
@@ -329,6 +347,7 @@ class _VideoScreenState extends State<VideoScreen> {
child: _VideoButton( child: _VideoButton(
video: _filteredVideos[index], video: _filteredVideos[index],
scopeId: widget.scopeId, scopeId: widget.scopeId,
onVideoClosed: _loadWatchedCount,
), ),
); );
}, },
@@ -344,6 +363,66 @@ class _VideoScreenState extends State<VideoScreen> {
} }
} }
/// Cartão com o número de episódios já assistidos até ao fim pela criança
/// selecionada — vivia antes na Home como um dos dois "stat cards", mas
/// mudou-se para aqui (a própria biblioteca de vídeos) por ser mais
/// relevante neste contexto.
class _CompletedEpisodesCard extends StatelessWidget {
const _CompletedEpisodesCard({required this.count});
final int? count;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.purple.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.movie_filter_rounded,
color: AppColors.purple,
size: 20,
),
),
const SizedBox(width: 12),
Text(
count == null ? '--' : '$count',
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 20),
),
const SizedBox(width: 8),
Text(
HomeStrings.completedEpisodes,
style: TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.6),
),
),
],
),
);
}
}
/// Preview de um vídeo (frame local ou thumbnail do YouTube), reutilizável /// Preview de um vídeo (frame local ou thumbnail do YouTube), reutilizável
/// em qualquer card que precise mostrar "a cara" de um episódio. /// em qualquer card que precise mostrar "a cara" de um episódio.
class VideoThumbnail extends StatefulWidget { class VideoThumbnail extends StatefulWidget {
@@ -509,18 +588,20 @@ class _VideoThumbnailState extends State<VideoThumbnail> {
} }
class _VideoButton extends StatelessWidget { class _VideoButton extends StatelessWidget {
const _VideoButton({required this.video, this.scopeId}); const _VideoButton({required this.video, this.scopeId, this.onVideoClosed});
final VideoData video; final VideoData video;
final String? scopeId; final String? scopeId;
final VoidCallback? onVideoClosed;
void _showVideoPlayer(BuildContext context, VideoData video) { Future<void> _showVideoPlayer(BuildContext context, VideoData video) async {
if (video.youtubeId == null) { if (video.youtubeId == null) {
// Liberta todos os decodificadores de prévia antes de abrir o player // Liberta todos os decodificadores de prévia antes de abrir o player
// em dialog, que precisa dos seus próprios decoders de vídeo/áudio. // em dialog, que precisa dos seus próprios decoders de vídeo/áudio.
_evictAllVideoControllers(); _evictAllVideoControllers();
} }
showVideoPlayerDialog(context, video, scopeId: scopeId); await showVideoPlayerDialog(context, video, scopeId: scopeId);
onVideoClosed?.call();
} }
@override @override

View File

@@ -6,7 +6,7 @@ class ConsultoriosStrings {
static const String navConsultorios = 'Consultórios'; static const String navConsultorios = 'Consultórios';
static const String pageTitle = 'Consultórios'; static const String pageTitle = 'Consultórios';
static String _localCount(int count) => '$count locais${count == 1 ? '' : 'is'}'; static String _localCount(int count) => '$count loca${count == 1 ? 'l' : 'is'}';
static String sectionNear(int count) => 'A menos de 5km · ${_localCount(count)}'; static String sectionNear(int count) => 'A menos de 5km · ${_localCount(count)}';
static String sectionFar(int count) => 'Mais distantes · ${_localCount(count)}'; static String sectionFar(int count) => 'Mais distantes · ${_localCount(count)}';
@@ -18,6 +18,8 @@ class ConsultoriosStrings {
static const String noPhoneAppAvailable = 'Não foi possível abrir a app de telefone'; static const String noPhoneAppAvailable = 'Não foi possível abrir a app de telefone';
static String callFailed(Object e) => 'Não foi possível ligar: $e'; static String callFailed(Object e) => 'Não foi possível ligar: $e';
static const String distanceKm = 'km'; static const String distanceKm = 'km';
static String chooseNumber(int count) => 'Escolher número ($count)';
static const String chooseNumberTitle = 'Para que número quer ligar?';
static const String noAddressTitle = 'Ainda não registou uma morada'; static const String noAddressTitle = 'Ainda não registou uma morada';
static const String noAddressMessage = static const String noAddressMessage =

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 1.2.0
environment: environment:
sdk: ^3.10.4 sdk: ^3.10.4