mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 01:20:27 +00:00
perf(extensions): load store summaries and check updates asynchronously (#1583)
This commit is contained in:
41 files changed
+1567
-257
No files matched your search
@@ -17,7 +17,9 @@ import '../../../../shared/theme/app_design_tokens.dart';
|
||||
import '../../../../shared/theme/app_palette.dart';
|
||||
import '../../../../shared/widgets/app_swipe_tabs.dart';
|
||||
import '../../../../shared/widgets/app_primary_button.dart';
|
||||
import '../../../../shared/widgets/app_empty_state.dart';
|
||||
import '../../../../shared/widgets/app_text_field.dart';
|
||||
import '../../../../shared/widgets/app_search_field.dart';
|
||||
import '../../../../shared/widgets/app_tooltip.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
import '../../../../l10n/l10n.dart';
|
||||
@@ -124,10 +126,8 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
||||
),
|
||||
child: stateAsync.when(
|
||||
loading: () => _buildContent(const ExtensionsState(loadingInstalled: true, loadingStore: true)),
|
||||
error: (error, _) => _ErrorState(
|
||||
error: error,
|
||||
onRetry: () => ref.read(extensionsControllerProvider.notifier).loadInitialData(),
|
||||
),
|
||||
error: (error, _) =>
|
||||
_ErrorState(onRetry: () => ref.read(extensionsControllerProvider.notifier).loadInitialData()),
|
||||
data: (state) {
|
||||
if (state.listFilter == ExtensionListFilter.market &&
|
||||
state.storePagination?.hasNext == true &&
|
||||
@@ -409,11 +409,17 @@ class _Content extends StatelessWidget {
|
||||
)
|
||||
else if (state.displayItems.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Text(
|
||||
context.l10n.noExtensions,
|
||||
style: TextStyle(color: palette.textSecondary, fontSize: 14),
|
||||
),
|
||||
hasScrollBody: false,
|
||||
child: AppEmptyState(
|
||||
key: const ValueKey('extensions-empty-state'),
|
||||
illustration:
|
||||
state.listFilter == ExtensionListFilter.market && state.storeQuery.isNotEmpty
|
||||
? AppEmptyIllustrationType.search
|
||||
: AppEmptyIllustrationType.puzzle,
|
||||
illustrationKey: const ValueKey('extensions-empty-illustration'),
|
||||
message: state.listFilter == ExtensionListFilter.market && state.storeQuery.isNotEmpty
|
||||
? context.l10n.extensionStoreEmpty
|
||||
: context.l10n.noExtensions,
|
||||
),
|
||||
)
|
||||
else
|
||||
@@ -663,12 +669,11 @@ class _Toolbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
final search = AppTextField(
|
||||
final search = AppSearchField(
|
||||
key: const ValueKey('extension-search-input'),
|
||||
controller: searchController,
|
||||
padding: MediaQuery.sizeOf(context).width >= Breakpoints.mobile ? AppDesignTokens.compactTextFieldPadding : null,
|
||||
placeholder: Text(context.l10n.searchExtensions, style: TextStyle(color: palette.searchHint)),
|
||||
features: [shad.InputFeature.leading(Icon(Icons.search_rounded, size: 16, color: palette.textMuted))],
|
||||
onSubmitted: onSearch,
|
||||
);
|
||||
final sorting = Row(
|
||||
@@ -1255,36 +1260,23 @@ class _ExtensionSettingsPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _ErrorState extends StatelessWidget {
|
||||
const _ErrorState({required this.error, required this.onRetry});
|
||||
const _ErrorState({required this.onRetry});
|
||||
|
||||
final Object error;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.extension_off_outlined, size: 32, color: palette.error),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
context.l10n.unableLoadExtensions,
|
||||
style: TextStyle(color: palette.textPrimary, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: palette.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
shad.SecondaryButton(onPressed: onRetry, child: Text(context.l10n.retry)),
|
||||
],
|
||||
),
|
||||
return AppEmptyState(
|
||||
key: const ValueKey('extensions-error-state'),
|
||||
illustration: AppEmptyIllustrationType.connectionError,
|
||||
illustrationKey: const ValueKey('extensions-error-illustration'),
|
||||
message: context.l10n.unableLoadExtensions,
|
||||
action: AppEmptyStateAction(
|
||||
buttonKey: const ValueKey('extensions-error-retry'),
|
||||
icon: Icons.refresh_rounded,
|
||||
label: context.l10n.retry,
|
||||
hint: context.l10n.emptyStateRetryHint,
|
||||
onPressed: onRetry,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ import '../../../../l10n/l10n.dart';
|
||||
import '../../../../shared/theme/app_design_tokens.dart';
|
||||
import '../../../../shared/theme/app_palette.dart';
|
||||
import '../../../../shared/widgets/app_loading_button.dart';
|
||||
import '../../../../shared/widgets/app_empty_state.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
import '../../../../shared/widgets/detail/app_detail_surface.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../application/extensions_controller.dart';
|
||||
import '../../application/store_extension_details_provider.dart';
|
||||
import 'extension_icon.dart';
|
||||
import 'extension_update_dialog.dart';
|
||||
import 'extension_update_status.dart';
|
||||
@@ -63,7 +65,16 @@ class ExtensionDetailView extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final palette = AppPalette.of(context);
|
||||
final state = ref.watch(extensionsControllerProvider).value;
|
||||
final current = state?.findItem(item.id) ?? item;
|
||||
final summary = state?.findItem(item.id) ?? item;
|
||||
final summaryStore = summary.store;
|
||||
// Native installed extensions provide their manifest and local README.
|
||||
// Web cannot read the local files and still needs the store README.
|
||||
final useLocalDetails = summary.isInstalled && !Util.isWeb();
|
||||
final detailKey = useLocalDetails || summaryStore == null || summaryStore.hasDetails
|
||||
? null
|
||||
: (id: summaryStore.id, version: summaryStore.version);
|
||||
final details = detailKey == null ? null : ref.watch(storeExtensionDetailsProvider(detailKey));
|
||||
final current = ExtensionListItem(installed: summary.installed, store: details?.value ?? summaryStore);
|
||||
final installed = current.installed;
|
||||
final store = current.store;
|
||||
final busy = state?.busyExtensionIds.contains(current.id) ?? false;
|
||||
@@ -116,7 +127,7 @@ class ExtensionDetailView extends ConsumerWidget {
|
||||
'v${current.version}',
|
||||
style: TextStyle(color: palette.textMuted, fontSize: 11.5, fontWeight: FontWeight.w600),
|
||||
),
|
||||
if (store != null) _ExtensionStats(store: store),
|
||||
if (summaryStore != null) _ExtensionStats(store: summaryStore),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -164,7 +175,31 @@ class ExtensionDetailView extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: mobile ? 24 : 28),
|
||||
if (details?.isLoading == true)
|
||||
const Padding(
|
||||
key: ValueKey('extension-details-loading'),
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: Center(
|
||||
child: SizedBox.square(dimension: 44, child: Center(child: shad.CircularProgressIndicator())),
|
||||
),
|
||||
),
|
||||
if (details?.hasError == true && details?.isLoading != true && detailKey != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
child: AppEmptyState(
|
||||
key: const ValueKey('extension-details-error-state'),
|
||||
illustration: AppEmptyIllustrationType.documentError,
|
||||
message: context.l10n.unableLoadExtensionDetails,
|
||||
illustrationKey: const ValueKey('extension-details-error-illustration'),
|
||||
action: AppEmptyStateAction(
|
||||
buttonKey: const ValueKey('extension-details-retry'),
|
||||
icon: Icons.refresh_rounded,
|
||||
label: context.l10n.retry,
|
||||
hint: context.l10n.emptyStateRetryHint,
|
||||
onPressed: () => ref.invalidate(storeExtensionDetailsProvider(detailKey)),
|
||||
),
|
||||
),
|
||||
),
|
||||
_ExtensionReadme(
|
||||
// Snapshot the version so upgrades reload the installed README.
|
||||
key: ValueKey((current.id, installed?.version, installed?.devMode, installed?.devPath, store?.readme)),
|
||||
@@ -213,7 +248,7 @@ class _ExtensionReadmeState extends State<_ExtensionReadme> {
|
||||
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),
|
||||
padding: const EdgeInsets.only(top: AppDesignTokens.space24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
Reference in new issue
Block a user