about: new version check

This commit is contained in:
Thibault Deckers 2021-02-03 18:13:54 +09:00
parent c05b646ddd
commit 45ba3155b0
13 changed files with 330 additions and 112 deletions

View file

@ -1,11 +1,17 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:connectivity/connectivity.dart'; import 'package:connectivity/connectivity.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:github/github.dart';
import 'package:google_api_availability/google_api_availability.dart'; import 'package:google_api_availability/google_api_availability.dart';
import 'package:package_info/package_info.dart';
import 'package:version/version.dart';
final AvesAvailability availability = AvesAvailability._private(); final AvesAvailability availability = AvesAvailability._private();
class AvesAvailability { class AvesAvailability {
bool _isConnected, _hasPlayServices; bool _isConnected, _hasPlayServices, _isNewVersionAvailable;
AvesAvailability._private() { AvesAvailability._private() {
Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult); Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult);
@ -38,4 +44,29 @@ class AvesAvailability {
// local geolocation with `geocoder` requires Play Services // local geolocation with `geocoder` requires Play Services
Future<bool> get canGeolocate => Future.wait<bool>([isConnected, hasPlayServices]).then((results) => results.every((result) => result)); Future<bool> get canGeolocate => Future.wait<bool>([isConnected, hasPlayServices]).then((results) => results.every((result) => result));
Future<bool> get isNewVersionAvailable async {
if (_isNewVersionAvailable != null) return SynchronousFuture(_isNewVersionAvailable);
final now = DateTime.now();
final dueDate = settings.lastVersionCheckDate.add(Durations.lastVersionCheckInterval);
if (now.isBefore(dueDate)) {
_isNewVersionAvailable = false;
return SynchronousFuture(_isNewVersionAvailable);
}
if (!(await isConnected)) return false;
Version version(String s) => Version.parse(s.replaceFirst('v', ''));
final currentTag = (await PackageInfo.fromPlatform()).version;
final latestTag = (await GitHub().repositories.getLatestRelease(RepositorySlug('deckerst', 'aves'))).tagName;
_isNewVersionAvailable = version(latestTag) > version(currentTag);
if (_isNewVersionAvailable) {
debugPrint('Aves $latestTag is available on github');
} else {
debugPrint('Aves $currentTag is the latest version');
settings.lastVersionCheckDate = now;
}
return _isNewVersionAvailable;
}
} }

View file

@ -64,6 +64,9 @@ class Settings extends ChangeNotifier {
static const saveSearchHistoryKey = 'save_search_history'; static const saveSearchHistoryKey = 'save_search_history';
static const searchHistoryKey = 'search_history'; static const searchHistoryKey = 'search_history';
// version
static const lastVersionCheckDateKey = 'last_version_check_date';
Future<void> init() async { Future<void> init() async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
} }
@ -214,6 +217,12 @@ class Settings extends ChangeNotifier {
set searchHistory(List<CollectionFilter> newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); set searchHistory(List<CollectionFilter> newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList());
// version
DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs.getInt(lastVersionCheckDateKey) ?? 0);
set lastVersionCheckDate(DateTime newValue) => setAndNotify(lastVersionCheckDateKey, newValue.millisecondsSinceEpoch);
// convenience methods // convenience methods
// ignore: avoid_positional_boolean_parameters // ignore: avoid_positional_boolean_parameters

View file

@ -35,7 +35,7 @@ class Durations {
static const viewerOverlayChangeAnimation = Duration(milliseconds: 150); static const viewerOverlayChangeAnimation = Duration(milliseconds: 150);
static const viewerOverlayPageChooserAnimation = Duration(milliseconds: 200); static const viewerOverlayPageChooserAnimation = Duration(milliseconds: 200);
// info // info animations
static const mapStyleSwitchAnimation = Duration(milliseconds: 300); static const mapStyleSwitchAnimation = Duration(milliseconds: 300);
static const xmpStructArrayCardTransition = Duration(milliseconds: 300); static const xmpStructArrayCardTransition = Duration(milliseconds: 300);
@ -49,4 +49,7 @@ class Durations {
static const softKeyboardDisplayDelay = Duration(milliseconds: 300); static const softKeyboardDisplayDelay = Duration(milliseconds: 300);
static const searchDebounceDelay = Duration(milliseconds: 250); static const searchDebounceDelay = Duration(milliseconds: 250);
static const contentChangeDebounceDelay = Duration(milliseconds: 500); static const contentChangeDebounceDelay = Duration(milliseconds: 500);
// app life
static const lastVersionCheckInterval = Duration(days: 7);
} }

View file

@ -167,6 +167,12 @@ class Constants {
licenseUrl: 'https://github.com/aloisdeniel/flutter_geocoder/blob/master/LICENSE', licenseUrl: 'https://github.com/aloisdeniel/flutter_geocoder/blob/master/LICENSE',
sourceUrl: 'https://github.com/aloisdeniel/flutter_geocoder', sourceUrl: 'https://github.com/aloisdeniel/flutter_geocoder',
), ),
Dependency(
name: 'Github',
license: 'MIT',
licenseUrl: 'https://github.com/SpinlockLabs/github.dart/blob/master/LICENSE',
sourceUrl: 'https://github.com/SpinlockLabs/github.dart',
),
Dependency( Dependency(
name: 'Google Maps for Flutter', name: 'Google Maps for Flutter',
license: 'BSD 3-Clause', license: 'BSD 3-Clause',
@ -287,6 +293,12 @@ class Constants {
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE', licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE',
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher', sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher',
), ),
Dependency(
name: 'Version',
license: 'BSD 3-Clause',
licenseUrl: 'https://github.com/dartninja/version/blob/master/LICENSE',
sourceUrl: 'https://github.com/dartninja/version',
),
Dependency( Dependency(
name: 'XML', name: 'XML',
license: 'MIT', license: 'MIT',

View file

@ -1,5 +1,8 @@
import 'package:aves/flutter_version.dart'; import 'package:aves/flutter_version.dart';
import 'package:aves/widgets/about/app_ref.dart';
import 'package:aves/widgets/about/credits.dart';
import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/about/licenses.dart';
import 'package:aves/widgets/about/new_version.dart';
import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -23,43 +26,9 @@ class AboutPage extends StatelessWidget {
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
AppReference(), AppReference(),
SizedBox(height: 16),
Divider(), Divider(),
Padding( AboutNewVersion(),
padding: EdgeInsets.symmetric(horizontal: 16), AboutCredits(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: BoxConstraints(minHeight: 48),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
'Credits',
style: Theme.of(context).textTheme.headline6.copyWith(fontFamily: 'Concourse Caps'),
),
),
),
RichText(
text: TextSpan(
children: [
TextSpan(text: 'This app uses the font '),
WidgetSpan(
child: LinkChip(
text: 'Concourse',
url: 'https://mbtype.com/fonts/concourse/',
textStyle: TextStyle(fontWeight: FontWeight.bold),
),
alignment: PlaceholderAlignment.middle,
),
TextSpan(text: ' for titles and the media information page.'),
],
),
),
SizedBox(height: 16),
],
),
),
Divider(), Divider(),
], ],
), ),
@ -72,71 +41,3 @@ class AboutPage extends StatelessWidget {
); );
} }
} }
class AppReference extends StatefulWidget {
@override
_AppReferenceState createState() => _AppReferenceState();
}
class _AppReferenceState extends State<AppReference> {
Future<PackageInfo> packageInfoLoader;
@override
void initState() {
super.initState();
packageInfoLoader = PackageInfo.fromPlatform();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
_buildAvesLine(),
_buildFlutterLine(),
],
),
);
}
Widget _buildAvesLine() {
final textTheme = Theme.of(context).textTheme;
final style = textTheme.headline6.copyWith(fontWeight: FontWeight.bold);
return FutureBuilder<PackageInfo>(
future: packageInfoLoader,
builder: (context, snapshot) {
return LinkChip(
leading: AvesLogo(
size: style.fontSize * 1.25,
),
text: 'Aves ${snapshot.data?.version}',
url: 'https://github.com/deckerst/aves',
textStyle: style,
);
},
);
}
Widget _buildFlutterLine() {
final style = DefaultTextStyle.of(context).style;
final subColor = style.color.withOpacity(.6);
return Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: EdgeInsetsDirectional.only(end: 4),
child: FlutterLogo(
size: style.fontSize * 1.25,
),
),
),
TextSpan(text: 'Flutter ${version['frameworkVersion']}'),
],
),
style: TextStyle(color: subColor),
);
}
}

