perf(extensions): load store summaries and check updates asynchronously (#1583)

This commit is contained in:
Levi authored and GitHub committed 2026-10-14 20:08:41 +08:00
1 parent b86c2c18e7
commit 12860bb207
41 files changed
+1567 -257

No files matched your search

@@ -3,7 +3,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../../../../l10n/l10n.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_search_field.dart';
class HistorySearchField extends StatelessWidget {
const HistorySearchField({super.key, required this.controller, required this.onChanged});
@@ -18,7 +18,7 @@ class HistorySearchField extends StatelessWidget {
return ConstrainedBox(
// Allow large accessibility text to grow instead of clipping the line.
constraints: const BoxConstraints(minHeight: 38),
child: AppTextField(
child: AppSearchField(
key: const ValueKey('create-history-filter'),
controller: controller,
style: textStyle,
@@ -30,7 +30,7 @@ class HistorySearchField extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
features: [InputFeature.leading(Icon(Icons.search_rounded, size: 15, color: palette.textMuted))],
iconSize: 15,
onChanged: onChanged,
),
);
@@ -1,10 +1,8 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../../../../l10n/l10n.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_tooltip.dart';
import '../../../../shared/widgets/app_empty_state.dart';
/// Keeps the action inside the localized sentence, including RTL word order.
class TaskEmptyCreateHint extends StatelessWidget {
const TaskEmptyCreateHint({super.key, required this.onCreateTask});
@@ -12,38 +10,12 @@ class TaskEmptyCreateHint extends StatelessWidget {
@override
Widget build(BuildContext context) {
const actionMarker = '\uFFFC';
final parts = context.l10n.emptyTaskCreateHint(actionMarker).split(actionMarker);
return Text.rich(
TextSpan(
children: [
TextSpan(text: parts.first.trimRight()),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: AppTooltip(
message: context.l10n.create,
child: Semantics(
label: context.l10n.create,
child: SizedBox.square(
dimension: 30,
child: IconButton.outline(
key: const ValueKey('tasks-empty-create-button'),
density: ButtonDensity.compact,
onPressed: onCreateTask,
icon: const Icon(Icons.add, size: 16),
),
),
),
),
),
),
TextSpan(text: parts.last.trimLeft()),
],
),
textAlign: TextAlign.center,
style: TextStyle(color: AppPalette.of(context).textMuted, fontSize: 13),
return AppEmptyStateAction(
buttonKey: const ValueKey('tasks-empty-create-button'),
icon: Icons.add,
label: context.l10n.create,
hint: context.l10n.emptyTaskCreateHint,
onPressed: onCreateTask,
);
}
}
@@ -1,9 +1,8 @@
import 'package:flutter/widgets.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import 'task_empty_create_hint.dart';
import '../../../../l10n/l10n.dart';
import '../../../../shared/widgets/app_empty_state.dart';
import 'task_empty_create_hint.dart';
class TaskEmptyState extends StatelessWidget {
const TaskEmptyState({super.key, this.message, this.onCreateTask});
@@ -13,101 +12,11 @@ class TaskEmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return Center(
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CustomPaint(
key: const ValueKey('task-empty-illustration'),
size: const Size(148, 108),
painter: _TaskEmptyIllustrationPainter(palette: palette),
),
const SizedBox(height: 18),
Text(
message ?? context.l10n.emptyTaskList,
textAlign: TextAlign.center,
style: TextStyle(color: palette.textPrimary, fontSize: 17, fontWeight: FontWeight.w700),
),
if (onCreateTask != null) ...[
const SizedBox(height: AppDesignTokens.space24),
TaskEmptyCreateHint(onCreateTask: onCreateTask!),
],
],
),
),
),
),
return AppEmptyState(
message: message ?? context.l10n.emptyTaskList,
illustration: AppEmptyIllustrationType.inbox,
illustrationKey: const ValueKey('task-empty-illustration'),
action: onCreateTask == null ? null : TaskEmptyCreateHint(onCreateTask: onCreateTask!),
);
}
}
class _TaskEmptyIllustrationPainter extends CustomPainter {
const _TaskEmptyIllustrationPainter({required this.palette});
final AppPalette palette;
@override
void paint(Canvas canvas, Size size) {
final borderPaint = Paint()
..color = palette.border
..style = PaintingStyle.stroke
..strokeWidth = 1.4;
final sheetRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 34, 6, 68, 72), const Radius.circular(7));
canvas.drawRRect(sheetRect.shift(const Offset(0, 3)), Paint()..color = palette.textMuted.withValues(alpha: 0.06));
canvas.drawRRect(sheetRect, Paint()..color = palette.cardBg);
canvas.drawRRect(sheetRect, borderPaint);
final rowPaint = Paint()
..color = palette.textMuted.withValues(alpha: 0.34)
..strokeWidth = 2
..strokeCap = StrokeCap.round;
final centerX = size.width / 2;
for (var row = 0; row < 2; row++) {
final y = 20.0 + row * 14;
canvas.drawCircle(Offset(centerX - 22, y), 2.5, Paint()..color = palette.surfaceSoft);
canvas.drawLine(Offset(centerX - 14, y), Offset(centerX + 21, y), rowPaint);
}
final emptyBadgePaint = Paint()
..color = palette.surfaceSoft
..style = PaintingStyle.fill;
canvas.drawCircle(Offset(centerX, 56), 11, emptyBadgePaint);
canvas.drawCircle(Offset(centerX, 56), 11, borderPaint);
canvas.drawLine(Offset(centerX - 4, 56), Offset(centerX + 4, 56), rowPaint);
final trayPath = Path()
..moveTo(26, 63)
..lineTo(49, 63)
..lineTo(57, 75)
..quadraticBezierTo(59, 78, 63, 78)
..lineTo(85, 78)
..quadraticBezierTo(89, 78, 91, 75)
..lineTo(99, 63)
..lineTo(122, 63)
..lineTo(115, 96)
..quadraticBezierTo(114, 101, 108, 101)
..lineTo(40, 101)
..quadraticBezierTo(34, 101, 33, 96)
..close();
canvas.drawPath(trayPath, Paint()..color = palette.surfaceSoft);
final trayPaint = Paint()
..color = palette.textSecondary.withValues(alpha: 0.72)
..style = PaintingStyle.stroke
..strokeWidth = 1.8
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
canvas.drawPath(trayPath, trayPaint);
}
@override
bool shouldRepaint(_TaskEmptyIllustrationPainter oldDelegate) {
return oldDelegate.palette != palette;
}
}