Files
SpotiFLAC-Mobile/lib/widgets/app_announcement_dialog.dart
zarzet 1cd668c869 l10n: localize library settings and announcements
Move hardcoded strings in library settings, announcement link errors,
and unknown track title/artist fallbacks to AppLocalizations. Sync
locale-dependent fallback strings from MainShell.
2026-06-30 05:10:34 +07:00

132 lines
3.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/services/app_remote_config_service.dart';
class AppAnnouncementDialog extends StatelessWidget {
final RemoteAnnouncement announcement;
final VoidCallback onDismiss;
const AppAnnouncementDialog({
super.key,
required this.announcement,
required this.onDismiss,
});
void _close(BuildContext context) {
onDismiss();
Navigator.pop(context);
}
Future<void> _openCta(BuildContext context) async {
final ctaUrl = announcement.ctaUrl;
if (ctaUrl == null || ctaUrl.isEmpty) {
_showCtaOpenFailed(context);
return;
}
final uri = Uri.tryParse(ctaUrl);
if (uri == null) {
_showCtaOpenFailed(context);
return;
}
bool launched;
try {
launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (_) {
launched = false;
}
if (!context.mounted) return;
if (!launched) {
_showCtaOpenFailed(context);
return;
}
onDismiss();
Navigator.pop(context);
}
void _showCtaOpenFailed(BuildContext context) {
if (!context.mounted) return;
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
SnackBar(content: Text(context.l10n.announcementUnableToOpenLink)),
);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isUrgent = announcement.priority.toLowerCase() == 'high';
// The notice can only be closed through an explicit affordance: never by
// tapping the scrim or the system back button (handled by the barrier and
// PopScope below). Always keep at least one way out (the X). A
// non-dismissible announcement may omit the X only when it carries a CTA,
// so the user can never get permanently trapped.
final showCloseButton = announcement.dismissible || !announcement.hasCta;
final actions = <Widget>[
if (announcement.hasCta)
FilledButton(
onPressed: () => _openCta(context),
child: Text(announcement.ctaLabel!),
),
];
return PopScope(
canPop: false,
child: AlertDialog(
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
isUrgent ? Icons.priority_high_rounded : Icons.campaign_rounded,
color: isUrgent ? colorScheme.error : colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(announcement.title),
),
),
if (showCloseButton)
IconButton(
icon: const Icon(Icons.close_rounded),
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
visualDensity: VisualDensity.compact,
onPressed: () => _close(context),
),
],
),
content: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 260),
child: SingleChildScrollView(
child: Text(
announcement.message,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(height: 1.45),
),
),
),
actions: actions.isEmpty ? null : actions,
),
);
}
}
Future<void> showAppAnnouncementDialog(
BuildContext context, {
required RemoteAnnouncement announcement,
required VoidCallback onDismiss,
}) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) =>
AppAnnouncementDialog(announcement: announcement, onDismiss: onDismiss),
);
}