View file

@ -0,0 +1,74 @@
import 'package:aves/flutter_version.dart';
import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:aves/widgets/common/identity/aves_logo.dart';
import 'package:flutter/material.dart';
import 'package:package_info/package_info.dart';
class AppReference extends StatefulWidget {
@override
_AppReferenceState createState() => _AppReferenceState();
}
class _AppReferenceState extends State<AppReference> {
Future<PackageInfo> _packageInfoLoader;
@override
void initState() {
super.initState();
_packageInfoLoader = PackageInfo.fromPlatform();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
_buildAvesLine(),
_buildFlutterLine(),
SizedBox(height: 16),
],
),
);
}
Widget _buildAvesLine() {
final textTheme = Theme.of(context).textTheme;
final style = textTheme.headline6.copyWith(fontWeight: FontWeight.bold);
return FutureBuilder<PackageInfo>(
future: _packageInfoLoader,
builder: (context, snapshot) {
return LinkChip(
leading: AvesLogo(
size: style.fontSize * 1.25,
),
text: 'Aves ${snapshot.data?.version}',
url: 'https://github.com/deckerst/aves',
textStyle: style,
);
},
);
}
Widget _buildFlutterLine() {
final style = DefaultTextStyle.of(context).style;
final subColor = style.color.withOpacity(.6);
return Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: EdgeInsetsDirectional.only(end: 4),
child: FlutterLogo(
size: style.fontSize * 1.25,
),
),
),
TextSpan(text: 'Flutter ${version['frameworkVersion']}'),
],
),
style: TextStyle(color: subColor),
);
}
}

