diff --git a/app/lib/update_checker.dart b/app/lib/update_checker.dart new file mode 100644 index 00000000..facc088f --- /dev/null +++ b/app/lib/update_checker.dart @@ -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 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"); +} diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index c809637b..72ac806d 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -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? _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(); final _navigatorKey = GlobalKey(); diff --git a/app/lib/widget/settings.dart b/app/lib/widget/settings.dart index b801a95d..6d74b8f3 100644 --- a/app/lib/widget/settings.dart +++ b/app/lib/widget/settings.dart @@ -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 { 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: () { diff --git a/app/lib/widget/update_checker.dart b/app/lib/widget/update_checker.dart new file mode 100644 index 00000000..9477902c --- /dev/null +++ b/app/lib/widget/update_checker.dart @@ -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 { + @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 _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; +}