From 16305542e37ab803e2a3f188ff708eebdb076d84 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sat, 6 May 2023 22:12:18 +0200 Subject: [PATCH] Add update notifier --- lib/apis/github_api.dart | 50 ++++++++++++++++ lib/main.dart | 99 ++++++++++++++++++++++---------- lib/widgets/update_notifier.dart | 46 +++++++++++++++ 3 files changed, 164 insertions(+), 31 deletions(-) create mode 100644 lib/apis/github_api.dart create mode 100644 lib/widgets/update_notifier.dart diff --git a/lib/apis/github_api.dart b/lib/apis/github_api.dart new file mode 100644 index 0000000..02d63bb --- /dev/null +++ b/lib/apis/github_api.dart @@ -0,0 +1,50 @@ +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"; + + static Future getLatestTagName() async { + final response = await http.get(Uri.parse("$baseUrl/repos/Nutcake/contacts-plus-plus/releases/latest")); + if (response.statusCode != 200) return ""; + final body = jsonDecode(response.body); + return body["tag_name"] ?? ""; + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 3436221..e58c746 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,18 @@ import 'dart:developer'; import 'dart:io' show Platform; +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/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'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import 'package:workmanager/workmanager.dart'; import 'models/authentication_data.dart'; @@ -51,43 +55,76 @@ class ContactsPlusPlus extends StatefulWidget { class _ContactsPlusPlusState extends State { final Typography _typography = Typography.material2021(platform: TargetPlatform.android); AuthenticationData _authData = AuthenticationData.unauthenticated(); + bool _checkedForUpdate = false; + + void showUpdateDialogOnFirstBuild(BuildContext context) { + final navigator = Navigator.of(context); + if (_checkedForUpdate || kDebugMode) return; + _checkedForUpdate = true; + GithubApi.getLatestTagName().then((value) async { + final currentVer = (await PackageInfo.fromPlatform()).version; + + try { + final currentSem = SemVer.fromString(currentVer); + final remoteSem = SemVer.fromString(value); + if (remoteSem > currentSem && navigator.overlay?.context != null) { + showDialog( + context: navigator.overlay!.context, + builder: (context) { + return const UpdateNotifier(); + }, + ); + } + } catch (e) { + if (currentVer != value && navigator.overlay?.context != null) { + showDialog( + context: navigator.overlay!.context, + builder: (context) { + return const UpdateNotifier(); + }, + ); + } + } + }); + } @override Widget build(BuildContext context) { return ClientHolder( settingsClient: widget.settingsClient, authenticationData: _authData, - child: Builder( - builder: (context) { - final clientHolder = ClientHolder.of(context); - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Contacts++', - theme: ThemeData( - useMaterial3: true, - textTheme: _typography.white, - colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark) - ), - home: _authData.isAuthenticated ? - ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime. - create: (context) => - MessagingClient( - apiClient: clientHolder.apiClient, - notificationClient: clientHolder.notificationClient, - ), - child: const FriendsList(), - ) : - LoginScreen( - onLoginSuccessful: (AuthenticationData authData) async { - if (authData.isAuthenticated) { - setState(() { - _authData = authData; - }); - } - }, - ) - ); - } + child: MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Contacts++', + theme: ThemeData( + useMaterial3: true, + textTheme: _typography.white, + colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark) + ), + home: Builder( + builder: (context) { + showUpdateDialogOnFirstBuild(context); + final clientHolder = ClientHolder.of(context); + return _authData.isAuthenticated ? + ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime. + create: (context) => + MessagingClient( + apiClient: clientHolder.apiClient, + notificationClient: clientHolder.notificationClient, + ), + child: const FriendsList(), + ) : + LoginScreen( + onLoginSuccessful: (AuthenticationData authData) async { + if (authData.isAuthenticated) { + setState(() { + _authData = authData; + }); + } + }, + ); + } + ) ), ); } diff --git a/lib/widgets/update_notifier.dart b/lib/widgets/update_notifier.dart new file mode 100644 index 0000000..91dcc2b --- /dev/null +++ b/lib/widgets/update_notifier.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class UpdateNotifier extends StatelessWidget { + const UpdateNotifier({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("Update Available", style: Theme + .of(context) + .textTheme + .titleLarge), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("There is a new version available for download!"), + const SizedBox(height: 24,), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + TextButton.icon( + onPressed: () { + launchUrl(Uri.parse("https://github.com/Nutcake/contacts-plus-plus/releases/latest"), + mode: LaunchMode.externalApplication, + ); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + foregroundColor: Theme.of(context).colorScheme.onSecondary, + backgroundColor: Theme.of(context).colorScheme.secondary + ), + icon: const Icon(Icons.download), + label: const Text("Get it on Github"), + ), + ], + ), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("I'll do it later.")) + ], + ); + } +} \ No newline at end of file