about: new version check
This commit is contained in:
parent
c05b646ddd
commit
45ba3155b0
13 changed files with 330 additions and 112 deletions
|
@ -1,11 +1,17 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:connectivity/connectivity.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:package_info/package_info.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
final AvesAvailability availability = AvesAvailability._private();
|
||||
|
||||
class AvesAvailability {
|
||||
bool _isConnected, _hasPlayServices;
|
||||
bool _isConnected, _hasPlayServices, _isNewVersionAvailable;
|
||||
|
||||
AvesAvailability._private() {
|
||||
Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult);
|
||||
|
@ -38,4 +44,29 @@ class AvesAvailability {
|
|||
|
||||
// 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 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,9 @@ class Settings extends ChangeNotifier {
|
|||
static const saveSearchHistoryKey = 'save_search_history';
|
||||
static const searchHistoryKey = 'search_history';
|
||||
|
||||
// version
|
||||
static const lastVersionCheckDateKey = 'last_version_check_date';
|
||||
|
||||
Future<void> init() async {
|
||||
_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());
|
||||
|
||||
// version
|
||||
|
||||
DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs.getInt(lastVersionCheckDateKey) ?? 0);
|
||||
|
||||
set lastVersionCheckDate(DateTime newValue) => setAndNotify(lastVersionCheckDateKey, newValue.millisecondsSinceEpoch);
|
||||
|
||||
// convenience methods
|
||||
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
|
|
|
@ -35,7 +35,7 @@ class Durations {
|
|||
static const viewerOverlayChangeAnimation = Duration(milliseconds: 150);
|
||||
static const viewerOverlayPageChooserAnimation = Duration(milliseconds: 200);
|
||||
|
||||
// info
|
||||
// info animations
|
||||
static const mapStyleSwitchAnimation = Duration(milliseconds: 300);
|
||||
static const xmpStructArrayCardTransition = Duration(milliseconds: 300);
|
||||
|
||||
|
@ -49,4 +49,7 @@ class Durations {
|
|||
static const softKeyboardDisplayDelay = Duration(milliseconds: 300);
|
||||
static const searchDebounceDelay = Duration(milliseconds: 250);
|
||||
static const contentChangeDebounceDelay = Duration(milliseconds: 500);
|
||||
|
||||
// app life
|
||||
static const lastVersionCheckInterval = Duration(days: 7);
|
||||
}
|
||||
|
|
|
@ -167,6 +167,12 @@ class Constants {
|
|||
licenseUrl: 'https://github.com/aloisdeniel/flutter_geocoder/blob/master/LICENSE',
|
||||
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(
|
||||
name: 'Google Maps for Flutter',
|
||||
license: 'BSD 3-Clause',
|
||||
|
@ -287,6 +293,12 @@ class Constants {
|
|||
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',
|
||||
),
|
||||
Dependency(
|
||||
name: 'Version',
|
||||
license: 'BSD 3-Clause',
|
||||
licenseUrl: 'https://github.com/dartninja/version/blob/master/LICENSE',
|
||||
sourceUrl: 'https://github.com/dartninja/version',
|
||||
),
|
||||
Dependency(
|
||||
name: 'XML',
|
||||
license: 'MIT',
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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/new_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';
|
||||
|
@ -23,43 +26,9 @@ class AboutPage extends StatelessWidget {
|
|||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
AppReference(),
|
||||
SizedBox(height: 16),
|
||||
Divider(),
|
||||
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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
AboutNewVersion(),
|
||||
AboutCredits(),
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
74
lib/widgets/about/app_ref.dart
Normal file
74
lib/widgets/about/app_ref.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
43
lib/widgets/about/credits.dart
Normal file
43
lib/widgets/about/credits.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
92
lib/widgets/about/new_version.dart
Normal file
92
lib/widgets/about/new_version.dart
Normal 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(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
12
lib/widgets/about/news_badge.dart
Normal file
12
lib/widgets/about/news_badge.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ class DebugSettingsSection extends StatelessWidget {
|
|||
'infoMapZoom': '${settings.infoMapZoom}',
|
||||
'pinnedFilters': toMultiline(settings.pinnedFilters),
|
||||
'searchHistory': toMultiline(settings.searchHistory),
|
||||
'lastVersionCheckDate': '${settings.lastVersionCheckDate}',
|
||||
}),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/availability.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/favourite.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/utils/android_file_utils.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/identity/aves_icons.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
||||
|
@ -35,8 +37,16 @@ class AppDrawer extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _AppDrawerState extends State<AppDrawer> {
|
||||
Future<bool> _newVersionLoader;
|
||||
|
||||
CollectionSource get source => widget.source;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_newVersionLoader = availability.isNewVersionAvailable;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final drawerItems = <Widget>[
|
||||
|
@ -211,12 +221,19 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
pageBuilder: (_) => SettingsPage(),
|
||||
);
|
||||
|
||||
Widget get aboutTile => NavTile(
|
||||
icon: AIcons.info,
|
||||
title: 'About',
|
||||
topLevel: false,
|
||||
routeName: AboutPage.routeName,
|
||||
pageBuilder: (_) => AboutPage(),
|
||||
Widget get aboutTile => FutureBuilder<bool>(
|
||||
future: _newVersionLoader,
|
||||
builder: (context, snapshot) {
|
||||
final newVersion = snapshot.data == true;
|
||||
return NavTile(
|
||||
icon: AIcons.info,
|
||||
title: 'About',
|
||||
trailing: newVersion ? AboutNewsBadge() : null,
|
||||
topLevel: false,
|
||||
routeName: AboutPage.routeName,
|
||||
pageBuilder: (_) => AboutPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Widget get debugTile => NavTile(
|
||||
|
|
21
pubspec.lock
21
pubspec.lock
|
@ -380,6 +380,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
github:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: github
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.0.4"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -464,6 +471,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1043,6 +1057,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -55,6 +55,7 @@ dependencies:
|
|||
flutter_staggered_animations:
|
||||
flutter_svg:
|
||||
geocoder:
|
||||
github:
|
||||
google_api_availability:
|
||||
google_maps_flutter:
|
||||
intl:
|
||||
|
@ -76,6 +77,7 @@ dependencies:
|
|||
streams_channel:
|
||||
tuple:
|
||||
url_launcher:
|
||||
version:
|
||||
xml:
|
||||
|
||||
dev_dependencies:
|
||||
|
|
Loading…
Reference in a new issue