Add update checker

This commit is contained in:
Ming Ming 2022-05-26 18:40:18 +08:00
parent 41e9b6214a
commit 5e1d7be6a3
4 changed files with 282 additions and 0 deletions

View file

@ -0,0 +1,53 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:nc_photos/k.dart' as k;
enum UpdateCheckerResult {
updateAvailable,
alreadyLatest,
error,
}
class UpdateChecker {
Future<UpdateCheckerResult> call() async {
try {
final uri = Uri.https("bit.ly", "3pb2oG9");
final req = http.Request("GET", uri);
final response =
await http.Response.fromStream(await http.Client().send(req));
if (response.statusCode != 200) {
_log.severe("[call] Failed GETing URL: ${response.statusCode}");
return UpdateCheckerResult.error;
}
final body = response.body;
final json = jsonDecode(body) as Map;
final data = json[_buildVariant] as Map;
_log.info("[call] Update data: ${jsonEncode(data)}");
final latest = data["v"];
_versionStr = data["vStr"];
_updateUrl = data["url"];
if (latest > k.version) {
return UpdateCheckerResult.updateAvailable;
} else {
return UpdateCheckerResult.alreadyLatest;
}
} catch (e, stackTrace) {
_log.severe("[call] Exception", e, stackTrace);
return UpdateCheckerResult.error;
}
}
/// Return the binary url (if available) after [call] returned with
/// [UpdateCheckerResult.updateAvailable]
String? get updateUrl => _updateUrl;
String? get versionStr => _versionStr;
String? _updateUrl;
String? _versionStr;
static const _buildVariant = "";
static final _log = Logger("update_checker.UpdateChecker");
}

View file

@ -56,6 +56,7 @@ import 'package:nc_photos/widget/splash.dart';
import 'package:nc_photos/widget/trashbin_browser.dart';
import 'package:nc_photos/widget/trashbin_viewer.dart';
import 'package:nc_photos/widget/trusted_cert_manager.dart';
import 'package:nc_photos/widget/update_checker.dart';
import 'package:nc_photos/widget/viewer.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
@ -242,6 +243,7 @@ class _WrappedAppState extends State<_WrappedApp>
route ??= _handleCollectionBrowserRoute(settings);
route ??= _handleAccountSettingsRoute(settings);
route ??= _handlePlacePickerRoute(settings);
route ??= _handleUpdateCheckerRoute(settings);
return route;
}
@ -563,6 +565,17 @@ class _WrappedAppState extends State<_WrappedApp>
return null;
}
Route<dynamic>? _handleUpdateCheckerRoute(RouteSettings settings) {
try {
if (settings.name == UpdateChecker.routeName) {
return UpdateChecker.buildRoute();
}
} catch (e) {
_log.severe("[_handleUpdateCheckerRoute] Failed while handling route", e);
}
return null;
}
late final _bloc = context.read<_Bloc>();
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final _navigatorKey = GlobalKey<NavigatorState>();

View file

@ -27,6 +27,7 @@ import 'package:nc_photos/widget/settings/photos_settings.dart';
import 'package:nc_photos/widget/settings/settings_list_caption.dart';
import 'package:nc_photos/widget/settings/theme_settings.dart';
import 'package:nc_photos/widget/settings/viewer_settings.dart';
import 'package:nc_photos/widget/update_checker.dart';
import 'package:np_codegen/np_codegen.dart';
part 'settings.g.dart';
@ -146,6 +147,12 @@ class _SettingsState extends State<Settings> {
launch(help_util.donateUrl);
},
),
ListTile(
title: const Text("Check for updates"),
onTap: () {
Navigator.of(context).pushNamed(UpdateChecker.routeName);
},
),
ListTile(
title: Text(L10n.global().settingsSourceCodeTitle),
onTap: () {

View file

@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/help_utils.dart' as help_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/update_checker.dart' as checker;
import 'package:nc_photos/url_launcher_util.dart';
class UpdateChecker extends StatefulWidget {
static const routeName = "/update-checker";
static Route buildRoute() => MaterialPageRoute(
builder: (context) => const UpdateChecker(),
);
const UpdateChecker({super.key});
@override
createState() => _UpdateCheckerState();
}
class _UpdateCheckerState extends State<UpdateChecker> {
@override
initState() {
super.initState();
_work();
}
@override
build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
body: Builder(
builder: _buildContent,
),
);
}
Widget _buildContent(BuildContext context) {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 32),
const Align(
alignment: Alignment.center,
child: Text(
"Photos ${k.versionStr}",
style: TextStyle(fontSize: 24),
),
),
const SizedBox(height: 32),
Align(
alignment: Alignment.center,
child: _getStatusWidget(),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.center,
child: Text(
_getStatusText(),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
),
const SizedBox(height: 16),
if (_result == checker.UpdateCheckerResult.updateAvailable)
Align(
alignment: Alignment.center,
child: ElevatedButton(
onPressed: () {
if (_updateUrl != null) {
launch(_updateUrl!);
} else {
// fallback
launch("https://bit.ly/3wNLHFo");
}
},
child: const Text("GET UPDATE"),
),
)
else
const SizedBox(height: 48),
const SizedBox(height: 56),
Center(
child: Text(
L10n.global().donationShortMessage,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.center,
child: ElevatedButton(
onPressed: () {
launch(help_util.donateUrl);
},
child: Text(L10n.global().donationButtonLabel),
),
),
const SizedBox(height: 16),
],
),
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
const Expanded(
child: Text("Having problems?"),
),
TextButton(
onPressed: () {
launch("https://bit.ly/3wNLHFo");
},
child: const Text("CHECK MANUALLY"),
),
],
),
),
],
);
}
Future<void> _work() async {
await Future.delayed(const Duration(seconds: 1));
final uc = checker.UpdateChecker();
final result = await uc();
if (result == checker.UpdateCheckerResult.updateAvailable) {
_updateUrl = uc.updateUrl;
_versionStr = uc.versionStr;
}
if (mounted) {
setState(() {
_result = result;
});
}
}
Widget _getStatusWidget() {
if (_result == null) {
return const SizedBox(
width: _statusIconSize,
height: _statusIconSize,
child: Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
),
);
} else {
switch (_result!) {
case checker.UpdateCheckerResult.updateAvailable:
return Icon(
Icons.upload,
color: Colors.orange[700],
size: _statusIconSize,
);
case checker.UpdateCheckerResult.alreadyLatest:
return Icon(
Icons.done,
color: Colors.green[600],
size: _statusIconSize,
);
case checker.UpdateCheckerResult.error:
return Icon(
Icons.warning,
color: Colors.red[700],
size: _statusIconSize,
);
}
}
}
String _getStatusText() {
if (_result == null) {
return "Checking...";
} else {
switch (_result!) {
case checker.UpdateCheckerResult.updateAvailable:
return "Update available ($_versionStr)";
case checker.UpdateCheckerResult.alreadyLatest:
return "You are running the latest version";
case checker.UpdateCheckerResult.error:
return "Failed checking for updates";
}
}
}
checker.UpdateCheckerResult? _result;
String? _updateUrl;
String? _versionStr;
static const _statusIconSize = 72.0;
}