feat(settings): About section with version + force update check
Adds an About card to Settings that shows the installed version (version+build from PackageInfo), the latest known version from clientUpdateProvider, and a "Check for updates" button that invalidates the provider to force a fresh poll. When an update is available, surfaces an Install CTA that reuses the same installer flow as the top banner. The existing banner (shouldShowUpdateBannerProvider) is unaffected — it gates on per-version dismissal, while the About section always reflects the current provider state regardless of dismissal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../theme/theme_extension.dart';
|
||||
import '../update/client_update_provider.dart';
|
||||
import '../update/update_info.dart';
|
||||
|
||||
final _packageInfoProvider = FutureProvider<PackageInfo>((_) {
|
||||
return PackageInfo.fromPlatform();
|
||||
});
|
||||
|
||||
class AboutSection extends ConsumerStatefulWidget {
|
||||
const AboutSection({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<AboutSection> createState() => _AboutSectionState();
|
||||
}
|
||||
|
||||
enum _InstallStage { idle, downloading, error }
|
||||
|
||||
class _AboutSectionState extends ConsumerState<AboutSection> {
|
||||
DateTime? _lastChecked;
|
||||
bool _checking = false;
|
||||
|
||||
_InstallStage _installStage = _InstallStage.idle;
|
||||
double _installProgress = 0;
|
||||
String? _installError;
|
||||
|
||||
Future<void> _checkNow() async {
|
||||
setState(() => _checking = true);
|
||||
ref.invalidate(clientUpdateProvider);
|
||||
try {
|
||||
await ref.read(clientUpdateProvider.future);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_checking = false;
|
||||
_lastChecked = DateTime.now();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _install(UpdateInfo info) async {
|
||||
setState(() {
|
||||
_installStage = _InstallStage.downloading;
|
||||
_installProgress = 0;
|
||||
_installError = null;
|
||||
});
|
||||
try {
|
||||
final installer = await ref.read(updateInstallerProvider.future);
|
||||
final path = await installer.download(
|
||||
info.apkUrl,
|
||||
onProgress: (p) {
|
||||
if (mounted) setState(() => _installProgress = p);
|
||||
},
|
||||
);
|
||||
await installer.install(path);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_installStage = _InstallStage.error;
|
||||
_installError = '$e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||
final pkg = ref.watch(_packageInfoProvider);
|
||||
final update = ref.watch(clientUpdateProvider);
|
||||
|
||||
final installed = pkg.value == null
|
||||
? '…'
|
||||
: '${pkg.value!.version}+${pkg.value!.buildNumber}';
|
||||
|
||||
final UpdateInfo? available = update.value;
|
||||
final hasUpdate = available != null;
|
||||
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: _SectionHeader('About'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: fs.parchment, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text('Installed version',
|
||||
style: TextStyle(color: fs.parchment, fontSize: 14)),
|
||||
const Spacer(),
|
||||
Text(installed,
|
||||
style: TextStyle(
|
||||
color: fs.ash,
|
||||
fontSize: 13,
|
||||
fontFamily: 'JetBrainsMono')),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.cloud_outlined, color: fs.parchment, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text('Latest version',
|
||||
style: TextStyle(color: fs.parchment, fontSize: 14)),
|
||||
const Spacer(),
|
||||
Text(
|
||||
hasUpdate ? available.version : _statusFor(update, installed),
|
||||
style: TextStyle(
|
||||
color: hasUpdate ? fs.accent : fs.ash,
|
||||
fontSize: 13,
|
||||
fontFamily: 'JetBrainsMono'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_lastChecked != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
|
||||
child: Text(
|
||||
'Last checked ${_formatTime(_lastChecked!)}',
|
||||
style: TextStyle(color: fs.ash, fontSize: 11),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
key: const Key('about_check_for_updates'),
|
||||
onPressed: _checking ? null : _checkNow,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: fs.accent,
|
||||
foregroundColor: fs.parchment,
|
||||
),
|
||||
icon: _checking
|
||||
? SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation(fs.parchment),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.refresh, size: 18),
|
||||
label: Text(_checking ? 'Checking…' : 'Check for updates'),
|
||||
),
|
||||
if (hasUpdate) ...[
|
||||
const SizedBox(width: 12),
|
||||
FilledButton.icon(
|
||||
key: const Key('about_install_update'),
|
||||
onPressed: _installStage == _InstallStage.downloading
|
||||
? null
|
||||
: () => _install(available),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: fs.moss,
|
||||
foregroundColor: fs.parchment,
|
||||
),
|
||||
icon: _installStage == _InstallStage.downloading
|
||||
? SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
value: _installProgress > 0 ? _installProgress : null,
|
||||
valueColor: AlwaysStoppedAnimation(fs.parchment),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.system_update, size: 18),
|
||||
label: Text(
|
||||
_installStage == _InstallStage.downloading
|
||||
? 'Downloading…'
|
||||
: _installStage == _InstallStage.error
|
||||
? 'Retry install'
|
||||
: 'Install ${available.version}',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_installStage == _InstallStage.error && _installError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Text(_installError!,
|
||||
style: TextStyle(color: fs.error, fontSize: 12),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
String _statusFor(AsyncValue<UpdateInfo?> update, String installed) {
|
||||
if (update.isLoading) return 'Checking…';
|
||||
if (update.hasError) return 'Check failed';
|
||||
return 'Up to date';
|
||||
}
|
||||
|
||||
String _formatTime(DateTime t) {
|
||||
final h = t.hour.toString().padLeft(2, '0');
|
||||
final m = t.minute.toString().padLeft(2, '0');
|
||||
return '$h:$m';
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader(this.label);
|
||||
final String label;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||
return Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: fs.parchment,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import '../library/library_providers.dart' show dioProvider;
|
||||
import '../models/my_profile.dart';
|
||||
import '../shared/widgets/main_app_bar_actions.dart';
|
||||
import '../theme/theme_extension.dart';
|
||||
import 'about_section.dart';
|
||||
import 'storage_section.dart';
|
||||
import '../theme/theme_mode_provider.dart';
|
||||
|
||||
@@ -56,6 +57,8 @@ class SettingsScreen extends ConsumerWidget {
|
||||
_PasswordSection(),
|
||||
_Divider(),
|
||||
_ListenBrainzSection(),
|
||||
_Divider(),
|
||||
AboutSection(),
|
||||
_AdminSection(),
|
||||
SizedBox(height: 96),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user