mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-17 16:37:17 +02:00
fix: harden download errors and re-enrich sidecars
This commit is contained in:
@@ -12,60 +12,107 @@ class AppAnnouncementDialog extends StatelessWidget {
|
||||
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) return;
|
||||
if (ctaUrl == null || ctaUrl.isEmpty) {
|
||||
_showCtaOpenFailed(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.tryParse(ctaUrl);
|
||||
if (uri == null) return;
|
||||
if (uri == null) {
|
||||
_showCtaOpenFailed(context);
|
||||
return;
|
||||
}
|
||||
|
||||
bool launched;
|
||||
try {
|
||||
launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {
|
||||
launched = false;
|
||||
}
|
||||
if (!launched) {
|
||||
_showCtaOpenFailed(context);
|
||||
return;
|
||||
}
|
||||
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
onDismiss();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _showCtaOpenFailed(BuildContext context) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
|
||||
const SnackBar(content: Text('Unable to open link. Please try again.')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isUrgent = announcement.priority.toLowerCase() == 'high';
|
||||
|
||||
return AlertDialog(
|
||||
icon: Icon(
|
||||
isUrgent ? Icons.priority_high_rounded : Icons.campaign_rounded,
|
||||
color: isUrgent ? colorScheme.error : colorScheme.primary,
|
||||
),
|
||||
title: Text(announcement.title),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 260),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
announcement.message,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(height: 1.45),
|
||||
// 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,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onDismiss();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
announcement.dismissible
|
||||
? MaterialLocalizations.of(context).closeButtonLabel
|
||||
: 'OK',
|
||||
),
|
||||
),
|
||||
if (announcement.hasCta)
|
||||
FilledButton(
|
||||
onPressed: () => _openCta(context),
|
||||
child: Text(announcement.ctaLabel!),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,10 +122,14 @@ Future<void> showAppAnnouncementDialog(
|
||||
required RemoteAnnouncement announcement,
|
||||
required VoidCallback onDismiss,
|
||||
}) {
|
||||
// barrierDismissible is false so a stray tap outside the dialog can no longer
|
||||
// close (and silently mark-as-seen) the notice. Dismissal — and the
|
||||
// mark-as-seen side effect in onDismiss — only happens via the explicit close
|
||||
// button or the CTA, both of which call onDismiss themselves.
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: announcement.dismissible,
|
||||
barrierDismissible: false,
|
||||
builder: (context) =>
|
||||
AppAnnouncementDialog(announcement: announcement, onDismiss: onDismiss),
|
||||
).whenComplete(onDismiss);
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user