refactor(ui): replace update dot with tappable status chip

Card and detail headers now show a green update chip next to the
extension title; on cards it opens the update dialog directly. The red
dot and the red card button are gone, and the status chip no longer
duplicates inside the detail content. Shorten the English label so the
chip fits narrow cards.
This commit is contained in:
Levi committed 2026-09-19 19:47:02 +08:00
1 parent d8758267c4
commit 51b7abed48
8 files changed
+103 -68

No files matched your search

@@ -8,6 +8,7 @@ import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/detail/app_detail_surface.dart';
import '../../application/extensions_controller.dart';
import '../widgets/extension_detail_view.dart';
import '../widgets/extension_update_status.dart';
class ExtensionDetailsPage extends ConsumerWidget {
const ExtensionDetailsPage({super.key, required this.extensionId, this.initialItem});
@@ -24,6 +25,9 @@ class ExtensionDetailsPage extends ConsumerWidget {
return AppDetailPage(
title: item?.title ?? context.l10n.extensions,
onBack: () => context.canPop() ? context.pop() : context.go('/extensions'),
titleTrailing: _canUpdate(stateAsync.value, item)
? ExtensionUpdateStatus(label: context.l10n.extensionCanUpdate)
: null,
child: item == null
? Center(
child: stateAsync.isLoading
@@ -33,4 +37,9 @@ class ExtensionDetailsPage extends ConsumerWidget {
: ExtensionDetailView(key: ValueKey(item.id), item: item, mobile: true),
);
}
bool _canUpdate(ExtensionsState? state, ExtensionListItem? item) {
final installed = item?.installed;
return item != null && installed != null && (state?.updateFlags.containsKey(installed.identity) ?? false);
}
}
@@ -28,6 +28,7 @@ import '../widgets/extension_detail_view.dart';
import '../widgets/extension_icon.dart';
import '../widgets/extension_setting_field.dart';
import '../widgets/extension_update_dialog.dart';
import '../widgets/extension_update_status.dart';
const _extensionCardMinWidth = 290.0;
const _extensionGridSpacing = 10.0;
@@ -992,15 +993,12 @@ class _ExtensionCard extends ConsumerWidget {
style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w700),
),
),
if (canUpdate) ...[
const SizedBox(width: 6),
Container(
key: ValueKey('extension-card-update-dot-${item.id}'),
width: 8,
height: 8,
decoration: BoxDecoration(color: palette.error, shape: BoxShape.circle),
if (canUpdate && installed != null)
ExtensionUpdateStatus(
key: ValueKey('extension-card-update-status-${item.id}'),
label: context.l10n.extensionCanUpdate,
onTap: busy ? null : () => showExtensionUpdateDialog(context, installed),
),
],
],
),
const SizedBox(height: 2),
@@ -1070,16 +1068,6 @@ class _ExtensionCard extends ConsumerWidget {
key: ValueKey('extension-card-actions-${item.id}'),
mainAxisSize: MainAxisSize.min,
children: [
if (canUpdate && installed != null)
shad.GhostButton(
key: ValueKey('update-extension-${installed.identity}'),
density: shad.ButtonDensity.dense,
onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed),
child: Text(
context.l10n.extensionUpdateAction,
style: TextStyle(color: palette.error, fontWeight: FontWeight.w600),
),
),
if (installed == null && item.store != null)
shad.GhostButton(
key: ValueKey('install-store-extension-${item.store!.id}'),
@@ -21,20 +21,26 @@ import '../../../../util/util.dart';
import '../../application/extensions_controller.dart';
import 'extension_icon.dart';
import 'extension_update_dialog.dart';
import 'extension_update_status.dart';
class ExtensionDetailDrawer extends StatelessWidget {
class ExtensionDetailDrawer extends ConsumerWidget {
const ExtensionDetailDrawer({super.key, required this.item, required this.onClose});
final ExtensionListItem? item;
final VoidCallback onClose;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(extensionsControllerProvider).value;
final installed = item?.installed;
final canUpdate =
item != null && installed != null && (state?.updateFlags.containsKey(installed.identity) ?? false);
return AppDetailDrawer(
open: item != null,
title: item?.title ?? '',
onClose: onClose,
drawerKey: const ValueKey('extension-details-drawer'),
titleTrailing: canUpdate ? ExtensionUpdateStatus(label: context.l10n.extensionCanUpdate) : null,
child: item == null ? const SizedBox.shrink() : ExtensionDetailView(item: item!),
);
}
@@ -104,8 +110,6 @@ class ExtensionDetailView extends ConsumerWidget {
style: TextStyle(color: palette.textMuted, fontSize: 11.5, fontWeight: FontWeight.w600),
),
if (store != null) _ExtensionStats(store: store),
if (installed != null && canUpdate)
_ExtensionStatus(label: context.l10n.extensionCanUpdate, emphasized: true),
],
),
],
@@ -276,35 +280,6 @@ class _ExtensionReadmeState extends State<_ExtensionReadme> {
}
}
class _ExtensionStatus extends StatelessWidget {
const _ExtensionStatus({required this.label, required this.emphasized});
final String label;
final bool emphasized;
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
final dotColor = emphasized ? palette.brand : palette.success;
final textColor = emphasized ? palette.textPrimary : palette.success;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 5,
height: 5,
decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle),
),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(color: textColor, fontSize: 11.5, fontWeight: FontWeight.w600),
),
],
);
}
}
class _LinkButtonContent extends StatelessWidget {
const _LinkButtonContent({required this.icon, required this.label});
@@ -0,0 +1,43 @@
import 'package:flutter/widgets.dart';
import '../../../../shared/theme/app_palette.dart';
/// Green status dot with an optional label, marking an available update.
/// Pass [onTap] to make it open the update prompt.
class ExtensionUpdateStatus extends StatelessWidget {
const ExtensionUpdateStatus({super.key, this.label, this.onTap});
final String? label;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
final content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(color: palette.brand, shape: BoxShape.circle),
),
if (label != null) ...[
const SizedBox(width: 5),
Text(
label!,
style: TextStyle(color: palette.textPrimary, fontSize: 11.5, fontWeight: FontWeight.w600),
),
],
],
);
if (onTap == null) return content;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
child: content,
),
);
}
}
+1 -1
View File
@@ -139,7 +139,7 @@
"extensionLoadMore": "Load More",
"extensionNoMore": "No more extensions",
"extensionInstalled": "Installed",
"extensionCanUpdate": "Update Available",
"extensionCanUpdate": "Update",
"extensionInstall": "Install",
"extensionLoadLocal": "Install Local Folder",
"extensionFilterAll": "All",
@@ -16,6 +16,7 @@ class AppDetailDrawer extends StatelessWidget {
required this.child,
this.drawerKey,
this.width,
this.titleTrailing,
});
final bool open;
@@ -24,6 +25,7 @@ class AppDetailDrawer extends StatelessWidget {
final Widget child;
final Key? drawerKey;
final double? width;
final Widget? titleTrailing;
@override
Widget build(BuildContext context) {
@@ -82,15 +84,22 @@ class AppDetailDrawer extends StatelessWidget {
child: Row(
children: [
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: palette.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
child: Row(
children: [
Flexible(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: palette.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
if (titleTrailing != null) ...[const SizedBox(width: 8), titleTrailing!],
],
),
),
const SizedBox(width: 12),
@@ -13,12 +13,14 @@ class AppDetailPage extends StatelessWidget {
required this.onBack,
required this.child,
this.backgroundColor,
this.titleTrailing,
});
final String title;
final VoidCallback onBack;
final Widget child;
final Color? backgroundColor;
final Widget? titleTrailing;
@override
Widget build(BuildContext context) {
@@ -53,12 +55,19 @@ class AppDetailPage extends StatelessWidget {
),
),
Expanded(
child: Text(
title,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: palette.textPrimary, fontSize: 17, fontWeight: FontWeight.w700),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: palette.textPrimary, fontSize: 17, fontWeight: FontWeight.w700),
),
),
if (titleTrailing != null) ...[const SizedBox(width: 8), titleTrailing!],
],
),
),
const SizedBox(width: 44),
+3 -1
View File
@@ -817,7 +817,9 @@ void main() {
await tester.tap(find.byKey(const ValueKey('extension-card-extension-0')));
await tester.pumpAndSettle();
}
final entry = find.byKey(ValueKey(details ? 'extension-details-update' : 'update-extension-extension-0'));
final entry = find.byKey(
ValueKey(details ? 'extension-details-update' : 'extension-card-update-status-extension-0'),
);
final dialog = find.byKey(const ValueKey('extension-update-dialog'));
final confirm = find.byKey(const ValueKey('confirm-update-extension-button'));
final cancel = find.byKey(const ValueKey('cancel-update-extension-button'));