mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-30 07:48:51 +02:00
feat(update): force update when 3+ stable releases behind
UpdateChecker now derives the latest release and a releasesBehind count from one releases-list call. When the installed version is forceUpdateThreshold (3) or more stable releases behind, the update dialog becomes mandatory: barrier and back-press are blocked, the later/don't-remind actions are hidden, backing out of the installer returns to the dialog, and the check bypasses the user's check-for-updates opt-out. Prereleases never count toward the threshold.
This commit is contained in:
+339
-193
@@ -12,11 +12,16 @@ class UpdateDialog extends StatefulWidget {
|
||||
final VoidCallback onDismiss;
|
||||
final VoidCallback onDisableUpdates;
|
||||
|
||||
/// When true the dialog cannot be dismissed: the installed version is too
|
||||
/// many releases behind and updating is mandatory.
|
||||
final bool forced;
|
||||
|
||||
const UpdateDialog({
|
||||
super.key,
|
||||
required this.updateInfo,
|
||||
required this.onDismiss,
|
||||
required this.onDisableUpdates,
|
||||
this.forced = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -27,10 +32,14 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
bool _isDownloading = false;
|
||||
double _progress = 0;
|
||||
String _statusText = '';
|
||||
static final RegExp _whatsNewPattern =
|
||||
RegExp(r"###?\s*What'?s\s*New\s*\n", caseSensitive: false);
|
||||
static final RegExp _cutoffPattern =
|
||||
RegExp(r'\n---|\n###?\s*Downloads', caseSensitive: false);
|
||||
static final RegExp _whatsNewPattern = RegExp(
|
||||
r"###?\s*What'?s\s*New\s*\n",
|
||||
caseSensitive: false,
|
||||
);
|
||||
static final RegExp _cutoffPattern = RegExp(
|
||||
r'\n---|\n###?\s*Downloads',
|
||||
caseSensitive: false,
|
||||
);
|
||||
static final RegExp _sectionPattern = RegExp(r'^#{1,3}\s*(.+)$');
|
||||
static final RegExp _listPattern = RegExp(r'^[-*]\s+(.+)$');
|
||||
static final RegExp _subListPattern = RegExp(r'^\s+[-*]\s+(.+)$');
|
||||
@@ -39,13 +48,13 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
|
||||
Future<void> _downloadAndInstall() async {
|
||||
final apkUrl = widget.updateInfo.apkDownloadUrl;
|
||||
|
||||
|
||||
if (apkUrl == null) {
|
||||
final uri = Uri.parse(widget.updateInfo.downloadUrl);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
if (mounted) Navigator.pop(context);
|
||||
if (mounted && !widget.forced) Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +65,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
});
|
||||
|
||||
final notificationService = NotificationService();
|
||||
|
||||
|
||||
final filePath = await ApkDownloader.downloadApk(
|
||||
url: apkUrl,
|
||||
version: widget.updateInfo.version,
|
||||
@@ -79,27 +88,36 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
|
||||
if (filePath != null) {
|
||||
await notificationService.cancelUpdateNotification();
|
||||
|
||||
|
||||
await notificationService.showUpdateDownloadComplete(
|
||||
version: widget.updateInfo.version,
|
||||
);
|
||||
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
if (widget.forced) {
|
||||
// Keep the mandatory dialog up behind the installer so backing out
|
||||
// of the install cannot bypass it; allow a retry.
|
||||
setState(() {
|
||||
_isDownloading = false;
|
||||
_statusText = '';
|
||||
});
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await ApkDownloader.installApk(filePath);
|
||||
} else {
|
||||
await notificationService.cancelUpdateNotification();
|
||||
|
||||
|
||||
await notificationService.showUpdateDownloadFailed();
|
||||
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isDownloading = false;
|
||||
_statusText = context.l10n.updateDownloadFailed;
|
||||
});
|
||||
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.updateFailedMessage)),
|
||||
);
|
||||
@@ -111,192 +129,299 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(Icons.system_update_rounded, color: colorScheme.onPrimaryContainer, size: 28),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.l10n.updateAvailable, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 2),
|
||||
Text(context.l10n.updateNewVersionReady, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? Color.alphaBlend(Colors.white.withValues(alpha: 0.08), colorScheme.surface)
|
||||
: Color.alphaBlend(Colors.black.withValues(alpha: 0.04), colorScheme.surface),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: colorScheme.outlineVariant.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
return PopScope(
|
||||
canPop: !widget.forced,
|
||||
child: Dialog(
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_VersionChip(version: AppInfo.displayVersion, label: context.l10n.updateCurrent, colorScheme: colorScheme),
|
||||
const SizedBox(width: 12),
|
||||
Icon(Icons.arrow_forward_rounded, size: 20, color: colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
_VersionChip(version: widget.updateInfo.version, label: context.l10n.updateNew, colorScheme: colorScheme, isNew: true),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.system_update_rounded,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.forced
|
||||
? context.l10n.updateRequiredTitle
|
||||
: context.l10n.updateAvailable,
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
widget.forced
|
||||
? context.l10n.updateRequiredNotice(
|
||||
widget.updateInfo.releasesBehind,
|
||||
)
|
||||
: context.l10n.updateNewVersionReady,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
color: widget.forced
|
||||
? colorScheme.error
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
if (_isDownloading) ...[
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), colorScheme.surface)
|
||||
: Color.alphaBlend(Colors.black.withValues(alpha: 0.03), colorScheme.surface),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20, height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? Color.alphaBlend(
|
||||
Colors.white.withValues(alpha: 0.08),
|
||||
colorScheme.surface,
|
||||
)
|
||||
: Color.alphaBlend(
|
||||
Colors.black.withValues(alpha: 0.04),
|
||||
colorScheme.surface,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.updateDownloading, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_VersionChip(
|
||||
version: AppInfo.displayVersion,
|
||||
label: context.l10n.updateCurrent,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: _progress,
|
||||
minHeight: 6,
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
Icons.arrow_forward_rounded,
|
||||
size: 20,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(_statusText, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
Text('${(_progress * 100).toInt()}%', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.primary, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
const SizedBox(width: 12),
|
||||
_VersionChip(
|
||||
version: widget.updateInfo.version,
|
||||
label: context.l10n.updateNew,
|
||||
colorScheme: colorScheme,
|
||||
isNew: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Text(context.l10n.updateWhatsNew, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 180),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), colorScheme.surface)
|
||||
: Color.alphaBlend(Colors.black.withValues(alpha: 0.03), colorScheme.surface),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
const SizedBox(height: 20),
|
||||
|
||||
if (_isDownloading) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
_formatChangelog(
|
||||
widget.updateInfo.changelog,
|
||||
context.l10n.updateSeeReleaseNotes,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? Color.alphaBlend(
|
||||
Colors.white.withValues(alpha: 0.05),
|
||||
colorScheme.surface,
|
||||
)
|
||||
: Color.alphaBlend(
|
||||
Colors.black.withValues(alpha: 0.03),
|
||||
colorScheme.surface,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
|
||||
if (_isDownloading)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _downloadAndInstall,
|
||||
icon: const Icon(Icons.download_rounded, size: 20),
|
||||
label: Text(context.l10n.updateDownloadInstall),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
widget.onDisableUpdates();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Text(context.l10n.updateDontRemind, style: TextStyle(color: colorScheme.onSurfaceVariant)),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.updateDownloading,
|
||||
style: Theme.of(context).textTheme.titleSmall
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: _progress,
|
||||
minHeight: 6,
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
widget.onDismiss();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_statusText,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
child: Text(context.l10n.updateLater),
|
||||
),
|
||||
Text(
|
||||
'${(_progress * 100).toInt()}%',
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
context.l10n.updateWhatsNew,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 180),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? Color.alphaBlend(
|
||||
Colors.white.withValues(alpha: 0.05),
|
||||
colorScheme.surface,
|
||||
)
|
||||
: Color.alphaBlend(
|
||||
Colors.black.withValues(alpha: 0.03),
|
||||
colorScheme.surface,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
_formatChangelog(
|
||||
widget.updateInfo.changelog,
|
||||
context.l10n.updateSeeReleaseNotes,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(height: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
|
||||
if (_isDownloading)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: widget.forced
|
||||
? const SizedBox.shrink()
|
||||
: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _downloadAndInstall,
|
||||
icon: const Icon(Icons.download_rounded, size: 20),
|
||||
label: Text(context.l10n.updateDownloadInstall),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!widget.forced) const SizedBox(height: 8),
|
||||
if (!widget.forced)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
widget.onDisableUpdates();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.updateDontRemind,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
widget.onDismiss();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(context.l10n.updateLater),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -305,24 +430,24 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
/// Format changelog - clean up markdown and extract relevant content
|
||||
String _formatChangelog(String changelog, String emptyFallback) {
|
||||
var content = changelog;
|
||||
|
||||
|
||||
final whatsNewMatch = _whatsNewPattern.firstMatch(content);
|
||||
if (whatsNewMatch != null) {
|
||||
content = content.substring(whatsNewMatch.end);
|
||||
}
|
||||
|
||||
|
||||
final cutoffMatch = _cutoffPattern.firstMatch(content);
|
||||
if (cutoffMatch != null) {
|
||||
content = content.substring(0, cutoffMatch.start);
|
||||
}
|
||||
|
||||
|
||||
final lines = content.split('\n');
|
||||
final formattedLines = <String>[];
|
||||
|
||||
|
||||
for (var line in lines) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty) continue;
|
||||
|
||||
|
||||
final sectionMatch = _sectionPattern.firstMatch(line);
|
||||
if (sectionMatch != null) {
|
||||
final section = sectionMatch.group(1)?.trim();
|
||||
@@ -332,30 +457,39 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
final listMatch = _listPattern.firstMatch(line);
|
||||
if (listMatch != null) {
|
||||
var itemText = listMatch.group(1) ?? '';
|
||||
itemText = itemText.replaceAllMapped(_boldPattern, (m) => m.group(1) ?? '');
|
||||
itemText = itemText.replaceAllMapped(_codePattern, (m) => m.group(1) ?? '');
|
||||
itemText = itemText.replaceAllMapped(
|
||||
_boldPattern,
|
||||
(m) => m.group(1) ?? '',
|
||||
);
|
||||
itemText = itemText.replaceAllMapped(
|
||||
_codePattern,
|
||||
(m) => m.group(1) ?? '',
|
||||
);
|
||||
formattedLines.add('• $itemText');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
final subListMatch = _subListPattern.firstMatch(line);
|
||||
if (subListMatch != null) {
|
||||
var itemText = subListMatch.group(1) ?? '';
|
||||
itemText = itemText.replaceAllMapped(_boldPattern, (m) => m.group(1) ?? '');
|
||||
itemText = itemText.replaceAllMapped(
|
||||
_boldPattern,
|
||||
(m) => m.group(1) ?? '',
|
||||
);
|
||||
formattedLines.add(' - $itemText');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var formatted = formattedLines.join('\n').trim();
|
||||
if (formatted.length > 2000) {
|
||||
formatted = '${formatted.substring(0, 2000)}...';
|
||||
}
|
||||
|
||||
|
||||
return formatted.isEmpty ? emptyFallback : formatted;
|
||||
}
|
||||
}
|
||||
@@ -377,18 +511,27 @@ class _VersionChip extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isNew ? colorScheme.primaryContainer : colorScheme.surfaceContainerHighest,
|
||||
color: isNew
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'v$version',
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: isNew ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant,
|
||||
color: isNew
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontWeight: isNew ? FontWeight.bold : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -402,13 +545,16 @@ Future<void> showUpdateDialog(
|
||||
BuildContext context, {
|
||||
required UpdateInfo updateInfo,
|
||||
required VoidCallback onDisableUpdates,
|
||||
bool forced = false,
|
||||
}) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: !forced,
|
||||
builder: (context) => UpdateDialog(
|
||||
updateInfo: updateInfo,
|
||||
onDismiss: () {},
|
||||
onDisableUpdates: onDisableUpdates,
|
||||
forced: forced,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user