refactor: share low-risk UI and tracker helpers

This commit is contained in:
edde746
2026-05-02 01:58:50 +02:00
parent 646832c6b1
commit 50bc245677
18 changed files with 270 additions and 265 deletions
+9
View File
@@ -18,6 +18,15 @@ bool flexibleBool(Object? v) => switch (v) {
_ => false,
};
/// Parse a value that may be [bool], [int] (0/1), or [String] ('1') to [bool].
/// Returns `null` for `null` or unrecognised values.
bool? flexibleBoolNullable(Object? v) => switch (v) {
final bool b => b,
final int n => n == 1,
final String s => s == '1',
_ => null,
};
/// Parse a value that may be [double], [num], or [String] to [double].
double? flexibleDouble(Object? v) => switch (v) {
final num n => n.toDouble(),
+15
View File
@@ -35,6 +35,21 @@ void scrollToCurrentItem(ScrollController controller, GlobalKey firstItemKey, in
});
}
/// Owns the boilerplate for one-time initial scrolling in selectable lists.
class InitialItemScrollController {
final GlobalKey firstItemKey = GlobalKey();
final ScrollController controller = ScrollController();
bool _didInitialScroll = false;
void maybeScrollTo(int? selectedIndex) {
if (_didInitialScroll || selectedIndex == null || selectedIndex <= 0) return;
_didInitialScroll = true;
scrollToCurrentItem(controller, firstItemKey, selectedIndex);
}
void dispose() => controller.dispose();
}
/// Scroll a horizontal list to center the item at the given index.
///
/// Assumes items are laid out with [leadingPadding] before the first item,
+63
View File
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../i18n/strings.g.dart';
import '../services/update_service.dart';
import '../widgets/dialog_action_button.dart';
Future<void> showUpdateAvailableDialog(
BuildContext context,
Map<String, dynamic> updateInfo, {
required String title,
required String dismissLabel,
bool showSkipVersion = false,
}) {
return showDialog<void>(
context: context,
builder: (dialogContext) {
final latestVersion = updateInfo['latestVersion'] as String;
final releaseUrl = updateInfo['releaseUrl'] as String;
return AlertDialog(
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.update.versionAvailable(version: latestVersion),
style: Theme.of(dialogContext).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
t.update.currentVersion(version: updateInfo['currentVersion']),
style: Theme.of(dialogContext).textTheme.bodySmall,
),
],
),
actions: [
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: dismissLabel),
if (showSkipVersion)
DialogActionButton(
onPressed: () async {
await UpdateService.skipVersion(latestVersion);
if (dialogContext.mounted) Navigator.pop(dialogContext);
},
label: t.update.skipVersion,
),
DialogActionButton(
onPressed: () async {
final url = Uri.parse(releaseUrl);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
if (dialogContext.mounted) Navigator.pop(dialogContext);
},
label: t.update.viewRelease,
isPrimary: true,
),
],
);
},
);
}