mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 08:30:29 +00:00
feat(ui): rework extension update detection and entry point (#1535)
This commit is contained in:
31 files changed
+434
-228
No files matched your search
@@ -185,12 +185,11 @@ class ExtensionsController extends AsyncNotifier<ExtensionsState> {
|
||||
}
|
||||
|
||||
bool canUpdateItem(ExtensionListItem item) {
|
||||
final current = _current;
|
||||
if (item.installed == null) return false;
|
||||
if (current.listFilter == ExtensionListFilter.market && item.store != null) {
|
||||
return _compareVersion(item.store!.version, item.installed!.version) > 0;
|
||||
}
|
||||
return current.updateFlags.containsKey(item.installed!.identity);
|
||||
final installed = item.installed;
|
||||
if (installed == null) return false;
|
||||
// The backend checks GitHub live for every installed extension; the store
|
||||
// index may lag behind and must not decide update visibility.
|
||||
return _current.updateFlags.containsKey(installed.identity);
|
||||
}
|
||||
|
||||
Future<void> installFromStore(StoreExtension extension) async {
|
||||
@@ -240,6 +239,9 @@ class ExtensionsController extends AsyncNotifier<ExtensionsState> {
|
||||
await _runBusy(extension.identity, () async {
|
||||
await ref.read(gopeedServiceProvider).updateExtension(extension.identity);
|
||||
await loadInstalled(refreshUpdates: false);
|
||||
// Drop the stale flag immediately; the background re-check confirms it.
|
||||
final flags = Map<String, String>.of(_current.updateFlags)..remove(extension.identity);
|
||||
state = AsyncValue.data(_current.copyWith(updateFlags: flags));
|
||||
unawaited(checkUpdate());
|
||||
_bumpStoreInstallCount(extension.identity);
|
||||
_reportInstallSafe(extension.identity);
|
||||
@@ -354,24 +356,4 @@ class ExtensionsController extends AsyncNotifier<ExtensionsState> {
|
||||
}());
|
||||
}
|
||||
|
||||
static int _compareVersion(String a, String b) {
|
||||
final aNums = _toVersionNumbers(a);
|
||||
final bNums = _toVersionNumbers(b);
|
||||
final maxLen = aNums.length > bNums.length ? aNums.length : bNums.length;
|
||||
for (var i = 0; i < maxLen; i++) {
|
||||
final left = i < aNums.length ? aNums[i] : 0;
|
||||
final right = i < bNums.length ? bNums[i] : 0;
|
||||
if (left > right) return 1;
|
||||
if (left < right) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static List<int> _toVersionNumbers(String version) {
|
||||
return version
|
||||
.split(RegExp(r'[^0-9]+'))
|
||||
.where((part) => part.isNotEmpty)
|
||||
.map((part) => int.tryParse(part) ?? 0)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ 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_dialog.dart';
|
||||
import '../widgets/extension_update_status.dart';
|
||||
|
||||
class ExtensionDetailsPage extends ConsumerWidget {
|
||||
const ExtensionDetailsPage({super.key, required this.extensionId, this.initialItem});
|
||||
@@ -24,6 +26,15 @@ 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(
|
||||
key: const ValueKey('extension-details-update-mobile'),
|
||||
label: context.l10n.extensionCanUpdate,
|
||||
onTap: item?.installed == null
|
||||
? null
|
||||
: () => showExtensionUpdateDialog(context, item!.installed!),
|
||||
)
|
||||
: null,
|
||||
child: item == null
|
||||
? Center(
|
||||
child: stateAsync.isLoading
|
||||
@@ -33,4 +44,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;
|
||||
@@ -485,25 +486,7 @@ class _Content extends StatelessWidget {
|
||||
}
|
||||
|
||||
bool _canUpdate(ExtensionsState state, ExtensionListItem item) {
|
||||
if (item.installed == null) return false;
|
||||
if (state.listFilter == ExtensionListFilter.market && item.store != null) {
|
||||
return _compareVersion(item.store!.version, item.installed!.version) > 0;
|
||||
}
|
||||
return state.updateFlags.containsKey(item.installed!.identity);
|
||||
}
|
||||
|
||||
int _compareVersion(String a, String b) {
|
||||
final aNums = a.split(RegExp(r'[^0-9]+')).where((part) => part.isNotEmpty).map((part) => int.tryParse(part) ?? 0);
|
||||
final bNums = b.split(RegExp(r'[^0-9]+')).where((part) => part.isNotEmpty).map((part) => int.tryParse(part) ?? 0);
|
||||
final left = aNums.toList();
|
||||
final right = bNums.toList();
|
||||
final maxLen = left.length > right.length ? left.length : right.length;
|
||||
for (var i = 0; i < maxLen; i++) {
|
||||
final l = i < left.length ? left[i] : 0;
|
||||
final r = i < right.length ? right[i] : 0;
|
||||
if (l != r) return l.compareTo(r);
|
||||
}
|
||||
return 0;
|
||||
return item.installed != null && state.updateFlags.containsKey(item.installed!.identity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -994,11 +977,28 @@ class _ExtensionCard extends ConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w700),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
if (canUpdate && installed != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: ExtensionUpdateStatus(
|
||||
key: ValueKey('extension-card-update-status-${item.id}'),
|
||||
label: context.l10n.extensionCanUpdate,
|
||||
onTap: busy ? null : () => showExtensionUpdateDialog(context, installed),
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
@@ -1067,13 +1067,6 @@ class _ExtensionCard extends ConsumerWidget {
|
||||
key: ValueKey('extension-card-actions-${item.id}'),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (canUpdate && installed != null)
|
||||
shad.GhostButton(
|
||||
density: shad.ButtonDensity.icon,
|
||||
key: ValueKey('update-extension-${installed.identity}'),
|
||||
onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed),
|
||||
child: const Icon(Icons.refresh),
|
||||
),
|
||||
if (installed == null && item.store != null)
|
||||
shad.GhostButton(
|
||||
key: ValueKey('install-store-extension-${item.store!.id}'),
|
||||
@@ -1127,21 +1120,17 @@ class _ExtensionCard extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
);
|
||||
if (item.store == null) return card;
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
key: ValueKey('open-extension-details-${item.id}'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (MediaQuery.sizeOf(context).width < Breakpoints.mobile) {
|
||||
context.push('/extensions/${Uri.encodeComponent(item.id)}', extra: item);
|
||||
return;
|
||||
}
|
||||
onOpenDetails(item);
|
||||
},
|
||||
child: card,
|
||||
),
|
||||
return GestureDetector(
|
||||
key: ValueKey('open-extension-details-${item.id}'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (MediaQuery.sizeOf(context).width < Breakpoints.mobile) {
|
||||
context.push('/extensions/${Uri.encodeComponent(item.id)}', extra: item);
|
||||
return;
|
||||
}
|
||||
onOpenDetails(item);
|
||||
},
|
||||
child: card,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+63
-116
@@ -21,20 +21,33 @@ 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);
|
||||
final updateChip = canUpdate
|
||||
? ExtensionUpdateStatus(
|
||||
key: const ValueKey('extension-details-update'),
|
||||
label: context.l10n.extensionCanUpdate,
|
||||
onTap: () => showExtensionUpdateDialog(context, installed),
|
||||
)
|
||||
: null;
|
||||
return AppDetailDrawer(
|
||||
open: item != null,
|
||||
title: item?.title ?? '',
|
||||
onClose: onClose,
|
||||
drawerKey: const ValueKey('extension-details-drawer'),
|
||||
titleTrailing: updateChip,
|
||||
child: item == null ? const SizedBox.shrink() : ExtensionDetailView(item: item!),
|
||||
);
|
||||
}
|
||||
@@ -104,8 +117,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),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -130,15 +141,6 @@ class ExtensionDetailView extends ConsumerWidget {
|
||||
_runAction(context, () => ref.read(extensionsControllerProvider.notifier).installFromStore(store)),
|
||||
child: Text(context.l10n.extensionInstall),
|
||||
),
|
||||
if (installed != null && canUpdate)
|
||||
AppLoadingButton(
|
||||
key: const ValueKey('extension-details-update'),
|
||||
loading: false,
|
||||
variant: AppLoadingButtonVariant.primary,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed),
|
||||
child: Text(context.l10n.newVersionUpdate),
|
||||
),
|
||||
if (installed != null && !canUpdate)
|
||||
AppLoadingButton(
|
||||
key: const ValueKey('extension-details-installed'),
|
||||
@@ -162,7 +164,13 @@ class ExtensionDetailView extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (store != null) ...[SizedBox(height: mobile ? 24 : 28), _ExtensionReadme(item: current, mobile: mobile)],
|
||||
SizedBox(height: mobile ? 24 : 28),
|
||||
_ExtensionReadme(
|
||||
// Snapshot the version so upgrades reload the installed README.
|
||||
key: ValueKey((current.id, installed?.version, installed?.devMode, installed?.devPath, store?.readme)),
|
||||
item: current,
|
||||
mobile: mobile,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -180,7 +188,7 @@ class ExtensionDetailView extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class _ExtensionReadme extends StatefulWidget {
|
||||
const _ExtensionReadme({required this.item, required this.mobile});
|
||||
const _ExtensionReadme({super.key, required this.item, required this.mobile});
|
||||
|
||||
final ExtensionListItem item;
|
||||
final bool mobile;
|
||||
@@ -190,7 +198,7 @@ class _ExtensionReadme extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ExtensionReadmeState extends State<_ExtensionReadme> {
|
||||
late Future<_ReadmeInfo> _readme;
|
||||
late final _ReadmeInfo _readme;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -198,109 +206,48 @@ class _ExtensionReadmeState extends State<_ExtensionReadme> {
|
||||
_readme = _loadReadme(widget.item);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _ExtensionReadme oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final previous = oldWidget.item.installed;
|
||||
final current = widget.item.installed;
|
||||
if (previous?.identity != current?.identity ||
|
||||
previous?.devMode != current?.devMode ||
|
||||
previous?.devPath != current?.devPath ||
|
||||
oldWidget.item.store?.readme != widget.item.store?.readme) {
|
||||
_readme = _loadReadme(widget.item);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
return FutureBuilder<_ReadmeInfo>(
|
||||
future: _readme,
|
||||
builder: (context, snapshot) {
|
||||
final info = snapshot.data;
|
||||
if (info == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (info.content.trim().isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
key: const ValueKey('extension-details-readme'),
|
||||
padding: EdgeInsets.only(top: widget.mobile ? 32 : 36),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'README',
|
||||
style: TextStyle(
|
||||
color: palette.textMuted,
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
MarkdownBody(
|
||||
data: info.content,
|
||||
selectable: true,
|
||||
styleSheet: _markdownStyle(palette, mobile: widget.mobile),
|
||||
onTapLink: (_, href, _) {
|
||||
final resolved = _resolveReadmeUrl(widget.item, href, info: info, forImage: false);
|
||||
if (resolved != null) unawaited(_openUrl(resolved));
|
||||
},
|
||||
imageBuilder: (uri, title, alt) {
|
||||
final resolved = _resolveReadmeUrl(widget.item, uri.toString(), info: info, forImage: true);
|
||||
if (resolved == null) return const SizedBox.shrink();
|
||||
final image = resolved.startsWith('file:')
|
||||
? Image.file(
|
||||
File(Uri.parse(resolved).toFilePath()),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) => const SizedBox.shrink(),
|
||||
)
|
||||
: Image.network(
|
||||
resolved,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) => const SizedBox.shrink(),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: ClipRRect(borderRadius: BorderRadius.circular(6), child: image),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
final info = _readme;
|
||||
if (info.content.trim().isEmpty) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
key: const ValueKey('extension-details-readme'),
|
||||
padding: EdgeInsets.only(top: widget.mobile ? 32 : 36),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'README',
|
||||
style: TextStyle(color: palette.textMuted, fontSize: 10.5, fontWeight: FontWeight.w800, letterSpacing: 1.2),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
MarkdownBody(
|
||||
data: info.content,
|
||||
selectable: true,
|
||||
styleSheet: _markdownStyle(palette, mobile: widget.mobile),
|
||||
onTapLink: (_, href, _) {
|
||||
final resolved = _resolveReadmeUrl(widget.item, href, info: info, forImage: false);
|
||||
if (resolved != null) unawaited(_openUrl(resolved));
|
||||
},
|
||||
imageBuilder: (uri, title, alt) {
|
||||
final resolved = _resolveReadmeUrl(widget.item, uri.toString(), info: info, forImage: true);
|
||||
if (resolved == null) return const SizedBox.shrink();
|
||||
final image = resolved.startsWith('file:')
|
||||
? Image.file(
|
||||
File(Uri.parse(resolved).toFilePath()),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) => const SizedBox.shrink(),
|
||||
)
|
||||
: Image.network(resolved, fit: BoxFit.contain, errorBuilder: (_, _, _) => const SizedBox.shrink());
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: ClipRRect(borderRadius: BorderRadius.circular(6), child: image),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -407,7 +354,7 @@ MarkdownStyleSheet _markdownStyle(AppPalette palette, {required bool mobile}) {
|
||||
);
|
||||
}
|
||||
|
||||
Future<_ReadmeInfo> _loadReadme(ExtensionListItem item) async {
|
||||
_ReadmeInfo _loadReadme(ExtensionListItem item) {
|
||||
final installed = item.installed;
|
||||
final remote = item.store?.readme ?? '';
|
||||
if (installed == null || Util.isWeb()) return _ReadmeInfo(content: remote);
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class _ExtensionUpdateDialogState extends ConsumerState<_ExtensionUpdateDialog>
|
||||
canPop: !_updating,
|
||||
child: AlertDialog(
|
||||
key: const ValueKey('extension-update-dialog'),
|
||||
title: Text(context.l10n.extensionCanUpdate),
|
||||
title: Text(context.l10n.newVersionAvailable),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
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, this.compact = false});
|
||||
|
||||
final String? label;
|
||||
final VoidCallback? onTap;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
final content = Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: compact ? 2 : 3),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.brandSoft,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: palette.brand.withValues(alpha: 0.35)),
|
||||
),
|
||||
child: 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),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 11.5, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
if (onTap == null) return content;
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 2, vertical: compact ? 2 : 4),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "إضافة",
|
||||
"edit": "تعديل",
|
||||
"newVersionTitle": "يتوفر إصدار جديد @version",
|
||||
"newVersionUpdate": "تحديث الآن",
|
||||
"newVersionUpdate": "تحديث",
|
||||
"newVersionLater": "لاحقاً",
|
||||
"extensions": "الإضافات",
|
||||
"extensionInstallUrl": "عنوان التثبيت",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"add": "Əlavə et",
|
||||
"edit": "Redaktə et",
|
||||
"newVersionTitle": "Yeni versiya @version tapıldı",
|
||||
"newVersionUpdate": "İndi Yenilə",
|
||||
"newVersionUpdate": "Yenilə",
|
||||
"newVersionLater": "Sonra",
|
||||
"extensions": "Uzantılar",
|
||||
"extensionInstallUrl": "Quraşdırma URL-i",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Afegeix",
|
||||
"edit": "Edita",
|
||||
"newVersionTitle": "Descobriu la nova versió @version",
|
||||
"newVersionUpdate": "Actualitza ara",
|
||||
"newVersionUpdate": "Actualitza",
|
||||
"newVersionLater": "Més tard",
|
||||
"extensions": "Extensions",
|
||||
"extensionInstallUrl": "URL d'instal·lació",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Hinzugefügt",
|
||||
"edit": "Bearbeiten",
|
||||
"newVersionTitle": "Entdecken Sie die neue Version @version",
|
||||
"newVersionUpdate": "Jetzt aktualisieren",
|
||||
"newVersionUpdate": "Aktualisieren",
|
||||
"newVersionLater": "Später",
|
||||
"extensions": "Erweiterungen",
|
||||
"extensionInstallUrl": "Installations-URL",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"newVersionTitle": "Discover new version @version",
|
||||
"newVersionUpdate": "Update Now",
|
||||
"newVersionUpdate": "Update",
|
||||
"newVersionLater": "Later",
|
||||
"extensions": "Extensions",
|
||||
"extensionInstallUrl": "Install URL",
|
||||
@@ -137,7 +137,7 @@
|
||||
"extensionLoadMore": "Load More",
|
||||
"extensionNoMore": "No more extensions",
|
||||
"extensionInstalled": "Installed",
|
||||
"extensionCanUpdate": "Update Available",
|
||||
"extensionCanUpdate": "Update",
|
||||
"extensionInstall": "Install",
|
||||
"extensionLoadLocal": "Install Local Folder",
|
||||
"extensionFilterAll": "All",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Añadir",
|
||||
"edit": "Editar",
|
||||
"newVersionTitle": "Nueva versión @version disponible",
|
||||
"newVersionUpdate": "Actualizar Ahora",
|
||||
"newVersionUpdate": "Actualizar",
|
||||
"newVersionLater": "Más tarde",
|
||||
"extensions": "Extensiones",
|
||||
"extensionInstallUrl": "URL de instalación",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "افزودن",
|
||||
"edit": "ویرایش",
|
||||
"newVersionTitle": "@version عنوان: کشف نسخه جدید",
|
||||
"newVersionUpdate": "بروزرسانی",
|
||||
"newVersionUpdate": "بهروزرسانی",
|
||||
"newVersionLater": "بعداً",
|
||||
"extensions": "افزونهها",
|
||||
"extensionInstallUrl": "نشانی نصب",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Ajouter",
|
||||
"edit": "Modifier",
|
||||
"newVersionTitle": "Nouvelle version @version disponible",
|
||||
"newVersionUpdate": "Mettre à jour maintenant",
|
||||
"newVersionUpdate": "Mettre à jour",
|
||||
"newVersionLater": "Plus tard",
|
||||
"extensions": "Extensions",
|
||||
"extensionInstallUrl": "URL d'installation",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Hozzáad",
|
||||
"edit": "Szerk",
|
||||
"newVersionTitle": "Új verzió felfedezése @version",
|
||||
"newVersionUpdate": "Frissítés most",
|
||||
"newVersionUpdate": "Frissítés",
|
||||
"newVersionLater": "Később",
|
||||
"extensions": "Bővítmények",
|
||||
"extensionInstallUrl": "Telepítési URL",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Tambah",
|
||||
"edit": "Sunting",
|
||||
"newVersionTitle": "Temukan versi batu @version",
|
||||
"newVersionUpdate": "Perbarui Sekarang",
|
||||
"newVersionUpdate": "Perbarui",
|
||||
"newVersionLater": "Nanti",
|
||||
"extensions": "Ekstensi",
|
||||
"extensionInstallUrl": "URL Instal",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Aggiungi",
|
||||
"edit": "Modifica",
|
||||
"newVersionTitle": "Scopri la nuova versione @version",
|
||||
"newVersionUpdate": "Aggiorna ora",
|
||||
"newVersionUpdate": "Aggiorna",
|
||||
"newVersionLater": "Dopo",
|
||||
"extensions": "Estensioni",
|
||||
"extensionInstallUrl": "Installa URL",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "追加",
|
||||
"edit": "編集",
|
||||
"newVersionTitle": "新しいバージョン @version を発見する",
|
||||
"newVersionUpdate": "アップデート",
|
||||
"newVersionUpdate": "更新",
|
||||
"newVersionLater": "後で",
|
||||
"extensions": "拡張機能",
|
||||
"extensionInstallUrl": "インストールURL",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "추가",
|
||||
"edit": "편집",
|
||||
"newVersionTitle": "새 버전 @version 발견",
|
||||
"newVersionUpdate": "지금 업데이트",
|
||||
"newVersionUpdate": "업데이트",
|
||||
"newVersionLater": "나중에",
|
||||
"extensions": "확장 프로그램",
|
||||
"extensionInstallUrl": "설치 URL",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Dodaj",
|
||||
"edit": "Edycja",
|
||||
"newVersionTitle": "Sprawdź aktualizację @version",
|
||||
"newVersionUpdate": "Aktualizuj teraz",
|
||||
"newVersionUpdate": "Aktualizuj",
|
||||
"newVersionLater": "Później",
|
||||
"extensions": "Wtyczki",
|
||||
"extensionInstallUrl": "Instaluj z url",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Adicionar",
|
||||
"edit": "Editar",
|
||||
"newVersionTitle": "Nova Versão Disponível @version",
|
||||
"newVersionUpdate": "Atualizar Agora",
|
||||
"newVersionUpdate": "Atualizar",
|
||||
"newVersionLater": "Mais Tarde",
|
||||
"extensions": "Extensões",
|
||||
"extensionInstallUrl": "Link de Instalação",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "சேர்",
|
||||
"edit": "திருத்துக",
|
||||
"newVersionTitle": "புதிய பதிப்பைக் கண்டறியவும் @version",
|
||||
"newVersionUpdate": "இப்பொழுது புதுப்பி",
|
||||
"newVersionUpdate": "புதுப்பிப்பு",
|
||||
"newVersionLater": "பின்னர்",
|
||||
"extensions": "நீட்டிப்புகள்",
|
||||
"extensionInstallUrl": "நிறுவப்படும் URL",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Ekle",
|
||||
"edit": "Düzenle",
|
||||
"newVersionTitle": "Yeni sürüm @version keşfedildi",
|
||||
"newVersionUpdate": "Şimdi Güncelle",
|
||||
"newVersionUpdate": "Güncelle",
|
||||
"newVersionLater": "Daha Sonra",
|
||||
"extensions": "Eklentiler",
|
||||
"extensionInstallUrl": "Kurulum URL'si",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Додати",
|
||||
"edit": "Редагування",
|
||||
"newVersionTitle": "Доступна нова версія @Version",
|
||||
"newVersionUpdate": "Оновити зараз",
|
||||
"newVersionUpdate": "Оновити",
|
||||
"newVersionLater": "Пізніше",
|
||||
"extensions": "Розширення",
|
||||
"extensionInstallUrl": "URL для встановлення",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "Thêm",
|
||||
"edit": "Sửa",
|
||||
"newVersionTitle": "Khám phá phiên bản mới @version",
|
||||
"newVersionUpdate": "Cập nhật ngay",
|
||||
"newVersionUpdate": "Cập nhật",
|
||||
"newVersionLater": "Sau",
|
||||
"extensions": "Tiện ích mở rộng",
|
||||
"extensionInstallUrl": "Liên kết cài đặt",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "添加",
|
||||
"edit": "编辑",
|
||||
"newVersionTitle": "发现新版本 @version",
|
||||
"newVersionUpdate": "立即更新",
|
||||
"newVersionUpdate": "更新",
|
||||
"newVersionLater": "稍后再说",
|
||||
"extensions": "扩展",
|
||||
"extensionInstallUrl": "安装链接",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"add": "新增",
|
||||
"edit": "編輯",
|
||||
"newVersionTitle": "發現新版本 @version",
|
||||
"newVersionUpdate": "立即更新",
|
||||
"newVersionUpdate": "更新",
|
||||
"newVersionLater": "稍後",
|
||||
"extensions": "擴充功能",
|
||||
"extensionInstallUrl": "安裝 URL",
|
||||
|
||||
@@ -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,25 @@ 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),
|
||||
Flexible(child: 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,22 @@ 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),
|
||||
Flexible(child: titleTrailing!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 44),
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gopeed/api/model/extension.dart';
|
||||
import 'package:gopeed/api/model/store_extension.dart';
|
||||
import 'package:gopeed/features/extensions/application/extensions_controller.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/pages/extension_details_page.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/widgets/extension_detail_view.dart';
|
||||
import 'package:gopeed/shared/theme/app_theme.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
void main() {
|
||||
for (final mobile in [false, true]) {
|
||||
testWidgets('local README renders while the market request is pending (mobile: $mobile)', (tester) async {
|
||||
final directory = Directory.systemTemp.createTempSync('gopeed-readme-');
|
||||
addTearDown(() => directory.deleteSync(recursive: true));
|
||||
File(path.join(directory.path, 'README.md')).writeAsStringSync('# Local README');
|
||||
final installed = _installed(directory.path);
|
||||
final controller = _ReadmeController(
|
||||
ExtensionsState(installedExtensions: [installed], loadingStore: true),
|
||||
pendingMarket: true,
|
||||
);
|
||||
await _pumpDetails(tester, controller, ExtensionListItem(installed: installed), mobile: mobile);
|
||||
|
||||
expect(controller.marketRequest.isCompleted, isFalse);
|
||||
expect(find.text('Local README'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// A stale market response must not replace the installed documentation.
|
||||
controller.finishMarket();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Local README'), findsOneWidget);
|
||||
expect(find.text('Old market README'), findsNothing);
|
||||
});
|
||||
|
||||
for (final mutate in [false, true]) {
|
||||
testWidgets('open README reloads after an upgrade (mobile: $mobile, mutate: $mutate)', (tester) async {
|
||||
final directory = Directory.systemTemp.createTempSync('gopeed-readme-');
|
||||
addTearDown(() => directory.deleteSync(recursive: true));
|
||||
final readme = File(path.join(directory.path, 'README.md'))..writeAsStringSync('# Before upgrade');
|
||||
final installed = _installed(directory.path);
|
||||
final controller = _ReadmeController(
|
||||
ExtensionsState(installedExtensions: [installed], storeExtensions: [_store()]),
|
||||
);
|
||||
await _pumpDetails(
|
||||
tester,
|
||||
controller,
|
||||
ExtensionListItem(installed: installed, store: _store()),
|
||||
mobile: mobile,
|
||||
);
|
||||
expect(find.text('Before upgrade'), findsOneWidget);
|
||||
|
||||
readme.writeAsStringSync('# After upgrade');
|
||||
controller.publishVersion('2.0.0', mutate: mutate);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('After upgrade'), findsOneWidget);
|
||||
expect(find.text('Before upgrade'), findsNothing);
|
||||
expect(find.text('Old market README'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('missing local README falls back to the store copy', (tester) async {
|
||||
final directory = Directory.systemTemp.createTempSync('gopeed-readme-');
|
||||
addTearDown(() => directory.deleteSync(recursive: true));
|
||||
final installed = _installed(directory.path);
|
||||
final controller = _ReadmeController(
|
||||
ExtensionsState(installedExtensions: [installed], storeExtensions: [_store()]),
|
||||
);
|
||||
await _pumpDetails(tester, controller, ExtensionListItem(installed: installed, store: _store()), mobile: false);
|
||||
expect(find.text('Old market README'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpDetails(
|
||||
WidgetTester tester,
|
||||
_ReadmeController controller,
|
||||
ExtensionListItem item, {
|
||||
required bool mobile,
|
||||
}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = Size(mobile ? 390 : 1100, 900);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(() => controller)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: mobile
|
||||
? ExtensionDetailsPage(extensionId: item.id, initialItem: item)
|
||||
: ExtensionDetailDrawer(item: item, onClose: () {}),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
class _ReadmeController extends ExtensionsController {
|
||||
_ReadmeController(this.initial, {this.pendingMarket = false});
|
||||
|
||||
final ExtensionsState initial;
|
||||
final bool pendingMarket;
|
||||
final marketRequest = Completer<ExtensionsState>();
|
||||
|
||||
@override
|
||||
Future<ExtensionsState> build() async {
|
||||
if (!pendingMarket) return initial;
|
||||
state = AsyncValue.data(initial);
|
||||
return marketRequest.future;
|
||||
}
|
||||
|
||||
void finishMarket() {
|
||||
marketRequest.complete(state.requireValue.copyWith(storeExtensions: [_store()], loadingStore: false));
|
||||
}
|
||||
|
||||
void publishVersion(String version, {required bool mutate}) {
|
||||
final current = state.requireValue;
|
||||
final installed = current.installedExtensions.single;
|
||||
final updated = mutate ? installed : Extension.fromJson(installed.toJson());
|
||||
updated.version = version;
|
||||
state = AsyncValue.data(current.copyWith(installedExtensions: [updated]));
|
||||
}
|
||||
}
|
||||
|
||||
Extension _installed(String directory) => Extension(
|
||||
identity: 'gopeed@test',
|
||||
name: 'test',
|
||||
author: 'Gopeed',
|
||||
title: 'Test extension',
|
||||
description: 'Installed extension',
|
||||
icon: '',
|
||||
version: '1.0.0',
|
||||
homepage: '',
|
||||
repository: null,
|
||||
disabled: false,
|
||||
devMode: true,
|
||||
devPath: directory,
|
||||
);
|
||||
|
||||
StoreExtension _store() => StoreExtension(
|
||||
id: 'gopeed@test',
|
||||
repoFullName: 'GopeedLab/test',
|
||||
repoUrl: 'https://github.com/GopeedLab/test',
|
||||
name: 'test',
|
||||
author: 'Gopeed',
|
||||
title: 'Test extension',
|
||||
description: 'Store extension',
|
||||
version: '1.0.0',
|
||||
readme: '# Old market README',
|
||||
installCount: 1,
|
||||
stars: 1,
|
||||
topics: const [],
|
||||
);
|
||||
@@ -50,6 +50,7 @@ import 'package:gopeed/features/home/presentation/widgets/tasks_top_bar.dart';
|
||||
import 'package:gopeed/features/extensions/application/extensions_controller.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/pages/extension_details_page.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/pages/extensions_page.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/widgets/extension_icon.dart';
|
||||
import 'package:gopeed/features/home/presentation/widgets/primary_rail.dart';
|
||||
import 'package:gopeed/features/tasks/application/pending_update_task.dart';
|
||||
import 'package:gopeed/features/tasks/application/task_batch_selection_controller.dart';
|
||||
@@ -688,6 +689,30 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
for (final width in [320.0, 1100.0]) {
|
||||
testWidgets('extension update status keeps the card header at icon height at $width', (tester) async {
|
||||
await _setTestSize(tester, Size(width, 900));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(UpdateableExtensionsController.new)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final card = find.byKey(const ValueKey('extension-card-extension-0'));
|
||||
final icon = find.descendant(of: card, matching: find.byType(ExtensionIcon));
|
||||
final description = find.descendant(of: card, matching: find.text('Installed extension'));
|
||||
expect(find.byKey(const ValueKey('extension-card-update-status-extension-0')), findsOneWidget);
|
||||
expect(tester.getRect(description).top - tester.getRect(icon).bottom, closeTo(10, 0.01));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('extension details reuse the desktop detail drawer', (WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(1100, 900));
|
||||
final controller = FakeExtensionsController();
|
||||
@@ -817,7 +842,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'));
|
||||
@@ -2327,7 +2354,7 @@ void main() {
|
||||
final progress = find.descendant(of: updateButton, matching: find.byType(shad.CircularProgressIndicator));
|
||||
expect(progress, findsOneWidget);
|
||||
expect(tester.getSize(progress), const Size.square(14));
|
||||
expect(find.descendant(of: updateButton, matching: find.text('Update Now')), findsOneWidget);
|
||||
expect(find.descendant(of: updateButton, matching: find.text('Update')), findsOneWidget);
|
||||
|
||||
runtimeController.completeTrackerUpdate();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Reference in new issue
Block a user