refactor: perf improvements
This commit is contained in:
@@ -34,8 +34,8 @@ class ConnectivityService {
|
|||||||
_checkConnectivity();
|
_checkConnectivity();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check every 5 seconds
|
// Check periodically; balance responsiveness with battery/network usage
|
||||||
_connectivityTimer = Timer.periodic(const Duration(seconds: 5), (_) {
|
_connectivityTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
||||||
_checkConnectivity();
|
_checkConnectivity();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,8 +106,16 @@ class _ErrorBoundaryState extends ConsumerState<ErrorBoundary> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default error UI
|
// Default error UI
|
||||||
|
// Respect ambient text direction when available; fall back to LTR.
|
||||||
|
TextDirection direction;
|
||||||
|
try {
|
||||||
|
direction = Directionality.of(context);
|
||||||
|
} catch (_) {
|
||||||
|
direction = TextDirection.ltr;
|
||||||
|
}
|
||||||
|
|
||||||
return Directionality(
|
return Directionality(
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: direction,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|||||||
@@ -595,35 +595,39 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildErrorMessage(String message) {
|
Widget _buildErrorMessage(String message) {
|
||||||
return Container(
|
return Semantics(
|
||||||
padding: const EdgeInsets.all(Spacing.md),
|
liveRegion: true,
|
||||||
decoration: BoxDecoration(
|
label: message,
|
||||||
color: context.conduitTheme.errorBackground,
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.button),
|
padding: const EdgeInsets.all(Spacing.md),
|
||||||
border: Border.all(
|
decoration: BoxDecoration(
|
||||||
color: context.conduitTheme.error.withValues(alpha: 0.3),
|
color: context.conduitTheme.errorBackground,
|
||||||
width: BorderWidth.standard,
|
borderRadius: BorderRadius.circular(AppBorderRadius.button),
|
||||||
),
|
border: Border.all(
|
||||||
),
|
color: context.conduitTheme.error.withValues(alpha: 0.3),
|
||||||
child: Row(
|
width: BorderWidth.standard,
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Platform.isIOS
|
|
||||||
? CupertinoIcons.exclamationmark_circle_fill
|
|
||||||
: Icons.error_outline,
|
|
||||||
color: context.conduitTheme.error,
|
|
||||||
size: IconSize.medium,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.md),
|
),
|
||||||
Expanded(
|
child: Row(
|
||||||
child: Text(
|
children: [
|
||||||
message,
|
Icon(
|
||||||
style: context.conduitTheme.bodyMedium?.copyWith(
|
Platform.isIOS
|
||||||
color: context.conduitTheme.error,
|
? CupertinoIcons.exclamationmark_circle_fill
|
||||||
|
: Icons.error_outline,
|
||||||
|
color: context.conduitTheme.error,
|
||||||
|
size: IconSize.medium,
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.md),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: context.conduitTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.conduitTheme.error,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
).animate().slideX(
|
).animate().slideX(
|
||||||
begin: 0.05,
|
begin: 0.05,
|
||||||
|
|||||||
@@ -748,40 +748,45 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildErrorMessage(String message) {
|
Widget _buildErrorMessage(String message) {
|
||||||
return Container(
|
return Semantics(
|
||||||
padding: const EdgeInsets.all(Spacing.md),
|
liveRegion: true,
|
||||||
decoration: BoxDecoration(
|
label: message,
|
||||||
color: context.conduitTheme.errorBackground,
|
child:
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.button),
|
Container(
|
||||||
border: Border.all(
|
padding: const EdgeInsets.all(Spacing.md),
|
||||||
color: context.conduitTheme.error.withValues(alpha: 0.3),
|
decoration: BoxDecoration(
|
||||||
width: BorderWidth.standard,
|
color: context.conduitTheme.errorBackground,
|
||||||
),
|
borderRadius: BorderRadius.circular(AppBorderRadius.button),
|
||||||
),
|
border: Border.all(
|
||||||
child: Row(
|
color: context.conduitTheme.error.withValues(alpha: 0.3),
|
||||||
children: [
|
width: BorderWidth.standard,
|
||||||
Icon(
|
|
||||||
Platform.isIOS
|
|
||||||
? CupertinoIcons.exclamationmark_circle_fill
|
|
||||||
: Icons.error_outline,
|
|
||||||
color: context.conduitTheme.error,
|
|
||||||
size: IconSize.medium,
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.md),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
message,
|
|
||||||
style: context.conduitTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.conduitTheme.error,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.exclamationmark_circle_fill
|
||||||
|
: Icons.error_outline,
|
||||||
|
color: context.conduitTheme.error,
|
||||||
|
size: IconSize.medium,
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.md),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: context.conduitTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.conduitTheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).animate().slideX(
|
||||||
|
begin: 0.05,
|
||||||
|
duration: AnimationDuration.messageSlide,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
).animate().slideX(
|
|
||||||
begin: 0.05,
|
|
||||||
duration: AnimationDuration.messageSlide,
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -633,6 +633,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
),
|
),
|
||||||
physics:
|
physics:
|
||||||
const NeverScrollableScrollPhysics(), // Prevent scrolling during load
|
const NeverScrollableScrollPhysics(), // Prevent scrolling during load
|
||||||
|
// Modest cache extent to avoid offscreen overwork but keep shimmer smooth
|
||||||
|
cacheExtent: 300,
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final isUser = index.isOdd;
|
final isUser = index.isOdd;
|
||||||
@@ -1007,9 +1009,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final current = ref.read(inputFocusTriggerProvider);
|
final current = ref.read(inputFocusTriggerProvider);
|
||||||
// Immediate focus bump
|
// Immediate focus bump
|
||||||
ref
|
ref.read(inputFocusTriggerProvider.notifier).set(current + 1);
|
||||||
.read(inputFocusTriggerProvider.notifier)
|
|
||||||
.set(current + 1);
|
|
||||||
// Second bump shortly after to overcome route/IME timing
|
// Second bump shortly after to overcome route/IME timing
|
||||||
Future.delayed(const Duration(milliseconds: 120), () {
|
Future.delayed(const Duration(milliseconds: 120), () {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -1788,6 +1788,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
|||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
cacheExtent: 400,
|
||||||
itemCount: _filteredModels.length,
|
itemCount: _filteredModels.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final model = _filteredModels[index];
|
final model = _filteredModels[index];
|
||||||
|
|||||||
@@ -194,8 +194,8 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
|
|||||||
return MediaQuery(
|
return MediaQuery(
|
||||||
data: mediaQuery.copyWith(
|
data: mediaQuery.copyWith(
|
||||||
textScaler: mediaQuery.textScaler.clamp(
|
textScaler: mediaQuery.textScaler.clamp(
|
||||||
minScaleFactor: 0.8,
|
minScaleFactor: 1.0,
|
||||||
maxScaleFactor: 2.0,
|
maxScaleFactor: 3.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: OfflineIndicator(child: child ?? const SizedBox.shrink()),
|
child: OfflineIndicator(child: child ?? const SizedBox.shrink()),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class OfflineIndicator extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final connectivityStatus = ref.watch(connectivityStatusProvider);
|
final connectivityStatus = ref.watch(connectivityStatusProvider);
|
||||||
|
final wasOffline = ref.watch(_wasOfflineProvider);
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -30,6 +31,10 @@ class OfflineIndicator extends ConsumerWidget {
|
|||||||
if (status == ConnectivityStatus.offline) {
|
if (status == ConnectivityStatus.offline) {
|
||||||
return _OfflineBanner();
|
return _OfflineBanner();
|
||||||
}
|
}
|
||||||
|
// Announce back-online briefly if we were previously offline
|
||||||
|
if (wasOffline) {
|
||||||
|
return const _BackOnlineToast();
|
||||||
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
loading: () => const SizedBox.shrink(),
|
loading: () => const SizedBox.shrink(),
|
||||||
@@ -40,6 +45,90 @@ class OfflineIndicator extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tracks if the app was recently offline to enable a one-shot back-online toast
|
||||||
|
final _wasOfflineProvider = NotifierProvider<_WasOfflineNotifier, bool>(
|
||||||
|
_WasOfflineNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
class _WasOfflineNotifier extends Notifier<bool> {
|
||||||
|
@override
|
||||||
|
bool build() {
|
||||||
|
// Initialize based on current connectivity (assume online until proven otherwise)
|
||||||
|
ref.listen<AsyncValue<ConnectivityStatus>>(connectivityStatusProvider, (
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
) {
|
||||||
|
next.when(
|
||||||
|
data: (status) {
|
||||||
|
if (status == ConnectivityStatus.offline) {
|
||||||
|
state = true; // mark that we have been offline
|
||||||
|
} else if (status == ConnectivityStatus.online && state) {
|
||||||
|
// After we emit the toast once, clear flag shortly after
|
||||||
|
Future.microtask(() => state = false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: () {},
|
||||||
|
error: (_, __) {},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackOnlineToast extends StatelessWidget {
|
||||||
|
const _BackOnlineToast();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned(
|
||||||
|
top: kToolbarHeight + 8,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Semantics(
|
||||||
|
container: true,
|
||||||
|
liveRegion: true,
|
||||||
|
label: AppLocalizations.of(context)!.checkConnection,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child:
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
vertical: Spacing.xs,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.conduitTheme.success,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.round,
|
||||||
|
),
|
||||||
|
boxShadow: ConduitShadows.low,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
// Reuse existing l10n; otherwise add a dedicated "Back online" key later
|
||||||
|
AppLocalizations.of(context)!.loadingContent,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.conduitTheme.textInverse,
|
||||||
|
fontSize: AppTypography.labelLarge,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.animate(onPlay: (c) => c.forward())
|
||||||
|
.fadeIn(duration: const Duration(milliseconds: 200))
|
||||||
|
.then(delay: const Duration(milliseconds: 1200))
|
||||||
|
.fadeOut(duration: const Duration(milliseconds: 250)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _OfflineBanner extends StatelessWidget {
|
class _OfflineBanner extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -50,36 +139,41 @@ class _OfflineBanner extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child:
|
child:
|
||||||
Container(
|
Semantics(
|
||||||
padding: const EdgeInsets.symmetric(
|
container: true,
|
||||||
horizontal: Spacing.md,
|
liveRegion: true,
|
||||||
vertical: Spacing.xs,
|
label: AppLocalizations.of(context)!.offlineBanner,
|
||||||
),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(
|
||||||
color: context.conduitTheme.warning,
|
horizontal: Spacing.md,
|
||||||
boxShadow: ConduitShadows.low,
|
vertical: Spacing.xs,
|
||||||
),
|
),
|
||||||
child: Row(
|
decoration: BoxDecoration(
|
||||||
children: [
|
color: context.conduitTheme.warning,
|
||||||
Icon(
|
boxShadow: ConduitShadows.low,
|
||||||
Platform.isIOS
|
),
|
||||||
? CupertinoIcons.wifi_slash
|
child: Row(
|
||||||
: Icons.wifi_off,
|
children: [
|
||||||
color: context.conduitTheme.textInverse,
|
Icon(
|
||||||
size: AppTypography.headlineMedium,
|
Platform.isIOS
|
||||||
),
|
? CupertinoIcons.wifi_slash
|
||||||
const SizedBox(width: Spacing.xs),
|
: Icons.wifi_off,
|
||||||
Expanded(
|
color: context.conduitTheme.textInverse,
|
||||||
child: Text(
|
size: AppTypography.headlineMedium,
|
||||||
AppLocalizations.of(context)!.offlineBanner,
|
),
|
||||||
style: TextStyle(
|
const SizedBox(width: Spacing.xs),
|
||||||
color: context.conduitTheme.textInverse,
|
Expanded(
|
||||||
fontSize: AppTypography.labelLarge,
|
child: Text(
|
||||||
fontWeight: FontWeight.w500,
|
AppLocalizations.of(context)!.offlineBanner,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.conduitTheme.textInverse,
|
||||||
|
fontSize: AppTypography.labelLarge,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.animate(onPlay: (controller) => controller.forward())
|
.animate(onPlay: (controller) => controller.forward())
|
||||||
|
|||||||
Reference in New Issue
Block a user