mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-23 11:22:37 +02:00
feat: show remote-config launch announcement on app start
Introduce AppRemoteConfigService which fetches a platform/version/locale-aware JSON payload from api.zarz.moe/v1/spotiflac-mobile/config and caches it in SharedPreferences. main_shell shows a one-shot announcement dialog (respecting dismissible, CTA, time window and version gates) when no update prompt is pending; dismissed IDs are persisted so each announcement surfaces only once. Tweaks bundled in: the service health dot loses its blur halo in favour of solid Material 3 tones, and AppInfo gains the remote config endpoint constant. The share listener and SAF migration hook stay synchronous inside the post-frame callback so share-intent URLs never race the network-bound checks. New unit tests cover the announcement CTA/active-window rules.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.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,
|
||||
});
|
||||
|
||||
Future<void> _openCta(BuildContext context) async {
|
||||
final ctaUrl = announcement.ctaUrl;
|
||||
if (ctaUrl == null || ctaUrl.isEmpty) return;
|
||||
|
||||
final uri = Uri.tryParse(ctaUrl);
|
||||
if (uri == null) return;
|
||||
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
onDismiss();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@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),
|
||||
),
|
||||
),
|
||||
),
|
||||
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!),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showAppAnnouncementDialog(
|
||||
BuildContext context, {
|
||||
required RemoteAnnouncement announcement,
|
||||
required VoidCallback onDismiss,
|
||||
}) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: announcement.dismissible,
|
||||
builder: (context) =>
|
||||
AppAnnouncementDialog(announcement: announcement, onDismiss: onDismiss),
|
||||
).whenComplete(onDismiss);
|
||||
}
|
||||
@@ -78,7 +78,8 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
||||
});
|
||||
final downloadExtensions = _downloadExtensions();
|
||||
final recommended = widget.recommendedService;
|
||||
if (recommended != null && _serviceExists(recommended, downloadExtensions)) {
|
||||
if (recommended != null &&
|
||||
_serviceExists(recommended, downloadExtensions)) {
|
||||
_selectedService = recommended;
|
||||
} else {
|
||||
_selectedService = ref.read(settingsProvider).defaultService;
|
||||
@@ -383,17 +384,7 @@ class _ServiceHealthDot extends StatelessWidget {
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.35),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -402,14 +393,14 @@ class _ServiceHealthDot extends StatelessWidget {
|
||||
Color _serviceHealthColor(String status) {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return const Color(0xFF35D07F);
|
||||
return const Color(0xFF24D47A);
|
||||
case 'degraded':
|
||||
case 'unknown':
|
||||
return const Color(0xFFFFC857);
|
||||
return const Color(0xFFFFC247);
|
||||
case 'offline':
|
||||
return const Color(0xFFFF4D5E);
|
||||
return const Color(0xFFFF5A66);
|
||||
default:
|
||||
return const Color(0xFFFFC857);
|
||||
return const Color(0xFFFFC247);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user