Make version check more reliable and make check only appear once per version

This commit is contained in:
Nutcake 2023-05-07 15:01:01 +02:00
parent 0d04bcdd1c
commit 639cdebf4e
6 changed files with 179 additions and 78 deletions

View file

@ -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";

View file

@ -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 {

View file

@ -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
View 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;
}
}

View file

@ -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),
);
}
}

View file

@ -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."),
),
],
);
}