Add update checker

This commit is contained in:
Ming Ming 2022-05-26 18:40:18 +08:00
parent e3b1c5ed30
commit e17532eea1
4 changed files with 285 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

@ -39,6 +39,7 @@ import 'package:nc_photos/widget/splash.dart';
import 'package:nc_photos/widget/tag_browser.dart';
import 'package:nc_photos/widget/trashbin_browser.dart';
import 'package:nc_photos/widget/trashbin_viewer.dart';
import 'package:nc_photos/widget/update_checker.dart';
import 'package:nc_photos/widget/viewer.dart';
class MyApp extends StatefulWidget {
@ -173,6 +174,7 @@ class _MyAppState extends State<MyApp>
route ??= _handlePlaceBrowserRoute(settings);
route ??= _handlePlacesBrowserRoute(settings);
route ??= _handleResultViewerRoute(settings);
route ??= _handleUpdateCheckerRoute(settings);
return route;
}
@ -621,6 +623,17 @@ class _MyAppState extends State<MyApp>
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;
}
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final _navigatorKey = GlobalKey<NavigatorState>();

View file

@ -33,6 +33,7 @@ import 'package:nc_photos/widget/root_picker.dart';
import 'package:nc_photos/widget/share_folder_picker.dart';
import 'package:nc_photos/widget/simple_input_dialog.dart';
import 'package:nc_photos/widget/stateful_slider.dart';
import 'package:nc_photos/widget/update_checker.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:tuple/tuple.dart';
@ -244,6 +245,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,212 @@
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/theme.dart';
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({Key? key}) : super(key: key);
@override
createState() => _UpdateCheckerState();
}
class _UpdateCheckerState extends State<UpdateChecker> {
@override
initState() {
super.initState();
_work();
}
@override
build(BuildContext context) {
return AppTheme(
child: 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;
}