mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-07 05:07:59 +02:00
1cd668c869
Move hardcoded strings in library settings, announcement link errors, and unknown track title/artist fallbacks to AppLocalizations. Sync locale-dependent fallback strings from MainShell.
132 lines
3.8 KiB
Dart
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),
|
|
);
|
|
}
|