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

feat: Unify update/install/open and use YaruSplitButton #1845

Merged
merged 8 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ command:
flutter: '>=3.24.3'

dev_dependencies:
ubuntu_lints: ^0.4.0
ubuntu_lints: ^0.4.1

scripts:
# build all packages
Expand Down
9 changes: 7 additions & 2 deletions packages/app_center/integration_test/app_center_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:integration_test/integration_test.dart';
import 'package:ubuntu_service/ubuntu_service.dart';
import 'package:ubuntu_test/ubuntu_test.dart';
import 'package:yaru/icons.dart';
import 'package:yaru/widgets.dart';
import 'package:yaru_test/yaru_test.dart';

import '../test/test_utils.dart';
Expand Down Expand Up @@ -105,10 +106,14 @@ Future<void> testRemoveSnap(
}) async {
final installButton = find.button(tester.l10n.snapActionInstallLabel);
expect(installButton, findsNothing);
final menuButton = find.descendant(
of: find.byType(YaruSplitButton),
matching: find.iconButton(YaruIcons.pan_down),
);

await tester.tap(find.iconButton(YaruIcons.view_more_horizontal));
await tester.tap(menuButton);
await tester.pumpAndSettle();
await tester.tap(find.button(tester.l10n.snapActionRemoveLabel));
await tester.tap(find.text(tester.l10n.snapActionRemoveLabel));
await tester.pumpUntil(installButton);

expect(installedFile.existsSync(), isFalse);
Expand Down
3 changes: 2 additions & 1 deletion packages/app_center/lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const kShimmerBaseDark = Color.fromARGB(255, 51, 51, 51);
const kShimmerHighLightLight = Color.fromARGB(200, 247, 247, 247);
const kShimmerHighLightDark = Color.fromARGB(255, 57, 57, 57);

const kCircularProgressIndicatorHeight = 16.0;
const kLoaderHeight = 16.0;
const kLoaderMediumHeight = 32.0;
const kSearchFieldIconConstraints = BoxConstraints(
minWidth: 32,
minHeight: 32,
Expand Down
2 changes: 1 addition & 1 deletion packages/app_center/lib/deb/deb_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class _DebActionButtons extends ConsumerWidget {
.valueOrNull;
return Center(
child: SizedBox.square(
dimension: kCircularProgressIndicatorHeight,
dimension: kLoaderHeight,
child: YaruCircularProgressIndicator(
value: (transaction?.percentage ?? 0) / 100.0,
strokeWidth: 2,
Expand Down
2 changes: 1 addition & 1 deletion packages/app_center/lib/deb/local_deb_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class _LocalDebActionButtons extends ConsumerWidget {
? Row(
children: [
const SizedBox.square(
dimension: kCircularProgressIndicatorHeight,
dimension: kLoaderHeight,
child: YaruCircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 8),
Expand Down
3 changes: 1 addition & 2 deletions packages/app_center/lib/manage/manage_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ class ManagePage extends ConsumerWidget {
index: index,
length: snapListState.snaps.length,
),
showUpdateButton: true,
),
);
},
Expand Down Expand Up @@ -461,7 +460,7 @@ class _SmallLoadingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: kCircularProgressIndicatorHeight,
dimension: kLoaderHeight,
child: YaruCircularProgressIndicator(
value: progress,
strokeWidth: 2,
Expand Down
100 changes: 5 additions & 95 deletions packages/app_center/lib/manage/manage_snap_tile.dart
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
import 'package:app_center/l10n.dart';
import 'package:app_center/layout.dart';
import 'package:app_center/manage/manage_l10n.dart';
import 'package:app_center/manage/update_button.dart';
import 'package:app_center/snapd/snap_action.dart';
import 'package:app_center/manage/snap_actions_button.dart';
import 'package:app_center/snapd/snapd.dart';
import 'package:app_center/store/store.dart';
import 'package:app_center/widgets/snap_menu_item.dart';
import 'package:app_center/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:snapd/snapd.dart';
import 'package:yaru/yaru.dart';

enum ManageTilePosition { first, middle, last, single }

class ManageSnapTile extends StatelessWidget {
const ManageSnapTile({
required this.snap,
this.position = ManageTilePosition.middle,
this.showUpdateButton = false,
this.hasFixedSize = false,
super.key,
});

final Snap snap;
final ManageTilePosition position;
final bool showUpdateButton;
final bool hasFixedSize;

@override
Expand All @@ -36,9 +30,9 @@ class ManageSnapTile extends StatelessWidget {
? DateTime.now().difference(snap.installDate!)
: null;
const radius = Radius.circular(8);
final buttonBar = Align(
final actionButtons = Align(
alignment: Alignment.centerRight,
child: _ButtonBar(snap, showUpdateButton),
child: SnapActionButtons(snapName: snap.name, isPrimary: false),
);

return DecoratedBox(
Expand Down Expand Up @@ -169,8 +163,8 @@ class ManageSnapTile extends StatelessWidget {
],
),
trailing: hasFixedSize
? SizedBox(width: 180, child: buttonBar)
: IntrinsicWidth(child: buttonBar),
? SizedBox(width: 200, child: actionButtons)
: IntrinsicWidth(child: actionButtons),
),
);
}
Expand All @@ -194,87 +188,3 @@ ManageTilePosition determineTilePosition({
return ManageTilePosition.middle;
}
}

class _ButtonBar extends ConsumerWidget {
const _ButtonBar(this.snap, this.showUpdateButton);

final Snap snap;
final bool showUpdateButton;

@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final snapLauncher = ref.watch(launchProvider(snap));
final snapModel = ref.watch(snapModelProvider(snap.name));
final activeChangeId = snapModel.valueOrNull?.activeChangeId;
final removeColor = Theme.of(context).colorScheme.error;
final initialWidgets = _initialWidgetOrder(
snapModel: snapModel,
snapLauncher: snapLauncher,
l10n: l10n,
activeChangeId: activeChangeId,
showUpdateButton: showUpdateButton,
);

return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (initialWidgets.isNotEmpty) ...[
initialWidgets.first,
const SizedBox(width: kSpacing),
],
MenuAnchor(
menuChildren: [
...initialWidgets.skip(1),
SnapMenuItem(
onPressed: () =>
StoreNavigator.pushSnap(context, name: snap.name),
title: l10n.managePageShowDetailsLabel,
),
SnapMenuItem(
onPressed: ref.read(snapModelProvider(snap.name).notifier).remove,
title: SnapAction.remove.label(l10n),
textStyle: TextStyle(color: removeColor),
),
],
builder: (context, controller, child) => YaruOptionButton(
onPressed: controller.isOpen ? controller.close : controller.open,
child: const Icon(YaruIcons.view_more_horizontal),
),
),
],
);
}

List<Widget> _initialWidgetOrder({
required AsyncValue<SnapData> snapModel,
required AppLocalizations l10n,
required SnapLauncher snapLauncher,
required bool showUpdateButton,
required String? activeChangeId,
}) {
final hasActiveChange = activeChangeId != null;
final canOpen = snapLauncher.isLaunchable;
return [
if (hasActiveChange)
ActiveChangeStatus(
snapName: snapModel.valueOrNull?.name,
activeChangeId: activeChangeId,
)
else ...[
if (showUpdateButton)
UpdateButton(snapModel: snapModel, activeChangeId: activeChangeId),
if (!showUpdateButton && canOpen)
OutlinedButton(
onPressed: snapLauncher.open,
child: Text(l10n.snapActionOpenLabel),
),
if (showUpdateButton && canOpen)
SnapMenuItem(
onPressed: snapLauncher.open,
title: l10n.snapActionOpenLabel,
),
],
];
}
}
143 changes: 143 additions & 0 deletions packages/app_center/lib/manage/snap_actions_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import 'package:app_center/constants.dart';
import 'package:app_center/extensions/iterable_extensions.dart';
import 'package:app_center/l10n.dart';
import 'package:app_center/layout.dart';
import 'package:app_center/manage/updates_model.dart';
import 'package:app_center/snapd/snap_action.dart';
import 'package:app_center/snapd/snapd.dart';
import 'package:app_center/widgets/active_change_content.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yaru/yaru.dart';

class SnapActionButtons extends ConsumerWidget {
const SnapActionButtons({
required this.snapName,
required this.isPrimary,
super.key,
});

final String snapName;
final bool isPrimary;

@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final snapModel = ref.watch(snapModelProvider(snapName));
if (!snapModel.hasValue) {
return const Center(
child: SizedBox.square(
dimension: kLoaderMediumHeight,
child: YaruCircularProgressIndicator(),
),
);
}
final snapData = snapModel.value!;
final shouldQuitToUpdate = snapData.localSnap?.refreshInhibit != null;
final snap = snapData.snap;
final snapViewModel = ref.watch(snapModelProvider(snap.name).notifier);
final snapLauncher = snapData.localSnap == null
? null
: ref.watch(launchProvider(snapData.localSnap!));
final canOpen = snapLauncher?.isLaunchable ?? false;
final hasActiveChange = snapData.activeChangeId != null;
final hasUpdate = ref.watch(hasUpdateProvider(snap.name));

final SnapAction? primaryAction;
if (snapData.isInstalled) {
final hasChangedChannel = snapData.selectedChannel != null &&
snapData.localSnap!.trackingChannel != null &&
snapData.selectedChannel != snapData.localSnap!.trackingChannel;

if (hasChangedChannel) {
primaryAction = SnapAction.switchChannel;
} else if (!shouldQuitToUpdate && hasUpdate) {
primaryAction = SnapAction.update;
} else if (canOpen) {
primaryAction = SnapAction.open;
} else {
primaryAction = null;
}
} else {
primaryAction = SnapAction.install;
}

final secondaryActions = [
if (canOpen) SnapAction.open,
if (!shouldQuitToUpdate && hasUpdate) SnapAction.update,
if (snapData.isInstalled) SnapAction.remove,
]..remove(primaryAction ?? SnapAction.open);

final secondaryActionsWidgets = [
...secondaryActions.map((action) {
final color = action == SnapAction.remove
? Theme.of(context).colorScheme.error
: null;
return PopupMenuItem(
onTap: action.callback(snapData, snapViewModel, snapLauncher),
child: IntrinsicWidth(
child: ListTile(
mouseCursor: SystemMouseCursors.click,
title: Text(
action.label(l10n),
style: TextStyle(color: color),
),
),
),
);
}),
];

return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (hasActiveChange)
ActiveChangeStatus(
snapName: snap.name,
activeChangeId: snapData.activeChangeId!,
)
else ...[
if (shouldQuitToUpdate) const QuitToUpdateNotice(),
(isPrimary ? YaruSplitButton.new : YaruSplitButton.outlined.call)(
items: [
if (snapData.isInstalled && snapData.activeChangeId == null)
...secondaryActionsWidgets,
],
onPressed: snapData.activeChangeId == null
? primaryAction?.callback(snapData, snapViewModel, snapLauncher)
: null,
child: Text(
primaryAction?.label(l10n) ?? SnapAction.open.label(l10n),
overflow: TextOverflow.ellipsis,
),
),
],
].separatedBy(const SizedBox(width: kSpacing)),
);
}
}

@visibleForTesting
class QuitToUpdateNotice extends StatelessWidget {
const QuitToUpdateNotice({super.key});

@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final colorScheme = Theme.of(context).colorScheme;

return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(YaruIcons.warning_filled, color: colorScheme.warning),
const SizedBox(width: 8),
Text(
l10n.managePageQuitToUpdate,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
],
);
}
}
Loading
Loading