Skip to content

Commit

Permalink
Merge pull request #3621 from canonical/update-available-notif
Browse files Browse the repository at this point in the history
Show in-app notification if an update is available
  • Loading branch information
sharder996 authored Sep 12, 2024
2 parents f2bde9e + e655413 commit 793f914
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 106 deletions.
3 changes: 3 additions & 0 deletions src/client/cli/cmd/info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ mp::ReturnCode cmd::Info::run(mp::ArgParser* parser)
auto on_success = [this](mp::InfoReply& reply) {
cout << chosen_formatter->format(reply);

if (term->is_live() && update_available(reply.update_info()))
cout << update_notice(reply.update_info());

return ReturnCode::Ok;
};

Expand Down
34 changes: 23 additions & 11 deletions src/client/gui/lib/grpc_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import 'package:grpc/grpc.dart';
import 'package:protobuf/protobuf.dart' hide RpcClient;
import 'package:rxdart/rxdart.dart';

import 'generated/multipass.pbgrpc.dart';
import 'logger.dart';
import 'providers.dart';
import 'update_available.dart';

export 'generated/multipass.pbgrpc.dart';

Expand All @@ -21,6 +22,21 @@ extension on RpcMessage {
String get repr => '$runtimeType${toProto3Json()}';
}

void checkForUpdate(RpcMessage message) {
final updateInfo = switch (message) {
LaunchReply launchReply => launchReply.updateInfo,
InfoReply infoReply => infoReply.updateInfo,
ListReply listReply => listReply.updateInfo,
NetworksReply networksReply => networksReply.updateInfo,
StartReply startReply => startReply.updateInfo,
RestartReply restartReply => restartReply.updateInfo,
VersionReply versionReply => versionReply.updateInfo,
_ => UpdateInfo(),
};

providerContainer.read(updateProvider.notifier).set(updateInfo);
}

void Function(StreamNotification<RpcMessage>) logGrpc(RpcMessage request) {
return (notification) {
switch (notification.kind) {
Expand Down Expand Up @@ -62,6 +78,7 @@ class GrpcClient {
logger.i('Sent ${request.repr}');
yield* _client
.launch(Stream.value(request))
.doOnData(checkForUpdate)
.doOnEach(logGrpc(request))
.map(Either.left);
for (final mountRequest in mountRequests) {
Expand All @@ -84,6 +101,7 @@ class GrpcClient {
logger.i('Sent ${request.repr}');
return _client
.start(Stream.value(request))
.doOnData(checkForUpdate)
.doOnEach(logGrpc(request))
.firstOrNull;
}
Expand Down Expand Up @@ -117,6 +135,7 @@ class GrpcClient {
logger.i('Sent ${request.repr}');
return _client
.restart(Stream.value(request))
.doOnData(checkForUpdate)
.doOnEach(logGrpc(request))
.firstOrNull;
}
Expand Down Expand Up @@ -167,6 +186,7 @@ class GrpcClient {
);
return _client
.info(Stream.value(request))
.doOnData(checkForUpdate)
.last
.then((r) => r.details.toList());
}
Expand Down Expand Up @@ -204,6 +224,7 @@ class GrpcClient {
logger.i('Sent ${request.repr}');
return _client
.networks(Stream.value(request))
.doOnData(checkForUpdate)
.doOnEach(logGrpc(request))
.last
.then((r) => r.interfaces);
Expand All @@ -214,21 +235,12 @@ class GrpcClient {
logger.i('Sent ${request.repr}');
return _client
.version(Stream.value(request))
.doOnData(checkForUpdate)
.doOnEach(logGrpc(request))
.last
.then((reply) => reply.version);
}

Future<UpdateInfo> updateInfo() {
final request = VersionRequest();
logger.i('Sent ${request.repr}');
return _client
.version(Stream.value(request))
.doOnEach(logGrpc(request))
.last
.then((reply) => reply.updateInfo);
}

Future<String> get(String key) {
final request = GetRequest(key: key);
logger.i('Sent ${request.repr}');
Expand Down
2 changes: 1 addition & 1 deletion src/client/gui/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ void main() async {
await hotKeyManager.unregisterAll();
final sharedPreferences = await SharedPreferences.getInstance();

final providerContainer = ProviderContainer(overrides: [
providerContainer = ProviderContainer(overrides: [
guiSettingProvider.overrideWith(() {
return GuiSettingNotifier(sharedPreferences);
}),
Expand Down
15 changes: 8 additions & 7 deletions src/client/gui/lib/notifications/notification_entries.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import 'package:flutter/material.dart';
import 'notifications_list.dart';

class SimpleNotification extends StatelessWidget {
final String text;
final Widget child;
final Widget icon;
final Color barColor;
final bool closeable;
final double barFullness;

const SimpleNotification({
super.key,
required this.text,
required this.child,
required this.icon,
required this.barColor,
this.closeable = true,
Expand Down Expand Up @@ -40,7 +40,7 @@ class SimpleNotification extends StatelessWidget {
child: FittedBox(fit: BoxFit.fill, child: icon),
),
const SizedBox(width: 8),
Expanded(child: Text(text)),
Expanded(child: child),
]),
),
),
Expand Down Expand Up @@ -113,21 +113,22 @@ class _TimeoutNotificationState extends State<TimeoutNotification>
child: AnimatedBuilder(
animation: timeoutController,
builder: (_, __) => SimpleNotification(
text: widget.text,
icon: widget.icon,
barColor: widget.barColor,
barFullness: 1.0 - timeoutController.value,
child: Text(widget.text),
),
),
);
}
}

class ErrorNotification extends SimpleNotification {
const ErrorNotification({
ErrorNotification({
super.key,
required super.text,
required String text,
}) : super(
child: Text(text),
barColor: Colors.red,
icon: const Icon(Icons.cancel_outlined, color: Colors.red),
);
Expand Down Expand Up @@ -167,14 +168,14 @@ class OperationNotification extends StatelessWidget {
}

return SimpleNotification(
text: text,
barColor: Colors.blue,
closeable: false,
icon: const CircularProgressIndicator(
color: Colors.blue,
strokeAlign: -2,
strokeWidth: 3.5,
),
child: Text(text),
);
},
);
Expand Down
2 changes: 2 additions & 0 deletions src/client/gui/lib/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import 'logger.dart';

export 'grpc_client.dart';

late final ProviderContainer providerContainer;

final grpcClientProvider = Provider((_) {
final address = getServerAddress();
final certPair = getCertPair();
Expand Down
119 changes: 32 additions & 87 deletions src/client/gui/lib/settings/general_settings.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import 'package:basics/basics.dart';
import 'package:flutter/material.dart' hide Switch;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';

import '../dropdown.dart';
import '../notifications/notifications_provider.dart';
import '../notifications.dart';
import '../providers.dart';
import '../switch.dart';
import '../update_available.dart';
import 'autostart_notifiers.dart';

final updateProvider = Provider.autoDispose((ref) {
ref
.watch(grpcClientProvider)
.updateInfo()
.then((value) => ref.state = value)
.ignore();
return UpdateInfo();
});

final onAppCloseProvider = guiSettingProvider(onAppCloseKey);

class GeneralSettings extends ConsumerWidget {
Expand All @@ -30,85 +20,40 @@ class GeneralSettings extends ConsumerWidget {
final autostart = ref.watch(autostartProvider).valueOrNull ?? false;
final onAppClose = ref.watch(onAppCloseProvider);

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'General',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
if (update.version.isNotBlank) UpdateAvailable(update),
Switch(
label: 'Open the Multipass GUI on startup',
value: autostart,
trailingSwitch: true,
size: 30,
onChanged: (value) {
ref
.read(autostartProvider.notifier)
.set(value)
.onError(ref.notifyError((e) => 'Failed to set autostart: $e'));
},
),
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text(
'General',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
if (update.version.isNotBlank) ...[
UpdateAvailable(update),
const SizedBox(height: 20),
Dropdown(
label: 'On close of application',
width: 260,
value: onAppClose ?? 'ask',
onChanged: (value) =>
ref.read(onAppCloseProvider.notifier).set(value!),
items: const {
'ask': 'Ask about running instances',
'stop': 'Stop running instances',
'nothing': 'Do not stop running instances',
},
),
],
);
}
}

class UpdateAvailable extends StatelessWidget {
final UpdateInfo updateInfo;

const UpdateAvailable(this.updateInfo, {super.key});

static final installUrl = Uri.parse('https://multipass.run/install');

static void launchInstallUrl() => launchUrl(installUrl);

@override
Widget build(BuildContext context) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Update available', style: TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Container(
color: const Color(0xffF7F7F7),
padding: const EdgeInsets.all(12),
width: 480,
child: Row(children: [
Container(
alignment: Alignment.center,
color: const Color(0xffE95420),
height: 48,
width: 48,
child: SvgPicture.asset('assets/multipass.svg', width: 30),
),
const SizedBox(width: 12),
Text(
'Multipass ${updateInfo.version}\nis available',
style: const TextStyle(fontSize: 16),
),
const SizedBox(width: 12),
const Spacer(),
const TextButton(
onPressed: launchInstallUrl,
child: Text('Upgrade now'),
),
]),
Switch(
label: 'Open the Multipass GUI on startup',
value: autostart,
trailingSwitch: true,
size: 30,
onChanged: (value) {
ref
.read(autostartProvider.notifier)
.set(value)
.onError(ref.notifyError((e) => 'Failed to set autostart: $e'));
},
),
const SizedBox(height: 20),
Dropdown(
label: 'On close of application',
width: 260,
value: onAppClose ?? 'ask',
onChanged: (value) => ref.read(onAppCloseProvider.notifier).set(value!),
items: const {
'ask': 'Ask about running instances',
'stop': 'Stop running instances',
'nothing': 'Do not stop running instances',
},
),
]);
}
}
Loading

0 comments on commit 793f914

Please sign in to comment.