feat: внятный показ обрыва сессии

This commit is contained in:
Jganenokk
2026-06-19 01:19:11 +07:00
parent c10817baf6
commit 73cdf7ef7f
27 changed files with 365 additions and 39 deletions
+15 -8
View File
@@ -9,6 +9,7 @@ import '../../../core/storage/app_database.dart';
import '../../../core/utils/format.dart';
import '../../../backend/modules/calls.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/connection_status.dart';
class CallsTab extends StatefulWidget {
const CallsTab({super.key});
@@ -260,14 +261,20 @@ class _CallsTabState extends State<CallsTab> {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
child: Text(
'Звонки',
style: TextStyle(
color: cs.onSurface,
fontSize: 24,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Звонки',
style: TextStyle(
color: cs.onSurface,
fontSize: 24,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
),
),
const ConnectionStatusLine(),
],
),
),
InkWell(
@@ -6,6 +6,7 @@ import '../../../backend/modules/messages.dart' show ContactCache;
import '../../../core/cache/info_cache.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/utils/format.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/komet_avatar.dart';
@@ -190,6 +191,8 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
return Scaffold(
backgroundColor: bg,
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const ConnectionSpinner(),
body: SafeArea(
child: _isLoading ? _buildShimmer(cs) : _buildScrollBody(cs),
),
@@ -39,6 +39,7 @@ import '../../../core/config/komet_settings.dart';
import '../../../models/attachment.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/online_dot.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/message_bubble.dart';
import '../../widgets/theme_reveal.dart';
import '../../widgets/message_actions_overlay.dart';
@@ -110,6 +111,7 @@ class _ChatScreenState extends State<ChatScreen>
StreamSubscription<UploadEvent>? _uploadSub;
StreamSubscription<Packet>? _pushSub;
StreamSubscription<MessageEvent>? _messageEventSub;
StreamSubscription<SessionState>? _connSub;
final Map<String, ValueNotifier<Map<String, dynamic>?>> _reactionNotifiers =
{};
final Map<String, ValueNotifier<List<double>>> _photoUploadProgress = {};
@@ -209,6 +211,10 @@ class _ChatScreenState extends State<ChatScreen>
_messageEventSub = ChatsModule.messageEvents
.where((e) => e.chatId == widget.chatId)
.listen(_onMessageEvent);
_connSub = api.stateStream.listen((_) {
if (mounted) _recomputeHeaderStatus();
});
debugForceOffline.addListener(_recomputeHeaderStatus);
PresenceFetch.revision.addListener(_onPresenceChanged);
_floatingDateAnimController = AnimationController(
vsync: this,
@@ -544,6 +550,8 @@ class _ChatScreenState extends State<ChatScreen>
_uploadSub?.cancel();
_pushSub?.cancel();
_messageEventSub?.cancel();
_connSub?.cancel();
debugForceOffline.removeListener(_recomputeHeaderStatus);
for (final n in _reactionNotifiers.values) {
n.dispose();
}
@@ -1360,6 +1368,8 @@ class _ChatScreenState extends State<ChatScreen>
}
String _headerStatus() {
final conn = connectionStatusLabel(api.state);
if (conn != null) return conn;
if (_typingUserIds.isNotEmpty) return 'Печатает...';
if (widget.chatType == 'CHAT') {
final count = _participantsCount ?? chat?.participants.length ?? 0;
@@ -8,6 +8,7 @@ import '../../../core/utils/format.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/swipe_route.dart';
import '../chats/chat_screen.dart';
@@ -127,6 +128,8 @@ class _ContactProfileScreenState extends State<ContactProfileScreen> {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const ConnectionSpinner(),
body: SafeArea(
child: _loading
? const Center(child: CircularProgressIndicator())
@@ -6,6 +6,7 @@ import '../../../core/storage/app_database.dart';
import '../../../backend/modules/contacts.dart';
import '../../../main.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/sheet_helpers.dart';
import 'contact_profile_screen.dart';
@@ -168,14 +169,21 @@ class _ContactsTabState extends State<ContactsTab> {
child: Row(
children: [
Expanded(
child: Text(
'Контакты',
style: TextStyle(
color: cs.onSurface,
fontSize: 24,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Контакты',
style: TextStyle(
color: cs.onSurface,
fontSize: 24,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
),
),
const ConnectionStatusLine(),
],
),
),
IconButton(
@@ -6,6 +6,7 @@ import '../../../backend/modules/webapp.dart';
import '../../../core/utils/webview_support.dart';
import '../../../main.dart' show digitalIdModule;
import '../../../models/digital_id.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../webapp/web_app_screen.dart';
@@ -155,6 +156,8 @@ class _DigitalIdScreenState extends State<DigitalIdScreen> {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const ConnectionSpinner(),
appBar: AppBar(
backgroundColor: cs.surface,
surfaceTintColor: Colors.transparent,
@@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../../backend/modules/webapp.dart';
import '../../../main.dart' show webAppModule, digitalIdModule;
import '../../widgets/connection_status.dart';
import '../../widgets/webview_permission_prompt.dart';
Future<void> resetDigitalIdWebData() async {
@@ -218,6 +219,8 @@ class _DigitalIdWebScreenState extends State<DigitalIdWebScreen> {
},
child: Scaffold(
backgroundColor: cs.surface,
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const ConnectionSpinner(),
appBar: AppBar(
backgroundColor: cs.surface,
surfaceTintColor: Colors.transparent,
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../widgets/connection_status.dart';
import '../../../core/config/app_icon.dart';
import '../../../core/utils/haptics.dart';
import '../../widgets/custom_notification.dart';
@@ -46,7 +47,7 @@ class _AppIconScreenState extends State<AppIconScreen> {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(
appBar: ConnectionTitleBar(
titleText: 'Иконка приложения',
backgroundColor: cs.surface,
),
@@ -1,9 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../widgets/connection_status.dart';
import '../../../core/config/app_bubble_behavior.dart';
import '../../../core/config/app_bubble_shape.dart';
import '../../../core/config/app_pill_gradient.dart';
@@ -86,7 +87,10 @@ class _AppearanceScreenState extends State<AppearanceScreen> {
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(titleText: 'Внешний вид', backgroundColor: cs.surface),
appBar: ConnectionTitleBar(
titleText: 'Внешний вид',
backgroundColor: cs.surface,
),
body: SafeArea(
top: false,
child: ListView(
@@ -13,6 +13,7 @@ import '../../../backend/modules/upload_manager.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/utils/format.dart';
import '../../../main.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
@@ -315,7 +316,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
onPressed: _onBack,
),
title: Text(
title: ConnectionTitleText(
'Облачное хранилище',
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
),
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../widgets/connection_status.dart';
import '../../../core/utils/haptics.dart';
import '../../widgets/glossy_pill.dart';
import 'app_icon_screen.dart';
@@ -80,7 +81,7 @@ class CustomizationScreen extends StatelessWidget {
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(
appBar: ConnectionTitleBar(
titleText: 'Кастомизация',
backgroundColor: cs.surface,
),
@@ -22,6 +22,7 @@ import '../../widgets/sheet_helpers.dart';
import '../../widgets/login_success_screen.dart';
import '../calls/call_screen.dart';
import '../../../core/calls/call_controller.dart';
import '../../widgets/connection_status.dart';
import '../digital_id/digital_id_web_screen.dart';
class DebugMenuScreen extends StatefulWidget {
@@ -205,6 +206,8 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
return Scaffold(
backgroundColor: cs.surface,
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const ConnectionSpinner(),
body: SafeArea(
bottom: false,
child: CustomScrollView(
@@ -368,6 +371,64 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: ValueListenableBuilder<bool>(
valueListenable: debugForceOffline,
builder: (context, offline, _) {
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
depth: 6,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.wifi_off,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Офлайн (тест)',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Показать индикаторы соединения во всех '
'экранах, не разрывая реальную сессию',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Switch(
value: offline,
onChanged: (v) => debugForceOffline.value = v,
),
],
),
);
},
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
@@ -10,6 +10,7 @@ import '../../../core/utils/format.dart';
import '../../../main.dart' show accountModule;
import '../../../backend/modules/account.dart' show SessionInfo;
import '../../widgets/custom_notification.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
import 'web_qr_scan_screen.dart';
@@ -327,7 +328,7 @@ class _DevicesScreenState extends State<DevicesScreen>
icon: const Icon(Symbols.chevron_left, size: 28),
onPressed: () => Navigator.pop(context),
),
title: Text(
title: ConnectionTitleText(
'Устройства',
style: GoogleFonts.outfit(
fontSize: 20,
@@ -5,6 +5,7 @@ import '../../../core/storage/app_database.dart';
import '../../../core/utils/image_utils.dart';
import '../../../l10n/app_localizations.dart';
import '../../../main.dart' show accountModule, fileUploader, KometApp;
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
class EditProfileScreen extends StatefulWidget {
@@ -170,7 +171,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
onPressed: () => Navigator.pop(context),
),
title: Text(
title: ConnectionTitleText(
l10n?.editProfileTitle ?? 'Edit Profile',
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
),
@@ -5,6 +5,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/utils/haptics.dart';
import '../../../main.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
@@ -139,7 +140,10 @@ class _FontSettingsScreenState extends State<FontSettingsScreen> {
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(titleText: 'Шрифты', backgroundColor: cs.surface),
appBar: ConnectionTitleBar(
titleText: 'Шрифты',
backgroundColor: cs.surface,
),
body: SafeArea(
top: false,
child: ListView(
@@ -4,6 +4,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/storage/token_storage.dart';
import '../../../l10n/app_localizations.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/section_header.dart';
@@ -60,7 +61,7 @@ class _InfoScreenState extends State<InfoScreen> {
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
onPressed: () => Navigator.pop(context),
),
title: Text(
title: ConnectionTitleText(
l10n?.infoTitle ?? 'Info',
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
),
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../widgets/connection_status.dart';
import '../../../core/config/komet_settings.dart';
import '../../../main.dart';
import '../../widgets/glossy_pill.dart';
@@ -16,7 +17,7 @@ class KometSettingsScreen extends StatelessWidget {
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(titleText: 'Komet', backgroundColor: cs.surface),
appBar: ConnectionTitleBar(titleText: 'Komet', backgroundColor: cs.surface),
body: SafeArea(
top: false,
child: ListView(
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../widgets/connection_status.dart';
import '../../../core/config/app_message_actions_style.dart';
import '../../../core/utils/haptics.dart';
import '../../widgets/glossy_pill.dart';
@@ -14,7 +15,7 @@ class MessageActionsScreen extends StatelessWidget {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(
appBar: ConnectionTitleBar(
titleText: 'Меню действий',
backgroundColor: cs.surface,
),
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/section_header.dart';
import '../../widgets/sheet_helpers.dart';
@@ -89,7 +90,10 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(titleText: 'Уведомления', backgroundColor: cs.surface),
appBar: ConnectionTitleBar(
titleText: 'Уведомления',
backgroundColor: cs.surface,
),
body: SafeArea(
top: false,
child: ListView(
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import '../../widgets/connection_status.dart';
import '../../../core/config/app_cache_extent.dart';
import '../../../core/utils/haptics.dart';
@@ -91,7 +91,7 @@ class _PerformanceScreenState extends State<PerformanceScreen> {
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(
appBar: ConnectionTitleBar(
titleText: 'Производительность',
backgroundColor: cs.surface,
),
@@ -7,6 +7,7 @@ import '../../../backend/modules/account.dart'
import '../../../core/storage/app_database.dart';
import '../../widgets/confirm_dialog.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
import 'password_entry_screen.dart';
@@ -197,7 +198,7 @@ class _SecurityScreenState extends State<SecurityScreen>
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 4),
Text(
ConnectionTitleText(
'Безопасность',
style: TextStyle(
color: cs.onSurface,
@@ -13,6 +13,7 @@ import '../../../core/utils/format.dart';
import '../../../core/utils/haptics.dart';
import '../../../l10n/app_localizations.dart';
import '../../../main.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/info_action_sheet.dart';
import '../../widgets/komet_avatar.dart';
@@ -565,6 +566,9 @@ class _SettingsTabState extends State<SettingsTab> {
),
onPressed: () {},
),
const Expanded(
child: ConnectionStatusLine(textAlign: TextAlign.center),
),
IconButton(
icon: Icon(
Symbols.edit,
@@ -13,6 +13,7 @@ import '../../../core/storage/spoofing_service.dart';
import '../../../core/storage/token_storage.dart';
import '../../../l10n/app_localizations.dart';
import '../../../main.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/info_action_sheet.dart';
import '../../widgets/section_header.dart';
import '../auth/login_screen.dart';
@@ -438,7 +439,10 @@ class _SpoofScreenState extends State<SpoofScreen> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.spoofScreenTitle), centerTitle: true),
appBar: AppBar(
title: ConnectionTitleText(l10n.spoofScreenTitle),
centerTitle: true,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../widgets/connection_status.dart';
import '../../../core/config/app_amoled.dart';
import '../../../core/config/app_theme_mode.dart';
import '../../../core/config/app_theme_schedule.dart';
@@ -18,7 +19,7 @@ class ThemeSettingsScreen extends StatelessWidget {
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(titleText: 'Тема', backgroundColor: cs.surface),
appBar: ConnectionTitleBar(titleText: 'Тема', backgroundColor: cs.surface),
body: SafeArea(
top: false,
child: ListView(
@@ -7,6 +7,8 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../../widgets/connection_status.dart';
class WebQrScanScreen extends StatefulWidget {
const WebQrScanScreen({super.key});
@@ -58,6 +60,8 @@ class _WebQrScanScreenState extends State<WebQrScanScreen> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const ConnectionSpinner(),
appBar: AppBar(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
@@ -3,6 +3,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../backend/modules/webapp.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/webview_permission_prompt.dart';
class WebAppScreen extends StatefulWidget {
@@ -67,6 +68,8 @@ class _WebAppScreenState extends State<WebAppScreen> {
},
child: Scaffold(
backgroundColor: cs.surface,
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const ConnectionSpinner(),
appBar: AppBar(
backgroundColor: cs.surface,
surfaceTintColor: Colors.transparent,
+191
View File
@@ -0,0 +1,191 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import '../../backend/api.dart';
import '../../main.dart' show api;
final ValueNotifier<bool> debugForceOffline = ValueNotifier<bool>(false);
String? connectionStatusLabel(SessionState state) {
if (debugForceOffline.value) return 'Ожидание сети...';
return switch (state) {
SessionState.online => null,
SessionState.connecting || SessionState.connected => 'Соединение...',
SessionState.disconnected => 'Ожидание сети...',
};
}
class ConnectionStatusBuilder extends StatefulWidget {
final Widget Function(BuildContext context, String? label) builder;
const ConnectionStatusBuilder({super.key, required this.builder});
@override
State<ConnectionStatusBuilder> createState() =>
_ConnectionStatusBuilderState();
}
class _ConnectionStatusBuilderState extends State<ConnectionStatusBuilder> {
late SessionState _state = api.state;
StreamSubscription<SessionState>? _sub;
@override
void initState() {
super.initState();
_sub = api.stateStream.listen((s) {
if (mounted && s != _state) setState(() => _state = s);
});
debugForceOffline.addListener(_onOverrideChanged);
}
void _onOverrideChanged() {
if (mounted) setState(() {});
}
@override
void dispose() {
_sub?.cancel();
debugForceOffline.removeListener(_onOverrideChanged);
super.dispose();
}
@override
Widget build(BuildContext context) =>
widget.builder(context, connectionStatusLabel(_state));
}
class ConnectionStatusLine extends StatelessWidget {
final TextAlign? textAlign;
final EdgeInsetsGeometry padding;
const ConnectionStatusLine({
super.key,
this.textAlign,
this.padding = EdgeInsets.zero,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return ConnectionStatusBuilder(
builder: (context, label) => AnimatedSwitcher(
duration: const Duration(milliseconds: 220),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionBuilder: (child, animation) => SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: FadeTransition(opacity: animation, child: child),
),
child: label == null
? const SizedBox(width: double.infinity, key: ValueKey('online'))
: Padding(
key: const ValueKey('offline'),
padding: padding,
child: Text(
label,
textAlign: textAlign,
style: TextStyle(
color: cs.onSurfaceVariant.withValues(alpha: 0.55),
fontSize: 13,
fontWeight: FontWeight.w400,
),
),
),
),
);
}
}
class ConnectionTitleText extends StatelessWidget {
final String title;
final TextStyle? style;
final TextAlign? textAlign;
const ConnectionTitleText(
this.title, {
super.key,
this.style,
this.textAlign,
});
@override
Widget build(BuildContext context) => ConnectionStatusBuilder(
builder: (context, label) => Text(
label ?? title,
style: style,
textAlign: textAlign,
overflow: TextOverflow.ellipsis,
),
);
}
class ConnectionTitleBar extends StatelessWidget implements PreferredSizeWidget {
final String titleText;
final Color? backgroundColor;
const ConnectionTitleBar({
super.key,
required this.titleText,
this.backgroundColor,
});
@override
Size get preferredSize => const Size.fromHeight(64);
@override
Widget build(BuildContext context) => ConnectionStatusBuilder(
builder: (context, label) => AppBarM3E(
titleText: label ?? titleText,
backgroundColor: backgroundColor,
),
);
}
class ConnectionSpinner extends StatelessWidget {
const ConnectionSpinner({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return ConnectionStatusBuilder(
builder: (context, label) => AnimatedSwitcher(
duration: const Duration(milliseconds: 220),
switchInCurve: Curves.easeOutBack,
switchOutCurve: Curves.easeIn,
transitionBuilder: (child, animation) =>
ScaleTransition(scale: animation, child: child),
child: label == null
? const SizedBox.shrink(key: ValueKey('online'))
: Container(
key: const ValueKey('offline'),
width: 40,
height: 40,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2.4,
color: cs.primary,
),
),
),
),
),
);
}
}