Make version check more reliable and make check only appear once per version
This commit is contained in:
parent
0d04bcdd1c
commit
639cdebf4e
6 changed files with 179 additions and 78 deletions
|
@ -2,42 +2,6 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
class SemVer {
|
|
||||||
final int major;
|
|
||||||
final int minor;
|
|
||||||
final int patch;
|
|
||||||
|
|
||||||
SemVer({required this.major, required this.minor, required this.patch});
|
|
||||||
|
|
||||||
factory SemVer.fromString(String str) {
|
|
||||||
final split = str.split(".");
|
|
||||||
if (split.length != 3) {
|
|
||||||
throw "Invalid version format";
|
|
||||||
}
|
|
||||||
return SemVer(major: int.parse(split[0]), minor: int.parse(split[1]), patch: int.parse(split[2]));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is SemVer &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
major == other.major &&
|
|
||||||
minor == other.minor &&
|
|
||||||
patch == other.patch;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode;
|
|
||||||
|
|
||||||
bool operator >(SemVer other) {
|
|
||||||
if (major > other.major || (major == other.major && minor > other.minor) || (major == other.major && minor == other.minor && patch > other.patch)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GithubApi {
|
class GithubApi {
|
||||||
static const baseUrl = "https://api.github.com";
|
static const baseUrl = "https://api.github.com";
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,8 @@ class SettingsClient {
|
||||||
Future<void> loadSettings() async {
|
Future<void> loadSettings() async {
|
||||||
final data = await _storage.read(key: _settingsKey);
|
final data = await _storage.read(key: _settingsKey);
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
try {
|
_currentSettings = Settings.fromMap(jsonDecode(data));
|
||||||
_currentSettings = Settings.fromMap(jsonDecode(data));
|
|
||||||
} catch (_) {
|
|
||||||
_storage.delete(key: _settingsKey);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> changeSettings(Settings newSettings) async {
|
Future<void> changeSettings(Settings newSettings) async {
|
||||||
|
|
|
@ -5,10 +5,10 @@ import 'package:contacts_plus_plus/apis/github_api.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:contacts_plus_plus/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/friends_list.dart';
|
import 'package:contacts_plus_plus/widgets/friends/friends_list.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/login_screen.dart';
|
import 'package:contacts_plus_plus/widgets/login_screen.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/update_notifier.dart';
|
import 'package:contacts_plus_plus/widgets/update_notifier.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -59,31 +59,49 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
||||||
|
|
||||||
void showUpdateDialogOnFirstBuild(BuildContext context) {
|
void showUpdateDialogOnFirstBuild(BuildContext context) {
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
if (_checkedForUpdate || kDebugMode) return;
|
final settings = ClientHolder
|
||||||
|
.of(context)
|
||||||
|
.settingsClient;
|
||||||
|
if (_checkedForUpdate) return;
|
||||||
_checkedForUpdate = true;
|
_checkedForUpdate = true;
|
||||||
GithubApi.getLatestTagName().then((value) async {
|
GithubApi.getLatestTagName().then((remoteVer) async {
|
||||||
final currentVer = (await PackageInfo.fromPlatform()).version;
|
final currentVer = (await PackageInfo.fromPlatform()).version;
|
||||||
|
SemVer currentSem;
|
||||||
|
SemVer remoteSem;
|
||||||
|
SemVer lastDismissedSem;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final currentSem = SemVer.fromString(currentVer);
|
currentSem = SemVer.fromString(currentVer);
|
||||||
final remoteSem = SemVer.fromString(value);
|
} catch (_) {
|
||||||
if (remoteSem > currentSem && navigator.overlay?.context != null) {
|
currentSem = SemVer.zero();
|
||||||
showDialog(
|
}
|
||||||
context: navigator.overlay!.context,
|
|
||||||
builder: (context) {
|
try {
|
||||||
return const UpdateNotifier();
|
lastDismissedSem = SemVer.fromString(settings.currentSettings.lastDismissedVersion.valueOrDefault);
|
||||||
},
|
} catch (_) {
|
||||||
);
|
lastDismissedSem = SemVer.zero();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
if (currentVer != value && navigator.overlay?.context != null) {
|
try {
|
||||||
showDialog(
|
remoteSem = SemVer.fromString(remoteVer);
|
||||||
context: navigator.overlay!.context,
|
} catch (_) {
|
||||||
builder: (context) {
|
return;
|
||||||
return const UpdateNotifier();
|
}
|
||||||
},
|
|
||||||
);
|
if (remoteSem <= lastDismissedSem && lastDismissedSem.isNotZero) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteSem > currentSem && navigator.overlay?.context != null) {
|
||||||
|
showDialog(
|
||||||
|
context: navigator.overlay!.context,
|
||||||
|
builder: (context) {
|
||||||
|
return UpdateNotifier(
|
||||||
|
remoteVersion: remoteSem,
|
||||||
|
localVersion: currentSem,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -101,7 +119,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
||||||
textTheme: _typography.white,
|
textTheme: _typography.white,
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark)
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark)
|
||||||
),
|
),
|
||||||
home: Builder(
|
home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
showUpdateDialogOnFirstBuild(context);
|
showUpdateDialogOnFirstBuild(context);
|
||||||
final clientHolder = ClientHolder.of(context);
|
final clientHolder = ClientHolder.of(context);
|
||||||
|
|
76
lib/models/sem_ver.dart
Normal file
76
lib/models/sem_ver.dart
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
|
||||||
|
class SemVer {
|
||||||
|
static final RegExp _versionMatcher = RegExp(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$");
|
||||||
|
static final RegExp _characterFilter = RegExp(r"[a-z]");
|
||||||
|
final int major;
|
||||||
|
final int minor;
|
||||||
|
final int patch;
|
||||||
|
|
||||||
|
SemVer({required this.major, required this.minor, required this.patch});
|
||||||
|
|
||||||
|
factory SemVer.fromString(String str) {
|
||||||
|
str = str.replaceAll(_characterFilter, "");
|
||||||
|
final match = _versionMatcher.firstMatch(str);
|
||||||
|
if (match == null || match.group(1) == null) {
|
||||||
|
throw "Invalid version format";
|
||||||
|
}
|
||||||
|
|
||||||
|
return SemVer(
|
||||||
|
major: int.parse(match.group(1)!),
|
||||||
|
minor: int.parse(match.group(2) ?? "0"),
|
||||||
|
patch: int.parse(match.group(3) ?? "0"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SemVer.zero() {
|
||||||
|
return SemVer(major: 0, minor: 0, patch: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SemVer.max() {
|
||||||
|
//Chosen because it is larger than any version this app will ever see but small enough to not look bad when displayed as text
|
||||||
|
const max = 999;
|
||||||
|
return SemVer(major: max, minor: max, patch: max);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isZero => major == 0 && minor == 0 && patch == 0;
|
||||||
|
|
||||||
|
bool get isNotZero => !isZero;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "$major.$minor.$patch";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SemVer &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
major == other.major &&
|
||||||
|
minor == other.minor &&
|
||||||
|
patch == other.patch;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode;
|
||||||
|
|
||||||
|
bool operator >(SemVer other) {
|
||||||
|
if (major > other.major || (major == other.major && minor > other.minor) || (major == other.major && minor == other.minor && patch > other.patch)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator >=(SemVer other) {
|
||||||
|
if (this == other) return true;
|
||||||
|
return this > other;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator <(SemVer other) {
|
||||||
|
return !(this > other);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator <=(SemVer other) {
|
||||||
|
if (this == other) return true;
|
||||||
|
return this < other;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/models/friend.dart';
|
import 'package:contacts_plus_plus/models/friend.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
||||||
|
|
||||||
class SettingsEntry<T> {
|
class SettingsEntry<T> {
|
||||||
final T? value;
|
final T? value;
|
||||||
|
@ -17,7 +18,7 @@ class SettingsEntry<T> {
|
||||||
|
|
||||||
Map toMap() {
|
Map toMap() {
|
||||||
return {
|
return {
|
||||||
"value": value.toString(),
|
"value": jsonEncode(value),
|
||||||
"default": deflt,
|
"default": deflt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -35,26 +36,36 @@ class Settings {
|
||||||
final SettingsEntry<bool> notificationsDenied;
|
final SettingsEntry<bool> notificationsDenied;
|
||||||
final SettingsEntry<int> unreadCheckIntervalMinutes;
|
final SettingsEntry<int> unreadCheckIntervalMinutes;
|
||||||
final SettingsEntry<int> lastOnlineStatus;
|
final SettingsEntry<int> lastOnlineStatus;
|
||||||
|
final SettingsEntry<String> lastDismissedVersion;
|
||||||
|
|
||||||
Settings({
|
Settings({
|
||||||
SettingsEntry<bool>? notificationsDenied,
|
SettingsEntry<bool>? notificationsDenied,
|
||||||
SettingsEntry<int>? unreadCheckIntervalMinutes,
|
SettingsEntry<int>? unreadCheckIntervalMinutes,
|
||||||
SettingsEntry<int>? lastOnlineStatus,
|
SettingsEntry<int>? lastOnlineStatus,
|
||||||
}) : notificationsDenied = notificationsDenied ?? const SettingsEntry(deflt: false),
|
SettingsEntry<String>? lastDismissedVersion
|
||||||
unreadCheckIntervalMinutes = unreadCheckIntervalMinutes ?? const SettingsEntry(deflt: 60),
|
})
|
||||||
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry(deflt: OnlineStatus.online.index);
|
: notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false),
|
||||||
|
unreadCheckIntervalMinutes = unreadCheckIntervalMinutes ?? const SettingsEntry<int>(deflt: 60),
|
||||||
|
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry<int>(deflt: OnlineStatus.online.index),
|
||||||
|
lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString())
|
||||||
|
;
|
||||||
|
|
||||||
factory Settings.fromMap(Map map) {
|
factory Settings.fromMap(Map map) {
|
||||||
return Settings(
|
return Settings(
|
||||||
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
|
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
|
||||||
unreadCheckIntervalMinutes: retrieveEntryOrNull<int>(map["unreadCheckIntervalMinutes"]),
|
unreadCheckIntervalMinutes: retrieveEntryOrNull<int>(map["unreadCheckIntervalMinutes"]),
|
||||||
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
|
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
|
||||||
|
lastDismissedVersion: retrieveEntryOrNull<String>(map["lastDismissedVersion"])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SettingsEntry<T>? retrieveEntryOrNull<T>(Map? map) {
|
static SettingsEntry<T>? retrieveEntryOrNull<T>(Map? map) {
|
||||||
if (map == null) return null;
|
if (map == null) return null;
|
||||||
return SettingsEntry<T>.fromMap(map);
|
try {
|
||||||
|
return SettingsEntry<T>.fromMap(map);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map toMap() {
|
Map toMap() {
|
||||||
|
@ -62,18 +73,23 @@ class Settings {
|
||||||
"notificationsDenied": notificationsDenied.toMap(),
|
"notificationsDenied": notificationsDenied.toMap(),
|
||||||
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(),
|
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(),
|
||||||
"lastOnlineStatus": lastOnlineStatus.toMap(),
|
"lastOnlineStatus": lastOnlineStatus.toMap(),
|
||||||
|
"lastDismissedVersion": lastDismissedVersion.toMap(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings copy() => copyWith();
|
Settings copy() => copyWith();
|
||||||
|
|
||||||
Settings copyWith({bool? notificationsDenied, int? unreadCheckIntervalMinutes, int? lastOnlineStatus}) {
|
Settings copyWith({
|
||||||
|
bool? notificationsDenied,
|
||||||
|
int? unreadCheckIntervalMinutes,
|
||||||
|
int? lastOnlineStatus,
|
||||||
|
String? lastDismissedVersion,
|
||||||
|
}) {
|
||||||
return Settings(
|
return Settings(
|
||||||
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
||||||
unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes),
|
unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes),
|
||||||
lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus),
|
lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus),
|
||||||
|
lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,8 +1,13 @@
|
||||||
|
import 'package:contacts_plus_plus/client_holder.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class UpdateNotifier extends StatelessWidget {
|
class UpdateNotifier extends StatelessWidget {
|
||||||
const UpdateNotifier({super.key});
|
const UpdateNotifier({required this.remoteVersion, required this.localVersion, super.key});
|
||||||
|
|
||||||
|
final SemVer remoteVersion;
|
||||||
|
final SemVer localVersion;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -15,6 +20,17 @@ class UpdateNotifier extends StatelessWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text("There is a new version available for download!"),
|
const Text("There is a new version available for download!"),
|
||||||
|
const SizedBox(height: 8,),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text("Your version: ${localVersion.toString()}"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text("New version: ${remoteVersion.toString()}"),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 24,),
|
const SizedBox(height: 24,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
@ -27,9 +43,15 @@ class UpdateNotifier extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
foregroundColor: Theme
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondary
|
.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondary,
|
||||||
|
backgroundColor: Theme
|
||||||
|
.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondary
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.download),
|
icon: const Icon(Icons.download),
|
||||||
label: const Text("Get it on Github"),
|
label: const Text("Get it on Github"),
|
||||||
|
@ -39,7 +61,16 @@ class UpdateNotifier extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("I'll do it later."))
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final sClient = ClientHolder
|
||||||
|
.of(context)
|
||||||
|
.settingsClient;
|
||||||
|
sClient.changeSettings(sClient.currentSettings.copyWith(lastDismissedVersion: remoteVersion.toString()));
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text("I'll do it later."),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue