Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track active changes in the application and show list in manage page. #1535

Closed
wants to merge 8 commits into from
8 changes: 8 additions & 0 deletions packages/app_center/lib/src/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@
}
}
},
"managePageInstallingApps": "Installing now ({n})",
"@managePageInstallingApps": {
"placeholders": {
"n": {
"type": "int"
}
}
},
"productivityPageLabel": "Productivity",
"developmentPageLabel": "Development",
"gamesPageLabel": "Games",
Expand Down
19 changes: 19 additions & 0 deletions packages/app_center/lib/src/manage/manage_model.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:app_center/snapd.dart';
import 'package:app_center/src/snapd/logger.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand All @@ -25,6 +26,8 @@ class ManageModel extends ChangeNotifier {

List<Snap>? _installedSnaps;
List<String>? _refreshableSnapNames;
Map<String, ({Snap snap, SnapdChange snapdChange})>
snapsWithInprogressChange = {};

bool _isRefreshable(Snap snap) => updatesModel.hasUpdate(snap.name);
Iterable<Snap> get refreshableSnaps =>
Expand Down Expand Up @@ -59,4 +62,20 @@ class ManageModel extends ChangeNotifier {
_installedSnaps = await snapd.getSnaps().then(
(snaps) => snaps.sortedBy((snap) => snap.titleOrName.toLowerCase()));
}

Future<void> handleSnapChange(Snap snap, SnapdChange change) async {
_state = await AsyncValue.guard(() async {
log.debug(
'handleSnapChange: ${snap.name} with changeId ${change.id} for kind: ${change.kind}, is ${change.status}');
if (change.status == 'Done') {
snapsWithInprogressChange.remove(snap.name);
} else {
snapsWithInprogressChange.putIfAbsent(
snap.name, () => (snap: snap, snapdChange: change));
}

await _getInstalledSnaps();
notifyListeners();
});
}
}
90 changes: 90 additions & 0 deletions packages/app_center/lib/src/manage/manage_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,63 @@ class _ManageView extends ConsumerWidget {
showUpdateButton: true,
),
),
if (manageModel.snapsWithInprogressChange.isNotEmpty)
SliverList.list(
children: [
const SizedBox(height: 48),
Builder(builder: (context) {
final compact = ResponsiveLayout.of(context).type ==
ResponsiveLayoutType.small;
return Flex(
direction: compact ? Axis.vertical : Axis.horizontal,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: compact
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.managePageInstallingApps(
manageModel.snapsWithInprogressChange.length),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.w500),
),
],
);
}),
const SizedBox(height: 24),
if (manageModel.snapsWithInprogressChange.isEmpty)
Text(
l10n.managePageNoUpdatesAvailableDescription,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
if (manageModel.snapsWithInprogressChange.isNotEmpty)
SliverList.builder(
itemCount: manageModel.snapsWithInprogressChange.length,
itemBuilder: (context, index) {
final snapDetails = manageModel.snapsWithInprogressChange[
manageModel.snapsWithInprogressChange.keys
.toList()
.elementAt(index)]!;

return _ManageSnapTile(
snapdChangeId: snapDetails.snapdChange.id,
snap: snapDetails.snap,
position: index ==
(manageModel.snapsWithInprogressChange.length - 1)
? index == 0
? ManageTilePosition.single
: ManageTilePosition.last
: index == 0
? ManageTilePosition.first
: ManageTilePosition.middle,
);
},
),
SliverList.list(children: [
const SizedBox(height: 48),
Text(
Expand Down Expand Up @@ -330,11 +387,13 @@ enum ManageTilePosition { first, middle, last, single }
class _ManageSnapTile extends ConsumerWidget {
const _ManageSnapTile({
required this.snap,
this.snapdChangeId,
this.position = ManageTilePosition.middle,
this.showUpdateButton = false,
});

final Snap snap;
final String? snapdChangeId;
final ManageTilePosition position;
final bool showUpdateButton;

Expand All @@ -347,6 +406,13 @@ class _ManageSnapTile extends ConsumerWidget {
? DateTime.now().difference(snap.installDate!).inDays
: null;

SnapdChange? change;
if (snapdChangeId != null) {
change = ref
.watch(changeProvider(snapdChangeId))
.whenOrNull(data: (data) => data);
}

return ListTile(
key: ValueKey(snap.id),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
Expand Down Expand Up @@ -405,6 +471,30 @@ class _ManageSnapTile extends ConsumerWidget {
)
: const SizedBox(),
),
if (change?.id != null)
Expanded(
child: Row(
children: [
SizedBox.square(
dimension: 16,
child: YaruCircularProgressIndicator(
value: change?.progress,
strokeWidth: 2,
),
),
if (change != null) ...[
const SizedBox(width: 8),
Flexible(
child: Text(
change.localize(l10n) ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
]
],
),
),
Expanded(
child: snap.installedSize != null
? Text(
Expand Down
19 changes: 10 additions & 9 deletions packages/app_center/lib/src/snapd/snap_model.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import 'dart:async';

import 'package:app_center/snapd.dart';
import 'package:app_center/src/manage/manage_model.dart';
import 'package:app_center/src/snapd/logger.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:snapd/snapd.dart';
import 'package:ubuntu_service/ubuntu_service.dart';

final snapModelProvider =
ChangeNotifierProvider.family.autoDispose<SnapModel, String>(
final snapModelProvider = ChangeNotifierProvider.family<SnapModel, String>(
(ref, snapName) => SnapModel(
ref: ref,
snapd: getService<SnapdService>(),
snapName: snapName,
)..init(),
);

final progressProvider =
StreamProvider.family.autoDispose<double, List<String>>((ref, ids) {
StreamProvider.family<double, List<String>>((ref, ids) {
final snapd = getService<SnapdService>();

final streamController = StreamController<double>.broadcast();
Expand All @@ -38,19 +39,17 @@ final progressProvider =
return streamController.stream;
});

final changeProvider =
StreamProvider.family.autoDispose<SnapdChange, String?>((ref, id) {
final changeProvider = StreamProvider.family<SnapdChange, String?>((ref, id) {
if (id == null) return const Stream.empty();
return getService<SnapdService>().watchChange(id);
});

class SnapModel extends ChangeNotifier {
SnapModel({
required this.snapd,
required this.snapName,
}) : _state = const AsyncValue.loading();
SnapModel({required this.snapd, required this.snapName, this.ref})
: _state = const AsyncValue.loading();
final SnapdService snapd;
final String snapName;
final ChangeNotifierProviderRef<SnapModel>? ref;

String? get activeChangeId => _activeChangeId;
String? _activeChangeId;
Expand Down Expand Up @@ -148,6 +147,8 @@ class SnapModel extends ChangeNotifier {
}

Future<void> _activeChangeListener(SnapdChange change) async {
await ref?.read(manageModelProvider).handleSnapChange(snap, change);

if (change.ready) {
log.debug('Change $_activeChangeId for $snapName done');
_setActiveChange(null);
Expand Down
39 changes: 39 additions & 0 deletions packages/app_center/test/manage_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ void main() {
),
];

final snapsWithInprogressChange =
<String, ({Snap snap, SnapdChange snapdChange})>{};
snapsWithInprogressChange.putIfAbsent(
'testsnap4',
() => (
snap: const Snap(
name: 'testsnap4',
title: 'Snap that is being installed',
version: '2.0',
channel: 'latest/stable',
),
snapdChange: SnapdChange(spawnTime: DateTime.now())
),
);

final snapModel = createMockSnapModel(
hasUpdate: true,
localSnap: refreshableSnaps[0],
Expand Down Expand Up @@ -74,6 +89,30 @@ void main() {
findsOneWidget);
});

testWidgets('list installing snaps', (tester) async {
await tester.pumpApp(
(_) => ProviderScope(
overrides: [
launchProvider.overrideWith((_, __) => createMockSnapLauncher()),
manageModelProvider.overrideWith(
(_) => createMockManageModel(
snapsWithInprogressChange: snapsWithInprogressChange),
),
showLocalSystemAppsProvider.overrideWith((ref) => true),
updatesModelProvider.overrideWith((_) => createMockUpdatesModel())
],
child: const ManagePage(),
),
);

final testTile = find.snapTile('Snap that is being installed');
expect(testTile, findsOneWidget);
expect(find.descendant(of: testTile, matching: find.text('2.0')),
findsOneWidget);
expect(find.descendant(of: testTile, matching: find.text('latest/stable')),
findsOneWidget);
});

testWidgets('launch desktop snap', (tester) async {
final snapLauncher = createMockSnapLauncher(isLaunchable: true);
await tester.pumpApp(
Expand Down
4 changes: 4 additions & 0 deletions packages/app_center/test/test_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ DebModel createMockDebModel({
ManageModel createMockManageModel({
Iterable<Snap>? refreshableSnaps,
Iterable<Snap>? nonRefreshableSnaps,
Map<String, ({Snap snap, SnapdChange snapdChange})>?
snapsWithInprogressChange,
AsyncValue<void>? state,
}) {
final model = MockManageModel();
Expand All @@ -171,6 +173,8 @@ ManageModel createMockManageModel({
.thenReturn(refreshableSnaps ?? const Iterable.empty());
when(model.nonRefreshableSnaps)
.thenReturn(nonRefreshableSnaps ?? const Iterable.empty());
when(model.snapsWithInprogressChange)
.thenReturn(snapsWithInprogressChange ?? {});
return model;
}

Expand Down
37 changes: 37 additions & 0 deletions packages/app_center/test/test_utils.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,26 @@ class MockManageModel extends _i1.Mock implements _i17.ManageModel {
),
) as _i6.UpdatesModel);

@override
Map<String, ({_i2.Snap snap, _i2.SnapdChange snapdChange})>
get snapsWithInprogressChange => (super.noSuchMethod(
Invocation.getter(#snapsWithInprogressChange),
returnValue: <String,
({_i2.Snap snap, _i2.SnapdChange snapdChange})>{},
) as Map<String, ({_i2.Snap snap, _i2.SnapdChange snapdChange})>);

@override
set snapsWithInprogressChange(
Map<String, ({_i2.Snap snap, _i2.SnapdChange snapdChange})>?
_snapsWithInprogressChange) =>
super.noSuchMethod(
Invocation.setter(
#snapsWithInprogressChange,
_snapsWithInprogressChange,
),
returnValueForMissingStub: null,
);

@override
_i5.AsyncValue<void> get state => (super.noSuchMethod(
Invocation.getter(#state),
Expand Down Expand Up @@ -783,6 +803,23 @@ class MockManageModel extends _i1.Mock implements _i17.ManageModel {
returnValueForMissingStub: null,
);

@override
_i14.Future<void> handleSnapChange(
_i2.Snap? snap,
_i2.SnapdChange? change,
) =>
(super.noSuchMethod(
Invocation.method(
#handleSnapChange,
[
snap,
change,
],
),
returnValue: _i14.Future<void>.value(),
returnValueForMissingStub: _i14.Future<void>.value(),
) as _i14.Future<void>);

@override
void addListener(_i15.VoidCallback? listener) => super.noSuchMethod(
Invocation.method(
Expand Down