feat: add app update checks and standalone release publishing panel
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
@@ -18,6 +20,8 @@ import '../features/school/school_selection_page.dart';
|
||||
import '../features/school/undergraduate/session.dart';
|
||||
import '../features/school/undergraduate/exam_query_picker.dart';
|
||||
import '../domain/undergraduate_exam_query.dart';
|
||||
import '../services/app_update_service.dart';
|
||||
import '../features/updates/update_prompt.dart';
|
||||
|
||||
class HepanApp extends StatelessWidget {
|
||||
const HepanApp({
|
||||
@@ -29,6 +33,8 @@ class HepanApp extends StatelessWidget {
|
||||
this.undergraduateBuilder,
|
||||
this.notificationSelection,
|
||||
this.feedbackSender,
|
||||
this.updateService,
|
||||
this.openUpdateDownload = openUpdateInBrowser,
|
||||
});
|
||||
final AppController controller;
|
||||
final Widget Function()? schoolBuilder;
|
||||
@@ -37,6 +43,8 @@ class HepanApp extends StatelessWidget {
|
||||
final Widget Function()? undergraduateBuilder;
|
||||
final ValueNotifier<String?>? notificationSelection;
|
||||
final FeedbackSender? feedbackSender;
|
||||
final AppUpdateService? updateService;
|
||||
final UpdateDownloadOpener openUpdateDownload;
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
@@ -53,6 +61,8 @@ class HepanApp extends StatelessWidget {
|
||||
undergraduateBuilder: undergraduateBuilder,
|
||||
notificationSelection: notificationSelection,
|
||||
feedbackSender: feedbackSender ?? configuredFeedbackSender(),
|
||||
updateService: updateService,
|
||||
openUpdateDownload: openUpdateDownload,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -66,6 +76,8 @@ class _Shell extends StatefulWidget {
|
||||
this.undergraduateBuilder,
|
||||
this.notificationSelection,
|
||||
required this.feedbackSender,
|
||||
this.updateService,
|
||||
required this.openUpdateDownload,
|
||||
});
|
||||
final AppController controller;
|
||||
final Widget Function()? schoolBuilder;
|
||||
@@ -74,6 +86,8 @@ class _Shell extends StatefulWidget {
|
||||
final Widget Function()? undergraduateBuilder;
|
||||
final ValueNotifier<String?>? notificationSelection;
|
||||
final FeedbackSender feedbackSender;
|
||||
final AppUpdateService? updateService;
|
||||
final UpdateDownloadOpener openUpdateDownload;
|
||||
@override
|
||||
State<_Shell> createState() => _ShellState();
|
||||
}
|
||||
@@ -81,6 +95,40 @@ class _Shell extends StatefulWidget {
|
||||
class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
||||
int _tab = 0;
|
||||
bool _openingSchool = false;
|
||||
bool _checkingUpdate = false;
|
||||
bool _updatePromptOpen = false;
|
||||
Timer? _startupUpdate;
|
||||
Future<void> _checkUpdate({bool silent = false}) async {
|
||||
if (_checkingUpdate || _updatePromptOpen || widget.updateService == null) {
|
||||
return;
|
||||
}
|
||||
setState(() => _checkingUpdate = true);
|
||||
try {
|
||||
final release = await widget.updateService!.check();
|
||||
if (mounted) setState(() => _checkingUpdate = false);
|
||||
if (!mounted || ModalRoute.of(context)?.isCurrent != true) return;
|
||||
if (release != null) {
|
||||
_updatePromptOpen = true;
|
||||
await showUpdatePrompt(
|
||||
context,
|
||||
release,
|
||||
openDownload: widget.openUpdateDownload,
|
||||
);
|
||||
} else if (!silent) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('当前已是最新版本')));
|
||||
}
|
||||
} on UpdateCheckException catch (error) {
|
||||
if (!silent && mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(error.message)));
|
||||
}
|
||||
} finally {
|
||||
_updatePromptOpen = false;
|
||||
if (mounted) setState(() => _checkingUpdate = false);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the saved school; legacy graduate imports infer their source.
|
||||
SupportedSchool? get _knownSchool =>
|
||||
widget.controller.snapshot.selectedSchool ??
|
||||
@@ -102,10 +150,19 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
widget.notificationSelection?.addListener(_selectNotification);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _selectNotification());
|
||||
if (widget.updateService != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_startupUpdate = Timer(const Duration(seconds: 2), () {
|
||||
if (mounted) unawaited(_checkUpdate(silent: true));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_startupUpdate?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
widget.notificationSelection?.removeListener(_selectNotification);
|
||||
super.dispose();
|
||||
@@ -400,6 +457,10 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
||||
ProfilePage(
|
||||
controller: widget.controller,
|
||||
feedbackSender: widget.feedbackSender,
|
||||
onCheckUpdates: widget.updateService == null
|
||||
? null
|
||||
: () => _checkUpdate(),
|
||||
checkingUpdates: _checkingUpdate,
|
||||
onLogin: () => _school(selectSchool: true),
|
||||
schoolName: _knownSchool?.label,
|
||||
onUpdateTerm: () => _school(chooseTerm: true),
|
||||
|
||||
Reference in New Issue
Block a user