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;
|
||||
|
||||
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 {
|
||||
static const baseUrl = "https://api.github.com";
|
||||
|
||||
|
|
|
@ -14,12 +14,8 @@ class SettingsClient {
|
|||
Future<void> loadSettings() async {
|
||||
final data = await _storage.read(key: _settingsKey);
|
||||
if (data == null) return;
|
||||
try {
|
||||
_currentSettings = Settings.fromMap(jsonDecode(data));
|
||||
} catch (_) {
|
||||
_storage.delete(key: _settingsKey);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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/clients/messaging_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/login_screen.dart';
|
||||
import 'package:contacts_plus_plus/widgets/update_notifier.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -59,32 +59,50 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
|||
|
||||
void showUpdateDialogOnFirstBuild(BuildContext context) {
|
||||
final navigator = Navigator.of(context);
|
||||
if (_checkedForUpdate || kDebugMode) return;
|
||||
final settings = ClientHolder
|
||||
.of(context)
|
||||
.settingsClient;
|
||||
if (_checkedForUpdate) return;
|
||||
_checkedForUpdate = true;
|
||||
GithubApi.getLatestTagName().then((value) async {
|
||||
GithubApi.getLatestTagName().then((remoteVer) async {
|
||||
final currentVer = (await PackageInfo.fromPlatform()).version;
|
||||
SemVer currentSem;
|
||||
SemVer remoteSem;
|
||||
SemVer lastDismissedSem;
|
||||
|
||||
try {
|
||||
final currentSem = SemVer.fromString(currentVer);
|
||||
final remoteSem = SemVer.fromString(value);
|
||||
currentSem = SemVer.fromString(currentVer);
|
||||
} catch (_) {
|
||||
currentSem = SemVer.zero();
|
||||
}
|
||||
|
||||
try {
|
||||
lastDismissedSem = SemVer.fromString(settings.currentSettings.lastDismissedVersion.valueOrDefault);
|
||||
} catch (_) {
|
||||
lastDismissedSem = SemVer.zero();
|
||||
}
|
||||
|
||||
try {
|
||||
remoteSem = SemVer.fromString(remoteVer);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteSem <= lastDismissedSem && lastDismissedSem.isNotZero) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteSem > currentSem && navigator.overlay?.context != null) {
|
||||
showDialog(
|
||||
context: navigator.overlay!.context,
|
||||
builder: (context) {
|
||||
return const UpdateNotifier();
|
||||
return UpdateNotifier(
|
||||
remoteVersion: remoteSem,
|
||||
localVersion: currentSem,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (currentVer != value && navigator.overlay?.context != null) {
|
||||
showDialog(
|
||||
context: navigator.overlay!.context,
|
||||
builder: (context) {
|
||||
return const UpdateNotifier();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -101,7 +119,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
|||
textTheme: _typography.white,
|
||||
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) {
|
||||
showUpdateDialogOnFirstBuild(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 'package:contacts_plus_plus/models/friend.dart';
|
||||
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
||||
|
||||
class SettingsEntry<T> {
|
||||
final T? value;
|
||||
|
@ -17,7 +18,7 @@ class SettingsEntry<T> {
|
|||
|
||||
Map toMap() {
|
||||
return {
|
||||
"value": value.toString(),
|
||||
"value": jsonEncode(value),
|
||||
"default": deflt,
|
||||
};
|
||||
}
|
||||
|
@ -35,26 +36,36 @@ class Settings {
|
|||
final SettingsEntry<bool> notificationsDenied;
|
||||
final SettingsEntry<int> unreadCheckIntervalMinutes;
|
||||
final SettingsEntry<int> lastOnlineStatus;
|
||||
final SettingsEntry<String> lastDismissedVersion;
|
||||
|
||||
Settings({
|
||||
SettingsEntry<bool>? notificationsDenied,
|
||||
SettingsEntry<int>? unreadCheckIntervalMinutes,
|
||||
SettingsEntry<int>? lastOnlineStatus,
|
||||
}) : notificationsDenied = notificationsDenied ?? const SettingsEntry(deflt: false),
|
||||
unreadCheckIntervalMinutes = unreadCheckIntervalMinutes ?? const SettingsEntry(deflt: 60),
|
||||
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry(deflt: OnlineStatus.online.index);
|
||||
SettingsEntry<String>? lastDismissedVersion
|
||||
})
|
||||
: 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) {
|
||||
return Settings(
|
||||
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
|
||||
unreadCheckIntervalMinutes: retrieveEntryOrNull<int>(map["unreadCheckIntervalMinutes"]),
|
||||
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
|
||||
lastDismissedVersion: retrieveEntryOrNull<String>(map["lastDismissedVersion"])
|
||||
);
|
||||
}
|
||||
|
||||
static SettingsEntry<T>? retrieveEntryOrNull<T>(Map? map) {
|
||||
if (map == null) return null;
|
||||
try {
|
||||
return SettingsEntry<T>.fromMap(map);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Map toMap() {
|
||||
|
@ -62,18 +73,23 @@ class Settings {
|
|||
"notificationsDenied": notificationsDenied.toMap(),
|
||||
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(),
|
||||
"lastOnlineStatus": lastOnlineStatus.toMap(),
|
||||
"lastDismissedVersion": lastDismissedVersion.toMap(),
|
||||
};
|
||||
}
|
||||
|
||||
Settings copy() => copyWith();
|
||||
|
||||
Settings copyWith({bool? notificationsDenied, int? unreadCheckIntervalMinutes, int? lastOnlineStatus}) {
|
||||
Settings copyWith({
|
||||
bool? notificationsDenied,
|
||||
int? unreadCheckIntervalMinutes,
|
||||
int? lastOnlineStatus,
|
||||
String? lastDismissedVersion,
|
||||
}) {
|
||||
return Settings(
|
||||
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
||||
unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes),
|
||||
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:url_launcher/url_launcher.dart';
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -15,6 +20,17 @@ class UpdateNotifier extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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,),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -28,8 +44,14 @@ class UpdateNotifier extends StatelessWidget {
|
|||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
|
||||
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary
|
||||
foregroundColor: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.onSecondary,
|
||||
backgroundColor: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
),
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text("Get it on Github"),
|
||||
|
@ -39,7 +61,16 @@ class UpdateNotifier extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
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