View file

@ -0,0 +1,43 @@
import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:flutter/material.dart';
class AboutCredits extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: BoxConstraints(minHeight: 48),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
'Credits',
style: Theme.of(context).textTheme.headline6.copyWith(fontFamily: 'Concourse Caps'),
),
),
),
Text.rich(
TextSpan(
children: [
TextSpan(text: 'This app uses the font '),
WidgetSpan(
child: LinkChip(
text: 'Concourse',
url: 'https://mbtype.com/fonts/concourse/',
textStyle: TextStyle(fontWeight: FontWeight.bold),
),
alignment: PlaceholderAlignment.middle,
),
TextSpan(text: ' for titles and the media information page.'),
],
),
),
SizedBox(height: 16),
],
),
);
}
}

View file

@ -0,0 +1,92 @@
import 'package:aves/model/availability.dart';
import 'package:aves/widgets/about/news_badge.dart';
import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:flutter/material.dart';
class AboutNewVersion extends StatefulWidget {
@override
_AboutNewVersionState createState() => _AboutNewVersionState();
}
class _AboutNewVersionState extends State<AboutNewVersion> {
Future<bool> _newVersionLoader;
@override
void initState() {
super.initState();
_newVersionLoader = availability.isNewVersionAvailable;
}
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: _newVersionLoader,
builder: (context, snapshot) {
final newVersion = snapshot.data == true;
if (!newVersion) return SizedBox();
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: BoxConstraints(minHeight: 48),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: EdgeInsetsDirectional.only(end: 8),
child: AboutNewsBadge(),
),
alignment: PlaceholderAlignment.middle,
),
TextSpan(
text: 'New Version Available',
style: Theme.of(context).textTheme.headline6.copyWith(fontFamily: 'Concourse Caps'),
),
],
),
),
),
),
Text.rich(
TextSpan(
children: [
TextSpan(text: 'A new version of Aves is available on '),
WidgetSpan(
child: LinkChip(
text: 'Github',
url: 'https://github.com/deckerst/aves/releases',
textStyle: TextStyle(fontWeight: FontWeight.bold),
),
alignment: PlaceholderAlignment.middle,
),
TextSpan(text: ' and '),
WidgetSpan(
child: LinkChip(
text: 'Google Play',
url: 'https://play.google.com/store/apps/details?id=deckers.thibault.aves',
textStyle: TextStyle(fontWeight: FontWeight.bold),
),
alignment: PlaceholderAlignment.middle,
),
TextSpan(text: '.'),
],
),
),
SizedBox(height: 16),
],
),
),
Divider(),
],
);
},
);
}
}

View file

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class AboutNewsBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Icon(
Icons.circle,
size: 12,
color: Colors.red,
);
}
}

View file

@ -39,6 +39,7 @@ class DebugSettingsSection extends StatelessWidget {
'infoMapZoom': '${settings.infoMapZoom}', 'infoMapZoom': '${settings.infoMapZoom}',
'pinnedFilters': toMultiline(settings.pinnedFilters), 'pinnedFilters': toMultiline(settings.pinnedFilters),
'searchHistory': toMultiline(settings.searchHistory), 'searchHistory': toMultiline(settings.searchHistory),
'lastVersionCheckDate': '${settings.lastVersionCheckDate}',
}), }),
), ),
], ],

View file

@ -1,5 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/availability.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
@ -11,6 +12,7 @@ import 'package:aves/ref/mime_types.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/about/about_page.dart';
import 'package:aves/widgets/about/news_badge.dart';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart';
import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart';
@ -35,8 +37,16 @@ class AppDrawer extends StatefulWidget {
} }
class _AppDrawerState extends State<AppDrawer> { class _AppDrawerState extends State<AppDrawer> {
Future<bool> _newVersionLoader;
CollectionSource get source => widget.source; CollectionSource get source => widget.source;
@override
void initState() {
super.initState();
_newVersionLoader = availability.isNewVersionAvailable;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final drawerItems = <Widget>[ final drawerItems = <Widget>[
@ -211,12 +221,19 @@ class _AppDrawerState extends State<AppDrawer> {
pageBuilder: (_) => SettingsPage(), pageBuilder: (_) => SettingsPage(),
); );
Widget get aboutTile => NavTile( Widget get aboutTile => FutureBuilder<bool>(
icon: AIcons.info, future: _newVersionLoader,
title: 'About', builder: (context, snapshot) {
topLevel: false, final newVersion = snapshot.data == true;
routeName: AboutPage.routeName, return NavTile(
pageBuilder: (_) => AboutPage(), icon: AIcons.info,
title: 'About',
trailing: newVersion ? AboutNewsBadge() : null,
topLevel: false,
routeName: AboutPage.routeName,
pageBuilder: (_) => AboutPage(),
);
},
); );
Widget get debugTile => NavTile( Widget get debugTile => NavTile(

View file

@ -380,6 +380,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.1" version: "0.2.1"
github:
dependency: "direct main"
description:
name: github
url: "https://pub.dartlang.org"
source: hosted
version: "7.0.4"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -464,6 +471,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3-nullsafety.2" version: "0.6.3-nullsafety.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1"
json_rpc_2: json_rpc_2:
dependency: transitive dependency: transitive
description: description:
@ -1043,6 +1057,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety.3" version: "2.1.0-nullsafety.3"
version:
dependency: "direct main"
description:
name: version
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:

View file

@ -55,6 +55,7 @@ dependencies:
flutter_staggered_animations: flutter_staggered_animations:
flutter_svg: flutter_svg:
geocoder: geocoder:
github:
google_api_availability: google_api_availability:
google_maps_flutter: google_maps_flutter:
intl: intl:
@ -76,6 +77,7 @@ dependencies:
streams_channel: streams_channel:
tuple: tuple:
url_launcher: url_launcher:
version:
xml: xml:
dev_dependencies: dev_dependencies: